From 8e863b8cf54e1c8510363559135156c8e5179fd7 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 27 Sep 2016 16:52:34 +0200 Subject: [PATCH 01/29] Implement new create and manager trigger UI Implements the new trigger setup user interface, which is now a linear workflow found on its own page, rather than a tiny modal dialog Fixes #1187 --- buildtrigger/basehandler.py | 52 ++- buildtrigger/bitbuckethandler.py | 53 ++- buildtrigger/customhandler.py | 15 + buildtrigger/githubhandler.py | 107 +++--- buildtrigger/gitlabhandler.py | 90 ++++- data/model/user.py | 5 + endpoints/api/trigger.py | 91 +++-- endpoints/bitbuckettrigger.py | 3 +- endpoints/githubtrigger.py | 3 +- endpoints/gitlabtrigger.py | 3 +- endpoints/web.py | 11 +- static/css/core-ui.css | 24 +- static/css/directives/ui/linear-workflow.css | 64 ++++ .../directives/ui/manage-trigger-control.css | 106 ++++++ static/css/directives/ui/regex-match-view.css | 36 ++ .../directives/ui/setup-trigger-dialog.css | 28 -- static/css/directives/ui/step-view-step.css | 9 - static/css/pages/trigger-setup.css | 35 ++ static/directives/credentials.html | 6 +- static/directives/dockerfile-path-select.html | 32 ++ .../directives/linear-workflow-section.html | 6 + static/directives/linear-workflow.html | 31 ++ .../directives/manage-trigger-custom-git.html | 43 +++ static/directives/manage-trigger-githost.html | 330 ++++++++++++++++++ static/directives/regex-match-view.html | 29 ++ .../repo-view/repo-panel-builds.html | 15 +- static/directives/setup-trigger-dialog.html | 133 ------- static/directives/step-view-step.html | 9 - static/directives/step-view.html | 3 - static/directives/trigger-setup-custom.html | 40 --- static/directives/trigger-setup-githost.html | 201 ----------- .../custom-git/trigger-description.html | 2 +- .../directives/repo-view/repo-panel-builds.js | 24 -- .../directives/ui/dockerfile-path-select.js | 54 +++ static/js/directives/ui/dropdown-select.js | 31 +- static/js/directives/ui/linear-workflow.js | 141 ++++++++ .../ui/manage-trigger-custom-git.js | 27 ++ .../directives/ui/manage-trigger-githost.js | 306 ++++++++++++++++ static/js/directives/ui/regex-match-view.js | 36 ++ static/js/directives/ui/step-view.js | 126 ------- .../js/directives/ui/trigger-setup-custom.js | 49 --- .../js/directives/ui/trigger-setup-githost.js | 242 ------------- static/js/pages/trigger-setup.js | 89 +++++ static/js/quay.routes.ts | 3 + static/partials/trigger-setup.html | 65 ++++ test/test_api_security.py | 48 +-- test/test_api_usage.py | 47 ++- 47 files changed, 1835 insertions(+), 1068 deletions(-) create mode 100644 static/css/directives/ui/linear-workflow.css create mode 100644 static/css/directives/ui/manage-trigger-control.css create mode 100644 static/css/directives/ui/regex-match-view.css delete mode 100644 static/css/directives/ui/setup-trigger-dialog.css delete mode 100644 static/css/directives/ui/step-view-step.css create mode 100644 static/css/pages/trigger-setup.css create mode 100644 static/directives/dockerfile-path-select.html create mode 100644 static/directives/linear-workflow-section.html create mode 100644 static/directives/linear-workflow.html create mode 100644 static/directives/manage-trigger-custom-git.html create mode 100644 static/directives/manage-trigger-githost.html create mode 100644 static/directives/regex-match-view.html delete mode 100644 static/directives/setup-trigger-dialog.html delete mode 100644 static/directives/step-view-step.html delete mode 100644 static/directives/step-view.html delete mode 100644 static/directives/trigger-setup-custom.html delete mode 100644 static/directives/trigger-setup-githost.html create mode 100644 static/js/directives/ui/dockerfile-path-select.js create mode 100644 static/js/directives/ui/linear-workflow.js create mode 100644 static/js/directives/ui/manage-trigger-custom-git.js create mode 100644 static/js/directives/ui/manage-trigger-githost.js create mode 100644 static/js/directives/ui/regex-match-view.js delete mode 100644 static/js/directives/ui/step-view.js delete mode 100644 static/js/directives/ui/trigger-setup-custom.js delete mode 100644 static/js/directives/ui/trigger-setup-githost.js create mode 100644 static/js/pages/trigger-setup.js create mode 100644 static/partials/trigger-setup.html diff --git a/buildtrigger/basehandler.py b/buildtrigger/basehandler.py index 2555b09ed..b9f035dc0 100644 --- a/buildtrigger/basehandler.py +++ b/buildtrigger/basehandler.py @@ -1,7 +1,10 @@ +from abc import ABCMeta, abstractmethod +from jsonschema import validate +from six import add_metaclass + from endpoints.building import PreparedBuild from data import model from buildtrigger.triggerutil import get_trigger_config, InvalidServiceException -from jsonschema import validate METADATA_SCHEMA = { 'type': 'object', @@ -18,7 +21,7 @@ METADATA_SCHEMA = { 'ref': { 'type': 'string', 'description': 'git reference for a git commit', - 'pattern': '^refs\/(heads|tags|remotes)\/(.+)$', + 'pattern': r'^refs\/(heads|tags|remotes)\/(.+)$', }, 'default_branch': { 'type': 'string', @@ -86,6 +89,7 @@ METADATA_SCHEMA = { } +@add_metaclass(ABCMeta) class BuildTriggerHandler(object): def __init__(self, trigger, override_config=None): self.trigger = trigger @@ -96,72 +100,90 @@ class BuildTriggerHandler(object): """ Returns the auth token for the trigger. """ return self.trigger.auth_token + @abstractmethod def load_dockerfile_contents(self): """ Loads the Dockerfile found for the trigger's config and returns them or None if none could be found/loaded. """ - raise NotImplementedError + pass - def list_build_sources(self): + @abstractmethod + def list_build_source_namespaces(self): """ Take the auth information for the specific trigger type and load the - list of build sources(repositories). + list of namespaces that can contain build sources. """ - raise NotImplementedError + pass + @abstractmethod + def list_build_sources_for_namespace(self, namespace): + """ + Take the auth information for the specific trigger type and load the + list of repositories under the given namespace. + """ + pass + + @abstractmethod def list_build_subdirs(self): """ Take the auth information and the specified config so far and list all of the possible subdirs containing dockerfiles. """ - raise NotImplementedError + pass - def handle_trigger_request(self): + @abstractmethod + def handle_trigger_request(self, request): """ Transform the incoming request data into a set of actions. Returns a PreparedBuild. """ - raise NotImplementedError + pass + @abstractmethod def is_active(self): """ Returns True if the current build trigger is active. Inactive means further setup is needed. """ - raise NotImplementedError + pass + @abstractmethod def activate(self, standard_webhook_url): """ Activates the trigger for the service, with the given new configuration. Returns new public and private config that should be stored if successful. """ - raise NotImplementedError + pass + @abstractmethod def deactivate(self): """ Deactivates the trigger for the service, removing any hooks installed in the remote service. Returns the new config that should be stored if this trigger is going to be re-activated. """ - raise NotImplementedError + pass + @abstractmethod def manual_start(self, run_parameters=None): """ Manually creates a repository build for this trigger. Returns a PreparedBuild. """ - raise NotImplementedError + pass + @abstractmethod def list_field_values(self, field_name, limit=None): """ Lists all values for the given custom trigger field. For example, a trigger might have a field named "branches", and this method would return all branches. """ - raise NotImplementedError + pass + @abstractmethod def get_repository_url(self): """ Returns the URL of the current trigger's repository. Note that this operation can be called in a loop, so it should be as fast as possible. """ - raise NotImplementedError + pass @classmethod def service_name(cls): diff --git a/buildtrigger/bitbuckethandler.py b/buildtrigger/bitbuckethandler.py index 364801e42..f96a38cee 100644 --- a/buildtrigger/bitbuckethandler.py +++ b/buildtrigger/bitbuckethandler.py @@ -1,6 +1,10 @@ import logging import re +from calendar import timegm + +import dateutil.parser + from jsonschema import validate from buildtrigger.triggerutil import (RepositoryReadException, TriggerActivationException, TriggerDeactivationException, TriggerStartException, @@ -217,7 +221,8 @@ def get_transformed_webhook_payload(bb_payload, default_branch=None): try: validate(bb_payload, BITBUCKET_WEBHOOK_PAYLOAD_SCHEMA) except Exception as exc: - logger.exception('Exception when validating Bitbucket webhook payload: %s from %s', exc.message, bb_payload) + logger.exception('Exception when validating Bitbucket webhook payload: %s from %s', exc.message, + bb_payload) raise InvalidPayloadException(exc.message) payload = JSONPathDict(bb_payload) @@ -225,8 +230,8 @@ def get_transformed_webhook_payload(bb_payload, default_branch=None): if not change: return None - ref = ('refs/heads/' + change['name'] if change['type'] == 'branch' - else 'refs/tags/' + change['name']) + is_branch = change['type'] == 'branch' + ref = 'refs/heads/' + change['name'] if is_branch else 'refs/tags/' + change['name'] repository_name = payload['repository.full_name'] target = change['target'] @@ -390,7 +395,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler): return config - def list_build_sources(self): + def list_build_source_namespaces(self): bitbucket_client = self._get_authorized_client() (result, data, err_msg) = bitbucket_client.get_visible_repositories() if not result: @@ -398,22 +403,40 @@ class BitbucketBuildTrigger(BuildTriggerHandler): namespaces = {} for repo in data: - if not repo['scm'] == 'git': - continue - owner = repo['owner'] - if not owner in namespaces: + if owner in namespaces: + namespaces[owner]['score'] = namespaces[owner]['score'] + 1 + else: namespaces[owner] = { 'personal': owner == self.config.get('username'), - 'repos': [], - 'info': { - 'name': owner - } + 'id': owner, + 'title': owner, + 'avatar_url': repo['logo'], + 'score': 0, } - namespaces[owner]['repos'].append(owner + '/' + repo['slug']) + return list(namespaces.values()) - return namespaces.values() + def list_build_sources_for_namespace(self, namespace): + def repo_view(repo): + last_modified = dateutil.parser.parse(repo['utc_last_updated']) + + return { + 'name': repo['slug'], + 'full_name': '%s/%s' % (repo['owner'], repo['slug']), + 'description': repo['description'] or '', + 'last_updated': timegm(last_modified.utctimetuple()), + 'url': 'https://bitbucket.org/%s/%s' % (repo['owner'], repo['slug']), + 'has_admin_permissions': repo['read_only'] is False, + 'private': repo['is_private'], + } + + bitbucket_client = self._get_authorized_client() + (result, data, err_msg) = bitbucket_client.get_visible_repositories() + if not result: + raise RepositoryReadException('Could not read repository list: ' + err_msg) + + return [repo_view(repo) for repo in data if repo['owner'] == namespace] def list_build_subdirs(self): config = self.config @@ -431,7 +454,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler): files = set([f['path'] for f in data['files']]) if 'Dockerfile' in files: - return ['/'] + return [''] return [] diff --git a/buildtrigger/customhandler.py b/buildtrigger/customhandler.py index b3b1b01ba..7541995a5 100644 --- a/buildtrigger/customhandler.py +++ b/buildtrigger/customhandler.py @@ -212,3 +212,18 @@ class CustomBuildTrigger(BuildTriggerHandler): def get_repository_url(self): return None + + def list_build_source_namespaces(self): + raise NotImplementedError + + def list_build_sources_for_namespace(self, namespace): + raise NotImplementedError + + def list_build_subdirs(self): + raise NotImplementedError + + def list_field_values(self, field_name, limit=None): + raise NotImplementedError + + def load_dockerfile_contents(self): + raise NotImplementedError diff --git a/buildtrigger/githubhandler.py b/buildtrigger/githubhandler.py index aaaccd4d5..fa31f21d1 100644 --- a/buildtrigger/githubhandler.py +++ b/buildtrigger/githubhandler.py @@ -2,14 +2,15 @@ import logging import os.path import base64 +from calendar import timegm from functools import wraps from ssl import SSLError from github import (Github, UnknownObjectException, GithubException, BadCredentialsException as GitHubBadCredentialsException) + from jsonschema import validate -from app import app, github_trigger from buildtrigger.triggerutil import (RepositoryReadException, TriggerActivationException, TriggerDeactivationException, TriggerStartException, EmptyRepositoryException, ValidationRequestException, @@ -273,55 +274,57 @@ class GithubBuildTrigger(BuildTriggerHandler): return config @_catch_ssl_errors - def list_build_sources(self): + def list_build_source_namespaces(self): gh_client = self._get_client() usr = gh_client.get_user() - try: - repos = usr.get_repos() - except GithubException: - raise RepositoryReadException('Unable to list user repositories') - + # Build the full set of namespaces for the user, starting with their own. namespaces = {} - has_non_personal = False + namespaces[usr.login] = { + 'personal': True, + 'id': usr.login, + 'title': usr.name or usr.login, + 'avatar_url': usr.avatar_url, + 'score': usr.plan.private_repos if usr.plan else 0, + } - for repository in repos: - namespace = repository.owner.login - if not namespace in namespaces: - is_personal_repo = namespace == usr.login - namespaces[namespace] = { - 'personal': is_personal_repo, - 'repos': [], - 'info': { - 'name': namespace, - 'avatar_url': repository.owner.avatar_url - } - } + for org in usr.get_orgs(): + namespaces[org.name] = { + 'personal': False, + 'id': org.login, + 'title': org.name or org.login, + 'avatar_url': org.avatar_url, + 'url': org.html_url, + 'score': org.plan.private_repos if org.plan else 0, + } - if not is_personal_repo: - has_non_personal = True + return list(namespaces.values()) - namespaces[namespace]['repos'].append(repository.full_name) + @_catch_ssl_errors + def list_build_sources_for_namespace(self, namespace): + def repo_view(repo): + return { + 'name': repo.name, + 'full_name': repo.full_name, + 'description': repo.description or '', + 'last_updated': timegm(repo.pushed_at.utctimetuple()), + 'url': repo.html_url, + 'has_admin_permissions': repo.permissions.admin, + 'private': repo.private, + } - # In older versions of GitHub Enterprise, the get_repos call above does not - # return any non-personal repositories. In that case, we need to lookup the - # repositories manually. - # TODO: Remove this once we no longer support GHE versions <= 2.1 - if not has_non_personal: - for org in usr.get_orgs(): - repo_list = [repo.full_name for repo in org.get_repos(type='member')] - namespaces[org.name] = { - 'personal': False, - 'repos': repo_list, - 'info': { - 'name': org.name or org.login, - 'avatar_url': org.avatar_url - } - } + gh_client = self._get_client() + usr = gh_client.get_user() + + if namespace == usr.login: + return [repo_view(repo) for repo in usr.get_repos() if repo.owner.login == namespace] + + org = gh_client.get_organization(namespace) + if org is None: + return [] + + return [repo_view(repo) for repo in org.get_repos(type='member')] - entries = list(namespaces.values()) - entries.sort(key=lambda e: e['info']['name']) - return entries @_catch_ssl_errors def list_build_subdirs(self): @@ -357,19 +360,17 @@ class GithubBuildTrigger(BuildTriggerHandler): source = config['build_source'] path = self.get_dockerfile_path() try: - repo = gh_client.get_repo(source) file_info = repo.get_file_contents(path) - if file_info is None: - return None - - content = file_info.content - if file_info.encoding == 'base64': - content = base64.b64decode(content) - return content - except GithubException as ghe: - message = ghe.data.get('message', 'Unable to read Dockerfile: %s' % source) - raise RepositoryReadException(message) + return None + + if file_info is None: + return None + + content = file_info.content + if file_info.encoding == 'base64': + content = base64.b64decode(content) + return content @_catch_ssl_errors def list_field_values(self, field_name, limit=None): @@ -535,7 +536,7 @@ class GithubBuildTrigger(BuildTriggerHandler): logger.debug('GitHub trigger payload %s', payload) metadata = get_transformed_webhook_payload(payload, default_branch=default_branch, - lookup_user=lookup_user) + lookup_user=lookup_user) prepared = self.prepare_build(metadata) # Check if we should skip this build. diff --git a/buildtrigger/gitlabhandler.py b/buildtrigger/gitlabhandler.py index 448fff4fc..19d701ca0 100644 --- a/buildtrigger/gitlabhandler.py +++ b/buildtrigger/gitlabhandler.py @@ -1,6 +1,10 @@ import logging +from calendar import timegm from functools import wraps + +import dateutil.parser + from app import app, gitlab_trigger from jsonschema import validate @@ -70,6 +74,17 @@ GITLAB_WEBHOOK_PAYLOAD_SCHEMA = { 'required': ['ref', 'checkout_sha', 'repository'], } +_ACCESS_LEVEL_MAP = { + 50: ("owner", True), + 40: ("master", True), + 30: ("developer", False), + 20: ("reporter", False), + 10: ("guest", False), +} + +_PER_PAGE_COUNT = 20 + + def _catch_timeouts(func): @wraps(func) def wrapper(*args, **kwargs): @@ -82,6 +97,27 @@ def _catch_timeouts(func): return wrapper +def _paginated_iterator(func, exc): + """ Returns an iterator over invocations of the given function, automatically handling + pagination. + """ + page = 0 + while True: + result = func(page=page, per_page=_PER_PAGE_COUNT) + if result is False: + raise exc + + counter = 0 + for item in result: + yield item + counter = counter + 1 + + if counter < _PER_PAGE_COUNT: + break + + page = page + 1 + + def get_transformed_webhook_payload(gl_payload, default_branch=None, lookup_user=None, lookup_commit=None): """ Returns the Gitlab webhook JSON payload transformed into our own payload @@ -223,35 +259,57 @@ class GitLabBuildTrigger(BuildTriggerHandler): config.pop('key_id', None) self.config = config - return config @_catch_timeouts - def list_build_sources(self): + def list_build_source_namespaces(self): gl_client = self._get_authorized_client() current_user = gl_client.currentuser() if current_user is False: raise RepositoryReadException('Unable to get current user') - repositories = gl_client.getprojects() - if repositories is False: - raise RepositoryReadException('Unable to list user repositories') - namespaces = {} + repositories = _paginated_iterator(gl_client.getprojects, RepositoryReadException) for repo in repositories: - owner = repo['namespace']['name'] - if not owner in namespaces: - namespaces[owner] = { + namespace = repo['namespace'] + namespace_id = namespace['id'] + if namespace_id in namespaces: + namespaces[namespace_id]['score'] = namespaces[namespace_id]['score'] + 1 + else: + owner = repo['namespace']['name'] + namespaces[namespace_id] = { 'personal': owner == current_user['username'], - 'repos': [], - 'info': { - 'name': owner, - } + 'id': namespace['path'], + 'title': namespace['name'], + 'avatar_url': repo['owner']['avatar_url'], + 'score': 0, } - namespaces[owner]['repos'].append(repo['path_with_namespace']) + return list(namespaces.values()) - return namespaces.values() + @_catch_timeouts + def list_build_sources_for_namespace(self, namespace): + def repo_view(repo): + last_modified = dateutil.parser.parse(repo['last_activity_at']) + has_admin_permission = False + + if repo.get('permissions'): + access_level = repo['permissions']['project_access']['access_level'] + has_admin_permission = _ACCESS_LEVEL_MAP.get(access_level, ("", False))[1] + + return { + 'name': repo['path'], + 'full_name': repo['path_with_namespace'], + 'description': repo['description'] or '', + 'last_updated': timegm(last_modified.utctimetuple()), + 'url': repo['web_url'], + 'has_admin_permissions': has_admin_permission, + 'private': repo['public'] is False, + } + + gl_client = self._get_authorized_client() + repositories = _paginated_iterator(gl_client.getprojects, RepositoryReadException) + return [repo_view(repo) for repo in repositories if repo['namespace']['path'] == namespace] @_catch_timeouts def list_build_subdirs(self): @@ -280,7 +338,7 @@ class GitLabBuildTrigger(BuildTriggerHandler): for node in repo_tree: if node['name'] == 'Dockerfile': - return ['/'] + return [''] return [] diff --git a/data/model/user.py b/data/model/user.py index 28ad75174..6f2f14f64 100644 --- a/data/model/user.py +++ b/data/model/user.py @@ -328,6 +328,11 @@ def delete_robot(robot_username): robot_username) +def list_namespace_robots(namespace): + """ Returns all the robots found under the given namespace. """ + return _list_entity_robots(namespace) + + def _list_entity_robots(entity_name): """ Return the list of robots for the specified entity. This MUST return a query, not a materialized list so that callers can use db_for_update. diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index c03e9fbd9..3dec076b6 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -127,7 +127,7 @@ class BuildTriggerSubdirs(RepositoryParamResource): try: subdirs = handler.list_build_subdirs() return { - 'subdir': subdirs, + 'subdir': ['/' + subdir for subdir in subdirs], 'status': 'success' } except EmptyRepositoryException as exc: @@ -288,8 +288,9 @@ class BuildTriggerAnalyze(RepositoryParamResource): contents = handler.load_dockerfile_contents() if not contents: return { - 'status': 'error', - 'message': 'Could not read the Dockerfile for the trigger' + 'status': 'warning', + 'message': 'Specified Dockerfile path for the trigger was not found on the main ' + + 'branch. This trigger may fail.', } # Parse the contents of the Dockerfile. @@ -341,42 +342,40 @@ class BuildTriggerAnalyze(RepositoryParamResource): 'message': 'Repository "%s" referenced by the Dockerfile was not found' % (base_image) } - # Check to see if the repository is public. If not, we suggest the - # usage of a robot account to conduct the pull. - read_robots = [] + # If the base image is public, mark it as such. + if found_repository.visibility.name == 'public': + return { + 'status': 'publicbase' + } + # Otherwise, retrieve the list of robots and mark whether they have read access already. + robots = [] if AdministerOrganizationPermission(base_namespace).can(): + perm_query = model.user.get_all_repo_users_transitive(base_namespace, base_repository) + user_ids_with_permission = set([user.id for user in perm_query]) + def robot_view(robot): return { 'name': robot.username, 'kind': 'user', - 'is_robot': True + 'is_robot': True, + 'can_read': robot.id in user_ids_with_permission, } - def is_valid_robot(user): - # Make sure the user is a robot. - if not user.robot: - return False - - # Make sure the current user can see/administer the robot. - (robot_namespace, shortname) = parse_robot_username(user.username) - return AdministerOrganizationPermission(robot_namespace).can() - - repo_users = list(model.user.get_all_repo_users_transitive(base_namespace, base_repository)) - read_robots = [robot_view(user) for user in repo_users if is_valid_robot(user)] + robots = [robot_view(robot) for robot in model.user.list_namespace_robots(base_namespace)] return { 'namespace': base_namespace, 'name': base_repository, - 'is_public': found_repository.visibility.name == 'public', - 'robots': read_robots, - 'status': 'analyzed' + 'robots': robots, + 'status': 'requiresrobot', + 'is_admin': AdministerOrganizationPermission(base_namespace).can(), } except RepositoryReadException as rre: return { 'status': 'error', - 'message': rre.message + 'message': 'Could not analyze the repository: %s' % rre.message, } except NotImplementedError: return { @@ -502,8 +501,54 @@ class BuildTriggerFieldValues(RepositoryParamResource): @internal_only class BuildTriggerSources(RepositoryParamResource): """ Custom verb to fetch the list of build sources for the trigger config. """ + schemas = { + 'BuildTriggerSourcesRequest': { + 'type': 'object', + 'description': 'Specifies the namespace under which to fetch sources', + 'properties': { + 'namespace': { + 'type': 'string', + 'description': 'The namespace for which to fetch sources' + }, + }, + } + } + @require_repo_admin @nickname('listTriggerBuildSources') + @validate_json_request('BuildTriggerSourcesRequest') + def post(self, namespace_name, repo_name, trigger_uuid): + """ List the build sources for the trigger configuration thus far. """ + namespace = request.get_json()['namespace'] + + try: + trigger = model.build.get_build_trigger(trigger_uuid) + except model.InvalidBuildTriggerException: + raise NotFound() + + user_permission = UserAdminPermission(trigger.connected_user.username) + if user_permission.can(): + handler = BuildTriggerHandler.get_handler(trigger) + + try: + return { + 'sources': handler.list_build_sources_for_namespace(namespace) + } + except RepositoryReadException as rre: + raise InvalidRequest(rre.message) + else: + raise Unauthorized() + + + +@resource('/v1/repository//trigger//namespaces') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('trigger_uuid', 'The UUID of the build trigger') +@internal_only +class BuildTriggerSourceNamespaces(RepositoryParamResource): + """ Custom verb to fetch the list of namespaces (orgs, projects, etc) for the trigger config. """ + @require_repo_admin + @nickname('listTriggerBuildSourceNamespaces') def get(self, namespace_name, repo_name, trigger_uuid): """ List the build sources for the trigger configuration thus far. """ try: @@ -517,7 +562,7 @@ class BuildTriggerSources(RepositoryParamResource): try: return { - 'sources': handler.list_build_sources() + 'namespaces': handler.list_build_source_namespaces() } except RepositoryReadException as rre: raise InvalidRequest(rre.message) diff --git a/endpoints/bitbuckettrigger.py b/endpoints/bitbuckettrigger.py index 8c8052235..ba2e5b215 100644 --- a/endpoints/bitbuckettrigger.py +++ b/endpoints/bitbuckettrigger.py @@ -40,8 +40,7 @@ def attach_bitbucket_build_trigger(trigger_uuid): repository = trigger.repository.name repo_path = '%s/%s' % (namespace, repository) - full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=', - trigger.uuid) + full_url = url_for('web.buildtrigger', path=repo_path, trigger=trigger.uuid) logger.debug('Redirecting to full url: %s', full_url) return redirect(full_url) diff --git a/endpoints/githubtrigger.py b/endpoints/githubtrigger.py index 1f0d9ca90..43b11e14d 100644 --- a/endpoints/githubtrigger.py +++ b/endpoints/githubtrigger.py @@ -34,8 +34,7 @@ def attach_github_build_trigger(namespace_name, repo_name): trigger = model.build.create_build_trigger(repo, 'github', token, current_user.db_user()) repo_path = '%s/%s' % (namespace_name, repo_name) - full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=', - trigger.uuid) + full_url = url_for('web.buildtrigger', path=repo_path, trigger=trigger.uuid) logger.debug('Redirecting to full url: %s', full_url) return redirect(full_url) diff --git a/endpoints/gitlabtrigger.py b/endpoints/gitlabtrigger.py index 4f51a2bdc..2626a068d 100644 --- a/endpoints/gitlabtrigger.py +++ b/endpoints/gitlabtrigger.py @@ -47,8 +47,7 @@ def attach_gitlab_build_trigger(): trigger = model.build.create_build_trigger(repo, 'gitlab', token, current_user.db_user()) repo_path = '%s/%s' % (namespace, repository) - full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=', - trigger.uuid) + full_url = url_for('web.buildtrigger', path=repo_path, trigger=trigger.uuid) logger.debug('Redirecting to full url: %s', full_url) return redirect(full_url) diff --git a/endpoints/web.py b/endpoints/web.py index f3a6f7ce7..a0ad23755 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -183,6 +183,13 @@ def confirm_invite(): def repository(path): return index('') + +@web.route('/repository//trigger/', methods=['GET']) +@no_cache +def buildtrigger(path, trigger): + return index('') + + @web.route('/starred/') @no_cache def starred(): @@ -659,9 +666,7 @@ def attach_custom_build_trigger(namespace_name, repo_name): None, current_user.db_user()) repo_path = '%s/%s' % (namespace_name, repo_name) - full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=', - trigger.uuid) - + full_url = url_for('web.buildtrigger', path=repo_path, trigger=trigger.uuid) logger.debug('Redirecting to full url: %s', full_url) return redirect(full_url) diff --git a/static/css/core-ui.css b/static/css/core-ui.css index cb7525cc9..89a82ad7d 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -1397,16 +1397,38 @@ a:focus { margin-bottom: 6px; } -.co-check-bar .co-filter-box input, .co-top-bar .co-filter-box input { +.co-check-bar .co-filter-box input, .co-top-bar .co-filter-box input[type="text"] { width: 300px; display: inline-block; vertical-align: middle; } +.co-check-bar .co-filter-box input, .co-top-bar .co-filter-box label { + margin-left: 6px; +} + .co-top-bar .co-filter-box input { vertical-align: top; } +@media screen and (max-width: 767px) { + .co-top-bar .page-controls { + display: block; + margin-bottom: 10px; + text-align: right; + } + + .co-top-bar .co-filter-box { + display: block; + margin-bottom: 10px; + } + + .co-top-bar .filter-options { + display: block; + margin-bottom: 10px; + } +} + .empty { border-bottom: none !important; } diff --git a/static/css/directives/ui/linear-workflow.css b/static/css/directives/ui/linear-workflow.css new file mode 100644 index 000000000..b55d10a63 --- /dev/null +++ b/static/css/directives/ui/linear-workflow.css @@ -0,0 +1,64 @@ +.linear-workflow-section { + margin-bottom: 10px; +} + +.linear-workflow-section.row { + margin-left: 0px; + margin-right: 0px; +} + +.linear-workflow .upcoming-table { + vertical-align: middle; + margin-left: 20px; +} + +.linear-workflow .upcoming-table .fa { + margin-right: 8px; +} + +.linear-workflow .upcoming { + color: #888; + vertical-align: middle; + margin-left: 10px; +} + +.linear-workflow .upcoming ul { + padding: 0px; + display: inline-block; + margin: 0px; +} + +.linear-workflow .upcoming li { + display: inline-block; + margin-right: 6px; + margin-left: 2px; +} + +.linear-workflow .upcoming li:after { + content: "•"; + display: inline-block; + margin-left: 6px; + margin-right: 2px; +} + +.linear-workflow .upcoming li:last-child:after { + content: ""; +} + +.linear-workflow .bottom-controls { + padding: 10px; +} + +.linear-workflow-section-element { + padding: 20px; + padding-top: 10px; +} + +.linear-workflow-section-element h3, .linear-workflow-section-element strong { + color: #444; +} + +.linear-workflow-section-element.current-section h3, +.linear-workflow-section-element.current-section strong { + color: black; +} diff --git a/static/css/directives/ui/manage-trigger-control.css b/static/css/directives/ui/manage-trigger-control.css new file mode 100644 index 000000000..708852881 --- /dev/null +++ b/static/css/directives/ui/manage-trigger-control.css @@ -0,0 +1,106 @@ +.manage-trigger-control .help-col { + padding: 30px; + padding-top: 100px; +} + +.manage-trigger-control .main-col { + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; +} + +.manage-trigger-control strong { + margin-bottom: 10px; + display: block; +} + +.manage-trigger-control .namespace-avatar { + margin-right: 4px; + width: 24px; + vertical-align: middle; +} + +.manage-trigger-control .importance-col { + text-align: center; + width: 120px; +} + +.manage-trigger-control .co-top-bar { + margin-top: 20px; + height: 28px; +} + +.manage-trigger-control .namespace-avatar { + margin-left: 2px; + margin-right: 2px; + display: inline-block; +} + +.manage-trigger-control .service-icon { + font-size: 24px; + margin-right: 4px; + vertical-align: middle; +} + +.manage-trigger-control .fa-exclamation-triangle { + color: #FCA657; +} + +.manage-trigger-control .empty-description { + color: #ccc; +} + +.manage-trigger-control .radio { + margin-bottom: 20px; +} + +.manage-trigger-control .radio input[type="radio"] { + padding: 4px; +} + +.manage-trigger-control .radio label .title { + font-size: 16px; +} + +.manage-trigger-control .radio label .weak { + font-weight: 300; +} + +.manage-trigger-control .radio label .description { + margin-top: 6px; + color: #aaa; +} + +.manage-trigger-control .radio label .extended { + margin-top: 20px; +} + +.manage-trigger-control .radio label td:first-child { + vertical-align: top; + padding: 4px; + padding-right: 10px; +} + +.manage-trigger-control .regex-match-view { + margin-top: 20px; +} + +.manage-trigger-control h3 .fa { + margin-right: 4px; +} + +.manage-trigger-control h3.warning { + color: #FCA657; +} + +.manage-trigger-control h3.error { + color: #D64456; +} + +.manage-trigger-control .success { + color: #2FC98E !important; +} + +.manage-trigger-control .nowrap-col { + white-space: nowrap; +} diff --git a/static/css/directives/ui/regex-match-view.css b/static/css/directives/ui/regex-match-view.css new file mode 100644 index 000000000..dd6796949 --- /dev/null +++ b/static/css/directives/ui/regex-match-view.css @@ -0,0 +1,36 @@ +.regex-match-view-element .match-list { + list-style: none; + overflow: auto; + max-height: 150px; +} + +.regex-match-view-element .match-list li { + display: inline-block; + margin-right: 4px; + width: 120px; + padding: 4px; +} + +.regex-match-view-element .match-list li .fa { + margin-right: 4px; + vertical-align: middle; +} + +.regex-match-view-element .match-list.not-matching li { + color: #aaa; +} + +.regex-match-view-element .match-list.matching li { + color: #2fc98e; +} + +.regex-match-view-element .match-table td:first-child { + vertical-align: top; + white-space: nowrap; +} + + +.regex-match-view-element .fa-exclamation-triangle { + margin-right: 4px; +} + diff --git a/static/css/directives/ui/setup-trigger-dialog.css b/static/css/directives/ui/setup-trigger-dialog.css deleted file mode 100644 index 482a687bf..000000000 --- a/static/css/directives/ui/setup-trigger-dialog.css +++ /dev/null @@ -1,28 +0,0 @@ -.setup-trigger-directive-element .dockerfile-found-content { - margin-left: 32px; -} - -.setup-trigger-directive-element .dockerfile-found-content:before { - content: "\f071"; - font-family: FontAwesome; - color: rgb(255, 194, 0); - position: absolute; - top: 0px; - left: 0px; - font-size: 20px; -} - -.setup-trigger-directive-element .loading { - text-align: center; -} - -.setup-trigger-directive-element .loading .cor-loader-inline { - margin-right: 4px; -} - -.setup-trigger-directive-element .dockerfile-found { - position: relative; - margin-bottom: 16px; - padding-bottom: 16px; - border-bottom: 1px solid #eee; -} \ No newline at end of file diff --git a/static/css/directives/ui/step-view-step.css b/static/css/directives/ui/step-view-step.css deleted file mode 100644 index 16f31abda..000000000 --- a/static/css/directives/ui/step-view-step.css +++ /dev/null @@ -1,9 +0,0 @@ -.step-view-step-content .loading-message { - position: relative; - text-align: center; - display: block; -} - -.step-view-step-content .loading-message .cor-loader-inline { - margin-right: 6px; -} \ No newline at end of file diff --git a/static/css/pages/trigger-setup.css b/static/css/pages/trigger-setup.css new file mode 100644 index 000000000..0539d309f --- /dev/null +++ b/static/css/pages/trigger-setup.css @@ -0,0 +1,35 @@ + +.trigger-setup-element .activated .content { + padding-top: 10px; + padding-bottom: 10px; +} + +.trigger-setup-element .activated h3 { + text-align: center; + margin-bottom: 30px; + display: block; +} + +.trigger-setup-element .button-bar { + text-align: right; + margin-top: 16px; +} + +.trigger-setup-element .activating .cor-loader-inline { + margin-right: 6px; +} + +.trigger-setup-element .activating .btn-success { + display: none; +} + +.trigger-setup-element .activating-message { + padding: 10px; + padding-left: 30px; +} + +.trigger-setup-element .activating-message b { + vertical-align: middle; + font-size: 18px; + font-weight: normal; +} diff --git a/static/directives/credentials.html b/static/directives/credentials.html index 9580f4ed1..1879f87fe 100644 --- a/static/directives/credentials.html +++ b/static/directives/credentials.html @@ -4,9 +4,7 @@
-

- {{ credential.name }}: -

-

+ {{ credential.name }}: +
diff --git a/static/directives/dockerfile-path-select.html b/static/directives/dockerfile-path-select.html new file mode 100644 index 000000000..699336bb7 --- /dev/null +++ b/static/directives/dockerfile-path-select.html @@ -0,0 +1,32 @@ +
+ + +
+
+ Path entered for folder containing Dockerfile is invalid: Must start with a '/'. +
+
+
\ No newline at end of file diff --git a/static/directives/linear-workflow-section.html b/static/directives/linear-workflow-section.html new file mode 100644 index 000000000..7eeafcf63 --- /dev/null +++ b/static/directives/linear-workflow-section.html @@ -0,0 +1,6 @@ +
+
+
+ +
\ No newline at end of file diff --git a/static/directives/linear-workflow.html b/static/directives/linear-workflow.html new file mode 100644 index 000000000..b1f366f93 --- /dev/null +++ b/static/directives/linear-workflow.html @@ -0,0 +1,31 @@ +
+ +
+ +
+ + + + + +
+ + + + +
+ Next: +
    +
  • + {{ section.title }} +
  • +
+
+
+
+
\ No newline at end of file diff --git a/static/directives/manage-trigger-custom-git.html b/static/directives/manage-trigger-custom-git.html new file mode 100644 index 000000000..96fb0819a --- /dev/null +++ b/static/directives/manage-trigger-custom-git.html @@ -0,0 +1,43 @@ +
+
+ +
+ +
+

Enter repository

+ + Please enter the HTTP or SSH style URL used to clone your git repository: + + +
+ +
+ + +
+ +
+

Select build context directory

+ Please select the build context directory under the git repository: + +
+ + +
+
\ No newline at end of file diff --git a/static/directives/manage-trigger-githost.html b/static/directives/manage-trigger-githost.html new file mode 100644 index 000000000..4912b5c2f --- /dev/null +++ b/static/directives/manage-trigger-githost.html @@ -0,0 +1,330 @@ +
+
+ + +
+
+

Select {{ namespaceTitle }}

+ + Please select the {{ namespaceTitle }} under which the repository lives + + +
+
+ + +
+
+ + + + + + + + + + + + + +
+ {{ namespaceTitle }} +
+ + + + {{ namespace.id }} +
+
+
No matching {{ namespaceTitle }} found.
+
Try expanding your filtering terms.
+
+
+ +
+ Retrieving {{ namespaceTitle }}s +
+ +
+ + +
+ +
+

Select Repository

+ + Select a repository in + + {{ local.selectedNamespace.id }} + + +
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + +
+ Repository Name + + Last Updated +
+ + + + + + + {{ repository.name }} + + +
+
+
No matching repositories found.
+
Try expanding your filtering terms.
+
+
+ +
+ Retrieving repositories +
+ + +
+ + +
+
+

Configure Trigger

+ + Configure trigger options for + + {{ local.selectedNamespace.id }}/{{ local.selectedRepository.name }} + + +
+ +
+
+ +
+
+ +
+ Retrieving repository refs +
+ +
+ + +
+
+
+ {{ local.dockerfileLocations.message }} +
+
+ +
+

Select Dockerfile

+ + Please select the location of the Dockerfile to build when this trigger is invoked + + +
+
+ +
+ Retrieving Dockerfile locations +
+ +
+ + +
+ +
+

Verification Error

+ + There was an error when verifying the state of + {{ local.selectedNamespace.id }}/{{ local.selectedRepository.name }} + + + {{ local.triggerAnalysis.message }} +
+ + +
+

Verification Warning

+ {{ local.triggerAnalysis.message }} +
+ + +
+

Ready to go!

+ Click "Create Trigger" to complete setup of this build trigger +
+ + +
+

Robot Account Required

+

The selected Dockerfile in the selected repository depends upon a private base image

+

A robot account with access to the base image is required to setup this trigger, but you are not the administrator of this namespace.

+

Administrative access is required to continue to ensure security of the robot credentials.

+
+ + +
+

Select Robot Account

+ + The selected Dockerfile in the selected repository depends upon a private base image. Select a robot account with access: + + +
+
+ + +
+
+ + + + + + + + + + + + + +
+ Robot Account + + Has Read Access +
+ + + + + Can Read + Read access will be added if selected +
+
+
No matching robot accounts found.
+
Try expanding your filtering terms.
+
+
+ + +
+ +
\ No newline at end of file diff --git a/static/directives/regex-match-view.html b/static/directives/regex-match-view.html new file mode 100644 index 000000000..da9316838 --- /dev/null +++ b/static/directives/regex-match-view.html @@ -0,0 +1,29 @@ +
+
+ Invalid Regular Expression! +
+
+ + + + + + + + + +
Matching: +
    +
  • + {{ item.title }} +
  • +
+
Not Matching: +
    +
  • + {{ item.title }} +
  • +
+
+
+
\ No newline at end of file diff --git a/static/directives/repo-view/repo-panel-builds.html b/static/directives/repo-view/repo-panel-builds.html index a38d783b4..327846002 100644 --- a/static/directives/repo-view/repo-panel-builds.html +++ b/static/directives/repo-view/repo-panel-builds.html @@ -126,10 +126,8 @@ - - Trigger Setup in progress: - Resume | - Cancel + This build trigger has not had its setup completed: + Delete Trigger @@ -185,14 +183,6 @@ build-started="handleBuildStarted(build)">
- -
-
-
diff --git a/static/directives/setup-trigger-dialog.html b/static/directives/setup-trigger-dialog.html deleted file mode 100644 index ad4309aaa..000000000 --- a/static/directives/setup-trigger-dialog.html +++ /dev/null @@ -1,133 +0,0 @@ -
- - -
diff --git a/static/directives/step-view-step.html b/static/directives/step-view-step.html deleted file mode 100644 index b7986e9dd..000000000 --- a/static/directives/step-view-step.html +++ /dev/null @@ -1,9 +0,0 @@ -
-
-
-
-
- - {{ loadMessage }} -
-
diff --git a/static/directives/step-view.html b/static/directives/step-view.html deleted file mode 100644 index 8d3124291..000000000 --- a/static/directives/step-view.html +++ /dev/null @@ -1,3 +0,0 @@ -
-
-
diff --git a/static/directives/trigger-setup-custom.html b/static/directives/trigger-setup-custom.html deleted file mode 100644 index 59227580a..000000000 --- a/static/directives/trigger-setup-custom.html +++ /dev/null @@ -1,40 +0,0 @@ -
-
- - - - - - - - - -
Repository{{ state.build_source }}
Dockerfile Location: -
- {{ state.subdir || '/' }} -
-
-
- - -
- - - -
-
Please enter an HTTP or SSH style URL used to clone your git repository:
- -
- - -
-
Dockerfile Location:
- -
-
-
diff --git a/static/directives/trigger-setup-githost.html b/static/directives/trigger-setup-githost.html deleted file mode 100644 index 1aeb6069e..000000000 --- a/static/directives/trigger-setup-githost.html +++ /dev/null @@ -1,201 +0,0 @@ -
- -
- - - - - - - - - - - - - - - -
- Repository: - -
- - - {{ state.currentRepo.repo }} -
-
- Branches and Tags: - -
- (Build All) - Regular Expression: {{ state.branchTagFilter }} -
-
- Dockerfile Location: - -
- {{ state.currentLocation || '(Repository Root)' }} -
-
-
- - - -
- -
-
Please choose the repository that will trigger the build:
- -
- - -
- -
Please choose the branches and tags to which this trigger will apply:
-
-
- - -
- -
-
-
-
- - -
-
-
- - -
-
- Branches: -
    -
  • - - {{ branchName }} - -
  • -
- ... -
-
- Tags: -
    -
  • - - {{ tagName }} - -
  • -
- ... -
-
- Warning: No branches found -
-
-
-
-
- - -
- -
Dockerfile Location:
- - -
-
- {{ locationError }} -
-
- Warning: No Dockerfiles were found in {{ state.currentRepo.repo }} -
-
- Note: The folder does not currently exist or contain a Dockerfile -
-
- -
-
diff --git a/static/directives/trigger/custom-git/trigger-description.html b/static/directives/trigger/custom-git/trigger-description.html index 2b41418ac..3d405f127 100644 --- a/static/directives/trigger/custom-git/trigger-description.html +++ b/static/directives/trigger/custom-git/trigger-description.html @@ -1,4 +1,4 @@ - Push to {{ trigger.config.build_source }} + Push to repository {{ trigger.config.build_source }} diff --git a/static/js/directives/repo-view/repo-panel-builds.js b/static/js/directives/repo-view/repo-panel-builds.js index 9bc017631..9ff834c97 100644 --- a/static/js/directives/repo-view/repo-panel-builds.js +++ b/static/js/directives/repo-view/repo-panel-builds.js @@ -29,11 +29,9 @@ angular.module('quay').directive('repoPanelBuilds', function () { $scope.currentFilter = null; $scope.currentStartTrigger = null; - $scope.currentSetupTrigger = null; $scope.showBuildDialogCounter = 0; $scope.showTriggerStartDialogCounter = 0; - $scope.showTriggerSetupCounter = 0; $scope.triggerCredentialsModalTrigger = null; $scope.triggerCredentialsModalCounter = 0; @@ -144,16 +142,6 @@ angular.module('quay').directive('repoPanelBuilds', function () { $scope.triggersResource = ApiService.listBuildTriggersAsResource(params).get(function(resp) { $scope.triggers = resp.triggers; - - // Check to see if we need to setup any trigger. - var newTriggerId = $routeParams.newtrigger; - if (newTriggerId) { - $scope.triggers.map(function(trigger) { - if (trigger['id'] == newTriggerId && !trigger['is_active']) { - $scope.setupTrigger(trigger); - } - }); - } }); }; @@ -208,18 +196,6 @@ angular.module('quay').directive('repoPanelBuilds', function () { $scope.showTriggerStartDialogCounter++; }; - $scope.cancelSetupTrigger = function(trigger) { - if ($scope.currentSetupTrigger != trigger) { return; } - - $scope.currentSetupTrigger = null; - $scope.deleteTrigger(trigger); - }; - - $scope.setupTrigger = function(trigger) { - $scope.currentSetupTrigger = trigger; - $scope.showTriggerSetupCounter++; - }; - $scope.deleteTrigger = function(trigger, opt_callback) { if (!trigger) { return; } diff --git a/static/js/directives/ui/dockerfile-path-select.js b/static/js/directives/ui/dockerfile-path-select.js new file mode 100644 index 000000000..b968e20f8 --- /dev/null +++ b/static/js/directives/ui/dockerfile-path-select.js @@ -0,0 +1,54 @@ +/** + * An element which displays a list of selectable paths containing Dockerfiles. + */ +angular.module('quay').directive('dockerfilePathSelect', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/dockerfile-path-select.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'currentPath': '=currentPath', + 'isValidPath': '=?isValidPath', + 'paths': '=paths', + 'supportsFullListing': '=supportsFullListing' + }, + controller: function($scope, $element) { + $scope.isUnknownPath = true; + $scope.selectedPath = null; + + var checkPath = function() { + $scope.isUnknownPath = false; + $scope.isValidPath = false; + + var path = $scope.currentPath || ''; + if (path.length == 0 || path[0] != '/') { + return; + } + + $scope.isValidPath = true; + + if (!$scope.paths) { + return; + } + + $scope.isUnknownPath = $scope.supportsFullListing && $scope.paths.indexOf(path) < 0; + }; + + $scope.setPath = function(path) { + $scope.currentPath = path; + $scope.selectedPath = null; + }; + + $scope.setSelectedPath = function(path) { + $scope.currentPath = path; + $scope.selectedPath = path; + }; + + $scope.$watch('currentPath', checkPath); + $scope.$watch('paths', checkPath); + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/directives/ui/dropdown-select.js b/static/js/directives/ui/dropdown-select.js index ff84b162b..e6cd0e289 100644 --- a/static/js/directives/ui/dropdown-select.js +++ b/static/js/directives/ui/dropdown-select.js @@ -28,36 +28,29 @@ angular.module('quay').directive('dropdownSelect', function ($compile) { } $scope.placeholder = $scope.placeholder || ''; - $scope.internalItem = null; + $scope.lookaheadSetup = false; // Setup lookahead. var input = $($element).find('.lookahead-input'); $scope.$watch('clearValue', function(cv) { - if (cv) { + if (cv && $scope.lookaheadSetup) { $scope.selectedItem = null; - $(input).val(''); + $(input).typeahead('val', ''); + $(input).typeahead('close'); } }); $scope.$watch('selectedItem', function(item) { - if ($scope.selectedItem == $scope.internalItem) { - // The item has already been set due to an internal action. - return; - } - - if ($scope.selectedItem != null) { - $(input).val(item.toString()); - } else { - $(input).val(''); + if (item != null && $scope.lookaheadSetup) { + $(input).typeahead('val', item.toString()); + $(input).typeahead('close'); } }); $scope.$watch('lookaheadItems', function(items) { $(input).off(); - if (!items) { - return; - } + items = items || []; var formattedItems = []; for (var i = 0; i < items.length; ++i) { @@ -80,7 +73,10 @@ angular.module('quay').directive('dropdownSelect', function ($compile) { }); dropdownHound.initialize(); - $(input).typeahead({}, { + $(input).typeahead({ + 'hint': false, + 'highlight': false + }, { source: dropdownHound.ttAdapter(), templates: { 'suggestion': function (datum) { @@ -92,7 +88,6 @@ angular.module('quay').directive('dropdownSelect', function ($compile) { $(input).on('input', function(e) { $scope.$apply(function() { - $scope.internalItem = null; $scope.selectedItem = null; if ($scope.handleInput) { $scope.handleInput({'input': $(input).val()}); @@ -102,7 +97,6 @@ angular.module('quay').directive('dropdownSelect', function ($compile) { $(input).on('typeahead:selected', function(e, datum) { $scope.$apply(function() { - $scope.internalItem = datum['item'] || datum['value']; $scope.selectedItem = datum['item'] || datum['value']; if ($scope.handleItemSelected) { $scope.handleItemSelected({'datum': datum}); @@ -111,6 +105,7 @@ angular.module('quay').directive('dropdownSelect', function ($compile) { }); $rootScope.__dropdownSelectCounter++; + $scope.lookaheadSetup = true; }); }, link: function(scope, element, attrs) { diff --git a/static/js/directives/ui/linear-workflow.js b/static/js/directives/ui/linear-workflow.js new file mode 100644 index 000000000..1f24adeb5 --- /dev/null +++ b/static/js/directives/ui/linear-workflow.js @@ -0,0 +1,141 @@ +/** + * An element which displays a linear workflow of sections, each completed in order before the next + * step is made visible. + */ +angular.module('quay').directive('linearWorkflow', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/linear-workflow.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'workflowState': '=?workflowState', + 'workflowComplete': '&workflowComplete', + 'doneTitle': '@doneTitle' + }, + controller: function($scope, $element, $timeout) { + $scope.sections = []; + + $scope.nextSection = function() { + if (!$scope.currentSection.valid) { return; } + + var currentIndex = $scope.currentSection.index; + if (currentIndex + 1 >= $scope.sections.length) { + $scope.workflowComplete(); + return; + } + + $scope.workflowState = $scope.sections[currentIndex + 1].id; + }; + + this.registerSection = function(sectionScope, sectionElement) { + // Add the section to the list. + var sectionInfo = { + 'index': $scope.sections.length, + 'id': sectionScope.sectionId, + 'title': sectionScope.sectionTitle, + 'scope': sectionScope, + 'element': sectionElement + }; + + $scope.sections.push(sectionInfo); + + // Add a watch on the `sectionValid` value on the section itself. If/when this value + // changes, we copy it over to the sectionInfo, so that the overall workflow can watch + // the change. + sectionScope.$watch('sectionValid', function(isValid) { + sectionInfo['valid'] = isValid; + if (!isValid) { + // Reset the sections back to this section. + updateState(); + } + }); + + // Bind the `submitSection` callback to move to the next section when the user hits + // enter (which calls this method on the scope via an ng-submit set on a wrapping + //
tag). + sectionScope.submitSection = function() { + $scope.nextSection(); + }; + + // Update the state of the workflow to account for the new section. + updateState(); + }; + + var updateState = function() { + // Find the furthest state we can show. + var foundIndex = 0; + var maxValidIndex = -1; + + $scope.sections.forEach(function(section, index) { + if (section.id == $scope.workflowState) { + foundIndex = index; + } + + if (maxValidIndex == index - 1 && section.valid) { + maxValidIndex = index; + } + }); + + var minSectionIndex = Math.min(maxValidIndex + 1, foundIndex); + $scope.sections.forEach(function(section, index) { + section.scope.sectionVisible = index <= minSectionIndex; + section.scope.isCurrentSection = false; + }); + + $scope.workflowState = null; + if (minSectionIndex >= 0 && minSectionIndex < $scope.sections.length) { + $scope.currentSection = $scope.sections[minSectionIndex]; + $scope.workflowState = $scope.currentSection.id; + $scope.currentSection.scope.isCurrentSection = true; + + // Focus to the first input (if any) in the section. + $timeout(function() { + var inputs = $scope.currentSection.element.find('input'); + if (inputs.length == 1) { + inputs.focus(); + } + }, 10); + } + }; + + $scope.$watch('workflowState', updateState); + } + }; + return directiveDefinitionObject; +}); + +/** + * An element which displays a single section in a linear workflow. + */ +angular.module('quay').directive('linearWorkflowSection', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/linear-workflow-section.html', + replace: false, + transclude: true, + restrict: 'C', + require: '^linearWorkflow', + scope: { + 'sectionId': '@sectionId', + 'sectionTitle': '@sectionTitle', + 'sectionValid': '=?sectionValid', + }, + + link: function($scope, $element, $attrs, $ctrl) { + $ctrl.registerSection($scope, $element); + }, + + controller: function($scope, $element) { + $scope.$watch('sectionVisible', function(visible) { + if (visible) { + $element.show(); + } else { + $element.hide(); + } + }); + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/directives/ui/manage-trigger-custom-git.js b/static/js/directives/ui/manage-trigger-custom-git.js new file mode 100644 index 000000000..e039e11c0 --- /dev/null +++ b/static/js/directives/ui/manage-trigger-custom-git.js @@ -0,0 +1,27 @@ +/** + * An element which displays the setup and management workflow for a custom git trigger. + */ +angular.module('quay').directive('manageTriggerCustomGit', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/manage-trigger-custom-git.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'trigger': '=trigger', + 'activateTrigger': '&activateTrigger' + }, + controller: function($scope, $element) { + $scope.config = {}; + $scope.currentState = null; + + $scope.$watch('trigger', function(trigger) { + if (trigger) { + $scope.config = trigger['config'] || {}; + } + }); + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/directives/ui/manage-trigger-githost.js b/static/js/directives/ui/manage-trigger-githost.js new file mode 100644 index 000000000..c63c86199 --- /dev/null +++ b/static/js/directives/ui/manage-trigger-githost.js @@ -0,0 +1,306 @@ +/** + * An element which displays the setup and management workflow for a normal SCM git trigger. + */ +angular.module('quay').directive('manageTriggerGithost', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/manage-trigger-githost.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'repository': '=repository', + 'trigger': '=trigger', + + 'activateTrigger': '&activateTrigger' + }, + controller: function($scope, $element, ApiService, TableService, TriggerService, RolesService) { + $scope.TableService = TableService; + + $scope.config = {}; + $scope.local = {}; + $scope.currentState = null; + + $scope.namespacesPerPage = 10; + $scope.repositoriesPerPage = 10; + $scope.robotsPerPage = 10; + + $scope.local.namespaceOptions = { + 'filter': '', + 'predicate': 'score', + 'reverse': false, + 'page': 0 + }; + + $scope.local.repositoryOptions = { + 'filter': '', + 'predicate': 'last_updated', + 'reverse': false, + 'page': 0, + 'hideStale': true + }; + + $scope.local.robotOptions = { + 'filter': '', + 'predicate': 'can_read', + 'reverse': false, + 'page': 0 + }; + + $scope.getTriggerIcon = function() { + return TriggerService.getIcon($scope.trigger.service); + }; + + $scope.createTrigger = function() { + var config = { + 'build_source': $scope.local.selectedRepository.full_name, + 'subdir': $scope.local.dockerfilePath.substr(1) // Remove starting / + }; + + if ($scope.local.triggerOptions.hasBranchTagFilter && + $scope.local.triggerOptions.branchTagFilter) { + config['branchtag_regex'] = $scope.local.triggerOptions.branchTagFilter; + } + + var activate = function() { + $scope.activateTrigger({'config': config, 'pull_robot': $scope.local.robotAccount}); + }; + + if ($scope.local.robotAccount) { + if ($scope.local.robotAccount.can_read) { + activate(); + } else { + // Add read permission onto the base repository for the robot and then activate the + // trigger. + var robot_name = $scope.local.robotAccount.name; + RolesService.setRepositoryRole($scope.repository, 'read', 'robot', robot_name, activate); + } + } else { + activate(); + } + }; + + var buildOrderedNamespaces = function() { + if (!$scope.local.namespaces) { + return; + } + + var namespaces = $scope.local.namespaces || []; + $scope.local.orderedNamespaces = TableService.buildOrderedItems(namespaces, + $scope.local.namespaceOptions, + ['id'], + ['score']) + + $scope.local.maxScore = 0; + namespaces.forEach(function(namespace) { + $scope.local.maxScore = Math.max(namespace.score, $scope.local.maxScore); + }); + }; + + var loadNamespaces = function() { + $scope.local.namespaces = null; + $scope.local.selectedNamespace = null; + $scope.local.orderedNamespaces = null; + + $scope.local.selectedRepository = null; + $scope.local.orderedRepositories = null; + + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'trigger_uuid': $scope.trigger.id + }; + + ApiService.listTriggerBuildSourceNamespaces(null, params).then(function(resp) { + $scope.local.namespaces = resp['namespaces']; + $scope.local.repositories = null; + buildOrderedNamespaces(); + }, ApiService.errorDisplay('Could not retrieve the list of ' + $scope.namespaceTitle)) + }; + + var buildOrderedRepositories = function() { + if (!$scope.local.repositories) { + return; + } + + var repositories = $scope.local.repositories || []; + repositories.forEach(function(repository) { + repository['last_updated_datetime'] = new Date(repository['last_updated'] * 1000); + }); + + if ($scope.local.repositoryOptions.hideStale) { + var existingRepositories = repositories; + + repositories = repositories.filter(function(repository) { + var older_date = moment(repository['last_updated_datetime']).add(1, 'months'); + return !moment().isAfter(older_date); + }); + + if (existingRepositories.length > 0 && repositories.length == 0) { + repositories = existingRepositories; + } + } + + $scope.local.orderedRepositories = TableService.buildOrderedItems(repositories, + $scope.local.repositoryOptions, + ['name', 'description'], + []); + }; + + var loadRepositories = function(namespace) { + $scope.local.repositories = null; + $scope.local.selectedRepository = null; + $scope.local.repositoryRefs = null; + $scope.local.triggerOptions = { + 'hasBranchTagFilter': false + }; + + $scope.local.orderedRepositories = null; + + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'trigger_uuid': $scope.trigger.id + }; + + var data = { + 'namespace': namespace.id + }; + + ApiService.listTriggerBuildSources(data, params).then(function(resp) { + if (namespace == $scope.local.selectedNamespace) { + $scope.local.repositories = resp['sources']; + buildOrderedRepositories(); + } + }, ApiService.errorDisplay('Could not retrieve repositories')); + }; + + var loadRepositoryRefs = function(repository) { + $scope.local.repositoryRefs = null; + $scope.local.triggerOptions = { + 'hasBranchTagFilter': false + }; + + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'trigger_uuid': $scope.trigger.id, + 'field_name': 'refs' + }; + + var config = { + 'build_source': repository.full_name + }; + + ApiService.listTriggerFieldValues(config, params).then(function(resp) { + if (repository == $scope.local.selectedRepository) { + $scope.local.repositoryRefs = resp['values']; + $scope.local.repositoryFullRefs = resp['values'].map(function(ref) { + var kind = ref.kind == 'branch' ? 'heads' : 'tags'; + var icon = ref.kind == 'branch' ? 'fa-code-fork' : 'fa-tag'; + return { + 'value': kind + '/' + ref.name, + 'icon': icon, + 'title': ref.name + }; + }); + } + }, ApiService.errorDisplay('Could not retrieve repository refs')); + }; + + var loadDockerfileLocations = function(repository) { + $scope.local.dockerfilePath = null; + + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'trigger_uuid': $scope.trigger.id + }; + + var config = { + 'build_source': repository.full_name + }; + + ApiService.listBuildTriggerSubdirs(config, params).then(function(resp) { + if (repository == $scope.local.selectedRepository) { + $scope.local.dockerfileLocations = resp; + } + }, ApiService.errorDisplay('Could not retrieve Dockerfile locations')); + }; + + var buildOrderedRobotAccounts = function() { + if (!$scope.local.triggerAnalysis || !$scope.local.triggerAnalysis.robots) { + return; + } + + var robots = $scope.local.triggerAnalysis.robots; + $scope.local.orderedRobotAccounts = TableService.buildOrderedItems(robots, + $scope.local.robotOptions, + ['name'], + []); + }; + + var checkDockerfilePath = function(repository, path) { + $scope.local.triggerAnalysis = null; + $scope.local.robotAccount = null; + + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'trigger_uuid': $scope.trigger.id + }; + + var config = { + 'build_source': repository.full_name, + 'subdir': path.substr(1) + }; + + var data = { + 'config': config + }; + + ApiService.analyzeBuildTrigger(data, params).then(function(resp) { + $scope.local.triggerAnalysis = resp; + buildOrderedRobotAccounts(); + }, ApiService.errorDisplay('Could not analyze trigger')); + }; + + $scope.$watch('trigger', function(trigger) { + if (trigger && $scope.repository) { + $scope.config = trigger['config'] || {}; + $scope.namespaceTitle = 'organization'; + $scope.local.selectedNamespace = null; + loadNamespaces(); + } + }); + + $scope.$watch('local.selectedNamespace', function(namespace) { + if (namespace) { + loadRepositories(namespace); + } + }); + + $scope.$watch('local.selectedRepository', function(repository) { + if (repository) { + loadRepositoryRefs(repository); + loadDockerfileLocations(repository); + } + }); + + $scope.$watch('local.dockerfilePath', function(path) { + if (path && $scope.local.selectedRepository) { + checkDockerfilePath($scope.local.selectedRepository, path); + } + }); + + $scope.$watch('local.namespaceOptions.predicate', buildOrderedNamespaces); + $scope.$watch('local.namespaceOptions.reverse', buildOrderedNamespaces); + $scope.$watch('local.namespaceOptions.filter', buildOrderedNamespaces); + + $scope.$watch('local.repositoryOptions.predicate', buildOrderedRepositories); + $scope.$watch('local.repositoryOptions.reverse', buildOrderedRepositories); + $scope.$watch('local.repositoryOptions.filter', buildOrderedRepositories); + $scope.$watch('local.repositoryOptions.hideStale', buildOrderedRepositories); + + $scope.$watch('local.robotOptions.predicate', buildOrderedRobotAccounts); + $scope.$watch('local.robotOptions.reverse', buildOrderedRobotAccounts); + $scope.$watch('local.robotOptions.filter', buildOrderedRobotAccounts); + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/directives/ui/regex-match-view.js b/static/js/directives/ui/regex-match-view.js new file mode 100644 index 000000000..6bd2380a5 --- /dev/null +++ b/static/js/directives/ui/regex-match-view.js @@ -0,0 +1,36 @@ +/** + * An element which displays the matches and non-matches for a regular expression against a set of + * items. + */ +angular.module('quay').directive('regexMatchView', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/regex-match-view.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'regex': '=regex', + 'items': '=items' + }, + controller: function($scope, $element) { + $scope.filterMatches = function(regexstr, items, shouldMatch) { + regexstr = regexstr || '.+'; + + try { + var regex = new RegExp(regexstr); + } catch (ex) { + return null; + } + + return items.filter(function(item) { + var value = item['value']; + var m = value.match(regex); + var matches = !!(m && m[0].length == value.length); + return matches == shouldMatch; + }); + }; + } + }; + return directiveDefinitionObject; +}); \ No newline at end of file diff --git a/static/js/directives/ui/step-view.js b/static/js/directives/ui/step-view.js deleted file mode 100644 index 70583fa1e..000000000 --- a/static/js/directives/ui/step-view.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * An element which displays the steps of the wizard-like dialog, changing them as each step - * is completed. - */ -angular.module('quay').directive('stepView', function ($compile) { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/step-view.html', - replace: true, - transclude: true, - restrict: 'C', - scope: { - 'nextStepCounter': '=nextStepCounter', - 'currentStepValid': '=currentStepValid', - - 'stepsCompleted': '&stepsCompleted' - }, - controller: function($scope, $element, $rootScope) { - var currentStepIndex = -1; - var steps = []; - var watcher = null; - - // Members on 'this' are accessed by the individual steps. - this.register = function(scope, element) { - element.hide(); - - steps.push({ - 'scope': scope, - 'element': element - }); - - nextStep(); - }; - - var getCurrentStep = function() { - return steps[currentStepIndex]; - }; - - var reset = function() { - currentStepIndex = -1; - for (var i = 0; i < steps.length; ++i) { - steps[i].element.hide(); - } - - $scope.currentStepValid = false; - }; - - var next = function() { - if (currentStepIndex >= 0) { - var currentStep = getCurrentStep(); - if (!currentStep || !currentStep.scope) { return; } - - if (!currentStep.scope.completeCondition) { - return; - } - - currentStep.element.hide(); - - if (unwatch) { - unwatch(); - unwatch = null; - } - } - - currentStepIndex++; - - if (currentStepIndex < steps.length) { - var currentStep = getCurrentStep(); - currentStep.element.show(); - currentStep.scope.load() - - unwatch = currentStep.scope.$watch('completeCondition', function(cc) { - $scope.currentStepValid = !!cc; - }); - } else { - $scope.stepsCompleted(); - } - }; - - var nextStep = function() { - if (!steps || !steps.length) { return; } - - if ($scope.nextStepCounter >= 0) { - next(); - } else { - reset(); - } - }; - - $scope.$watch('nextStepCounter', nextStep); - } - }; - return directiveDefinitionObject; -}); - - -/** - * A step in the step view. - */ -angular.module('quay').directive('stepViewStep', function () { - var directiveDefinitionObject = { - priority: 1, - require: '^stepView', - templateUrl: '/static/directives/step-view-step.html', - replace: false, - transclude: true, - restrict: 'C', - scope: { - 'completeCondition': '=completeCondition', - 'loadCallback': '&loadCallback', - 'loadMessage': '@loadMessage' - }, - link: function(scope, element, attrs, controller) { - controller.register(scope, element); - }, - controller: function($scope, $element) { - $scope.load = function() { - $scope.loading = true; - $scope.loadCallback({'callback': function() { - $scope.loading = false; - }}); - }; - } - }; - return directiveDefinitionObject; -}); \ No newline at end of file diff --git a/static/js/directives/ui/trigger-setup-custom.js b/static/js/directives/ui/trigger-setup-custom.js deleted file mode 100644 index b14899efd..000000000 --- a/static/js/directives/ui/trigger-setup-custom.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * An element which displays custom git-specific setup information for its build triggers. - */ -angular.module('quay').directive('triggerSetupCustom', function() { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/trigger-setup-custom.html', - replace: false, - transclude: false, - restrict: 'C', - scope: { - 'repository': '=repository', - 'trigger': '=trigger', - - 'nextStepCounter': '=nextStepCounter', - 'currentStepValid': '=currentStepValid', - - 'analyze': '&analyze' - }, - controller: function($scope, $element, ApiService) { - $scope.analyzeCounter = 0; - $scope.setupReady = false; - - $scope.state = { - 'build_source': null, - 'subdir': null - }; - - $scope.stepsCompleted = function() { - $scope.analyze({'isValid': $scope.state.build_source != null && $scope.state.subdir != null}); - }; - - $scope.$watch('state.build_source', function(build_source) { - $scope.trigger['config']['build_source'] = build_source; - }); - - $scope.$watch('state.subdir', function(subdir) { - $scope.trigger['config']['subdir'] = subdir; - $scope.trigger.$ready = subdir != null; - }); - - $scope.nopLoad = function(callback) { - callback(); - }; - } - }; - - return directiveDefinitionObject; -}); diff --git a/static/js/directives/ui/trigger-setup-githost.js b/static/js/directives/ui/trigger-setup-githost.js deleted file mode 100644 index 03be87852..000000000 --- a/static/js/directives/ui/trigger-setup-githost.js +++ /dev/null @@ -1,242 +0,0 @@ -/** - * An element which displays hosted Git (GitHub, Bitbucket)-specific setup information for its build triggers. - */ -angular.module('quay').directive('triggerSetupGithost', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/trigger-setup-githost.html', - replace: false, - transclude: false, - restrict: 'C', - scope: { - 'repository': '=repository', - 'trigger': '=trigger', - 'kind': '@kind', - - 'nextStepCounter': '=nextStepCounter', - 'currentStepValid': '=currentStepValid', - - 'analyze': '&analyze' - }, - controller: function($scope, $element, ApiService, TriggerService) { - $scope.analyzeCounter = 0; - $scope.setupReady = false; - $scope.refs = null; - $scope.branchNames = null; - $scope.tagNames = null; - - $scope.state = { - 'currentRepo': null, - 'branchTagFilter': '', - 'hasBranchTagFilter': false, - 'isInvalidLocation': true, - 'currentLocation': null - }; - - var checkLocation = function() { - var location = $scope.state.currentLocation || ''; - $scope.state.isInvalidLocation = $scope.supportsFullListing && - $scope.locations.indexOf(location) < 0; - }; - - $scope.isMatching = function(kind, name, filter) { - try { - var patt = new RegExp(filter); - } catch (ex) { - return false; - } - - var fullname = (kind + '/' + name); - var m = fullname.match(patt); - return m && m[0].length == fullname.length; - } - - $scope.addRef = function(kind, name) { - if ($scope.isMatching(kind, name, $scope.state.branchTagFilter)) { - return; - } - - var newFilter = kind + '/' + name; - var existing = $scope.state.branchTagFilter; - if (existing) { - $scope.state.branchTagFilter = '(' + existing + ')|(' + newFilter + ')'; - } else { - $scope.state.branchTagFilter = newFilter; - } - } - - $scope.stepsCompleted = function() { - $scope.analyze({'isValid': !$scope.state.isInvalidLocation}); - }; - - $scope.loadRepositories = function(callback) { - if (!$scope.trigger || !$scope.repository) { return; } - - var params = { - 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'trigger_uuid': $scope.trigger.id - }; - - ApiService.listTriggerBuildSources(null, params).then(function(resp) { - $scope.orgs = resp['sources']; - setupTypeahead(); - callback(); - }, ApiService.errorDisplay('Cannot load repositories')); - }; - - $scope.loadBranchesAndTags = function(callback) { - if (!$scope.trigger || !$scope.repository) { return; } - - var params = { - 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'trigger_uuid': $scope.trigger['id'], - 'field_name': 'refs' - }; - - ApiService.listTriggerFieldValues($scope.trigger['config'], params).then(function(resp) { - $scope.refs = resp['values']; - $scope.branchNames = []; - $scope.tagNames = []; - - for (var i = 0; i < $scope.refs.length; ++i) { - var ref = $scope.refs[i]; - if (ref.kind == 'branch') { - $scope.branchNames.push(ref.name); - } else { - $scope.tagNames.push(ref.name); - } - } - - callback(); - }, ApiService.errorDisplay('Cannot load branch and tag names')); - }; - - $scope.loadLocations = function(callback) { - if (!$scope.trigger || !$scope.repository) { return; } - - $scope.locations = null; - - var params = { - 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'trigger_uuid': $scope.trigger.id - }; - - ApiService.listBuildTriggerSubdirs($scope.trigger['config'], params).then(function(resp) { - if (resp['status'] == 'error') { - $scope.locations = []; - callback(resp['message'] || 'Could not load Dockerfile locations'); - return; - } - - $scope.locations = resp['subdir'] || []; - - // Select a default location (if any). - if ($scope.locations.length > 0) { - $scope.setLocation($scope.locations[0]); - } else { - $scope.state.currentLocation = null; - $scope.trigger.$ready = true; - checkLocation(); - } - - callback(); - }, ApiService.errorDisplay('Cannot load locations')); - } - - $scope.handleLocationInput = function(location) { - $scope.trigger['config']['subdir'] = location || ''; - $scope.trigger.$ready = true; - checkLocation(); - }; - - $scope.handleLocationSelected = function(datum) { - $scope.setLocation(datum['value']); - }; - - $scope.setLocation = function(location) { - $scope.state.currentLocation = location; - $scope.trigger['config']['subdir'] = location || ''; - $scope.trigger.$ready = true; - checkLocation(); - }; - - $scope.selectRepo = function(repo, org) { - $scope.state.currentRepo = { - 'repo': repo, - 'avatar_url': org['info']['avatar_url'], - 'toString': function() { - return this.repo; - } - }; - }; - - $scope.selectRepoInternal = function(currentRepo) { - $scope.trigger.$ready = false; - - var params = { - 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'trigger_uuid': $scope.trigger['id'] - }; - - var repo = currentRepo['repo']; - $scope.trigger['config'] = { - 'build_source': repo, - 'subdir': '' - }; - }; - - $scope.scmIcon = function(kind) { - return TriggerService.getIcon(kind); - }; - - var setupTypeahead = function() { - var repos = []; - for (var i = 0; i < $scope.orgs.length; ++i) { - var org = $scope.orgs[i]; - var orepos = org['repos']; - for (var j = 0; j < orepos.length; ++j) { - var repoValue = { - 'repo': orepos[j], - 'avatar_url': org['info']['avatar_url'], - 'toString': function() { - return this.repo; - } - }; - var datum = { - 'name': orepos[j], - 'org': org, - 'value': orepos[j], - 'title': orepos[j], - 'item': repoValue - }; - repos.push(datum); - } - } - - $scope.repoLookahead = repos; - }; - - $scope.$watch('trigger', function(trigger) { - if (!trigger) { return; } - $scope.supportsFullListing = TriggerService.supportsFullListing(trigger.service) - }); - - $scope.$watch('state.currentRepo', function(repo) { - if (repo) { - $scope.selectRepoInternal(repo); - } - }); - - $scope.$watch('state.branchTagFilter', function(bf) { - if (!$scope.trigger) { return; } - - if ($scope.state.hasBranchTagFilter) { - $scope.trigger['config']['branchtag_regex'] = bf; - } else { - delete $scope.trigger['config']['branchtag_regex']; - } - }); - } - }; - return directiveDefinitionObject; -}); \ No newline at end of file diff --git a/static/js/pages/trigger-setup.js b/static/js/pages/trigger-setup.js new file mode 100644 index 000000000..84c2ae1bc --- /dev/null +++ b/static/js/pages/trigger-setup.js @@ -0,0 +1,89 @@ +(function() { + /** + * Trigger setup page. + */ + angular.module('quayPages').config(['pages', function(pages) { + pages.create('trigger-setup', 'trigger-setup.html', TriggerSetupCtrl, { + 'title': 'Setup build trigger', + 'description': 'Setup build trigger', + 'newLayout': true + }); + }]); + + function TriggerSetupCtrl($scope, ApiService, $routeParams, $location, UserService, TriggerService) { + var namespace = $routeParams.namespace; + var name = $routeParams.name; + var trigger_uuid = $routeParams.triggerid; + + var loadRepository = function() { + var params = { + 'repository': namespace + '/' + name + }; + + $scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) { + $scope.repository = repo; + }); + }; + + var loadTrigger = function() { + var params = { + 'repository': namespace + '/' + name, + 'trigger_uuid': trigger_uuid + }; + + $scope.triggerResource = ApiService.getBuildTriggerAsResource(params).get(function(trigger) { + $scope.trigger = trigger; + }); + }; + + loadTrigger(); + loadRepository(); + + $scope.state = 'managing'; + + $scope.activateTrigger = function(config, pull_robot) { + $scope.state = 'activating'; + var params = { + 'repository': namespace + '/' + name, + 'trigger_uuid': trigger_uuid + }; + + var data = { + 'config': config + }; + + if (pull_robot) { + data['pull_robot'] = pull_robot['name']; + } + + var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) { + $scope.state = 'managing'; + return ApiService.getErrorMessage(resp) + + '\n\nNote: Errors can occur if you do not have admin access on the repository'; + }); + + ApiService.activateBuildTrigger(data, params).then(function(resp) { + $scope.trigger['is_active'] = true; + $scope.trigger['config'] = resp['config']; + $scope.trigger['pull_robot'] = resp['pull_robot']; + $scope.trigger['repository_url'] = resp['repository_url']; + $scope.state = 'activated'; + + // If there are no credentials to display, redirect to the builds tab. + if (!$scope.trigger['config'].credentials) { + $location.url('/repository/' + namespace + '/' + name + '?tab=builds'); + } + }, errorHandler); + }; + + $scope.getTriggerIcon = function() { + if (!$scope.trigger) { return ''; } + return TriggerService.getIcon($scope.trigger.service); + }; + + $scope.getTriggerId = function() { + if (!trigger_uuid) { return ''; } + return trigger_uuid.split('-')[0]; + }; + } +}()); \ No newline at end of file diff --git a/static/js/quay.routes.ts b/static/js/quay.routes.ts index 628c77dbd..1714c51ba 100644 --- a/static/js/quay.routes.ts +++ b/static/js/quay.routes.ts @@ -43,6 +43,9 @@ export function routeConfig( // Repo Build View .route('/repository/:namespace/:name/build/:buildid', 'build-view') + // Repo Trigger View + .route('/repository/:namespace/:name/trigger/:triggerid', 'trigger-setup') + // Create repository notification .route('/repository/:namespace/:name/create-notification', 'create-repository-notification') diff --git a/static/partials/trigger-setup.html b/static/partials/trigger-setup.html new file mode 100644 index 000000000..6ccb87bda --- /dev/null +++ b/static/partials/trigger-setup.html @@ -0,0 +1,65 @@ +
+
+
+ + + + {{ repository.namespace }}/{{ repository.name }} + + + + + Setup Build Trigger: {{ getTriggerId() }} + +
+ +
+
+ Trigger has already been activated. +
+
+ +
+ +
+
+
+

Trigger has been successfully activated

+
+ +
+
+
+ + +
+ +
+ +
+
+
+ + +
+
+
+
+ +
+
Completing setup of the build trigger +
+
+ +
+
+
diff --git a/test/test_api_security.py b/test/test_api_security.py index 62825709e..66d45e718 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -1301,17 +1301,17 @@ class TestBuildTriggerSources831cPublicPublicrepo(ApiTestCase): ApiTestCase.setUp(self) self._set_url(BuildTriggerSources, trigger_uuid="831C", repository="public/publicrepo") - def test_get_anonymous(self): - self._run_test('GET', 401, None, None) + def test_post_anonymous(self): + self._run_test('POST', 401, None, None) - def test_get_freshuser(self): - self._run_test('GET', 403, 'freshuser', None) + def test_post_freshuser(self): + self._run_test('POST', 403, 'freshuser', None) - def test_get_reader(self): - self._run_test('GET', 403, 'reader', None) + def test_post_reader(self): + self._run_test('POST', 403, 'reader', None) - def test_get_devtable(self): - self._run_test('GET', 403, 'devtable', None) + def test_post_devtable(self): + self._run_test('POST', 403, 'devtable', dict(namespace="foo")) class TestBuildTriggerSources831cDevtableShared(ApiTestCase): @@ -1319,17 +1319,17 @@ class TestBuildTriggerSources831cDevtableShared(ApiTestCase): ApiTestCase.setUp(self) self._set_url(BuildTriggerSources, trigger_uuid="831C", repository="devtable/shared") - def test_get_anonymous(self): - self._run_test('GET', 401, None, None) + def test_post_anonymous(self): + self._run_test('POST', 401, None, None) - def test_get_freshuser(self): - self._run_test('GET', 403, 'freshuser', None) + def test_post_freshuser(self): + self._run_test('POST', 403, 'freshuser', None) - def test_get_reader(self): - self._run_test('GET', 403, 'reader', None) + def test_post_reader(self): + self._run_test('POST', 403, 'reader', None) - def test_get_devtable(self): - self._run_test('GET', 404, 'devtable', None) + def test_post_devtable(self): + self._run_test('POST', 404, 'devtable', dict(namespace="foo")) class TestBuildTriggerSources831cBuynlargeOrgrepo(ApiTestCase): @@ -1337,17 +1337,17 @@ class TestBuildTriggerSources831cBuynlargeOrgrepo(ApiTestCase): ApiTestCase.setUp(self) self._set_url(BuildTriggerSources, trigger_uuid="831C", repository="buynlarge/orgrepo") - def test_get_anonymous(self): - self._run_test('GET', 401, None, None) + def test_post_anonymous(self): + self._run_test('POST', 401, None, None) - def test_get_freshuser(self): - self._run_test('GET', 403, 'freshuser', None) + def test_post_freshuser(self): + self._run_test('POST', 403, 'freshuser', None) - def test_get_reader(self): - self._run_test('GET', 403, 'reader', None) + def test_post_reader(self): + self._run_test('POST', 403, 'reader', None) - def test_get_devtable(self): - self._run_test('GET', 404, 'devtable', None) + def test_post_devtable(self): + self._run_test('POST', 404, 'devtable', dict(namespace="foo")) class TestBuildTriggerSubdirs4i2yPublicPublicrepo(ApiTestCase): diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 277410234..eead6487f 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -40,7 +40,8 @@ from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobo RegenerateUserRobot, RegenerateOrgRobot) from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs, TriggerBuildList, ActivateBuildTrigger, BuildTrigger, - BuildTriggerList, BuildTriggerAnalyze, BuildTriggerFieldValues) + BuildTriggerList, BuildTriggerAnalyze, BuildTriggerFieldValues, + BuildTriggerSourceNamespaces) from endpoints.api.repoemail import RepositoryAuthorizedEmail from endpoints.api.repositorynotification import (RepositoryNotification, RepositoryNotificationList, @@ -3758,8 +3759,23 @@ class FakeBuildTrigger(BuildTriggerHandler): def service_name(cls): return 'fakeservice' - def list_build_sources(self): - return [{'first': 'source'}, {'second': self.auth_token}] + def list_build_source_namespaces(self): + return [ + {'name': 'first', 'id': 'first'}, + {'name': 'second', 'id': 'second'}, + ] + + def list_build_sources_for_namespace(self, namespace): + if namespace == "first": + return [{ + 'name': 'source', + }] + elif namespace == "second": + return [{ + 'name': self.auth_token, + }] + else: + return [] def list_build_subdirs(self): return [self.auth_token, 'foo', 'bar', self.config['somevalue']] @@ -3882,8 +3898,9 @@ class TestBuildTriggers(ApiTestCase): trigger_uuid=trigger.uuid), data={'config': trigger_config}) - self.assertEquals('error', analyze_json['status']) - self.assertEquals('Could not read the Dockerfile for the trigger', analyze_json['message']) + self.assertEquals('warning', analyze_json['status']) + self.assertEquals('Specified Dockerfile path for the trigger was not ' + + 'found on the main branch. This trigger may fail.', analyze_json['message']) # Analyze the trigger's dockerfile: Second, missing FROM in dockerfile. trigger_config = {'dockerfile': 'MAINTAINER me'} @@ -3943,10 +3960,9 @@ class TestBuildTriggers(ApiTestCase): trigger_uuid=trigger.uuid), data={'config': trigger_config}) - self.assertEquals('analyzed', analyze_json['status']) + self.assertEquals('requiresrobot', analyze_json['status']) self.assertEquals('devtable', analyze_json['namespace']) self.assertEquals('complex', analyze_json['name']) - self.assertEquals(False, analyze_json['is_public']) self.assertEquals(ADMIN_ACCESS_USER + '+dtrobot', analyze_json['robots'][0]['name']) @@ -3968,11 +3984,18 @@ class TestBuildTriggers(ApiTestCase): self.assertEquals(trigger.service.name, json['triggers'][0]['service']) self.assertEquals(False, json['triggers'][0]['is_active']) - # List the trigger's sources. - source_json = self.getJsonResponse(BuildTriggerSources, + # List the trigger's source namespaces. + namespace_json = self.getJsonResponse(BuildTriggerSourceNamespaces, + params=dict(repository=ADMIN_ACCESS_USER + '/simple', + trigger_uuid=trigger.uuid)) + self.assertEquals([{'id': 'first', 'name': 'first'}, {'id': 'second', 'name': 'second'}], namespace_json['namespaces']) + + + source_json = self.postJsonResponse(BuildTriggerSources, params=dict(repository=ADMIN_ACCESS_USER + '/simple', - trigger_uuid=trigger.uuid)) - self.assertEquals([{'first': 'source'}, {'second': 'sometoken'}], source_json['sources']) + trigger_uuid=trigger.uuid), + data=dict(namespace='first')) + self.assertEquals([{'name': 'source'}], source_json['sources']) # List the trigger's subdirs. subdir_json = self.postJsonResponse(BuildTriggerSubdirs, @@ -3980,7 +4003,7 @@ class TestBuildTriggers(ApiTestCase): trigger_uuid=trigger.uuid), data={'somevalue': 'meh'}) - self.assertEquals({'status': 'success', 'subdir': ['sometoken', 'foo', 'bar', 'meh']}, + self.assertEquals({'status': 'success', 'subdir': ['/sometoken', '/foo', '/bar', '/meh']}, subdir_json) # Activate the trigger. From c60ce4a69664f2db5632eddc3611e285ac558a70 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Fri, 17 Feb 2017 02:55:52 -0800 Subject: [PATCH 02/29] using decorators to write AngularJS in nearly identical syntax to Angular 2 --- external_libraries.py | 2 +- package.json | 1 + static/directives/manage-trigger-githost.html | 8 +- static/directives/regex-match-view.html | 29 --- static/js/constants/name-patterns.constant.ts | 2 +- static/js/constants/pages.constant.ts | 44 ---- static/js/directives/ui/regex-match-view.js | 36 --- .../regex-match-view.component.spec.ts | 14 ++ .../regex-match-view.component.ts | 67 ++++++ static/js/quay-pages.module.ts | 28 ++- static/js/quay-routes.module.ts | 146 ++++++++++++ static/js/quay.config.ts | 67 ------ static/js/quay.module.ts | 221 ++++++++++++++++-- static/js/quay.routes.ts | 138 ----------- static/js/quay.run.ts | 143 ------------ .../services/page/page.service.impl.spec.ts | 22 ++ static/js/services/page/page.service.impl.ts | 45 ++++ static/js/services/page/page.service.ts | 32 +++ .../js/services/view-array/view-array.impl.ts | 2 + 19 files changed, 559 insertions(+), 488 deletions(-) delete mode 100644 static/directives/regex-match-view.html delete mode 100644 static/js/constants/pages.constant.ts delete mode 100644 static/js/directives/ui/regex-match-view.js create mode 100644 static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts create mode 100644 static/js/directives/ui/regex-match-view/regex-match-view.component.ts create mode 100644 static/js/quay-routes.module.ts delete mode 100644 static/js/quay.config.ts delete mode 100644 static/js/quay.routes.ts delete mode 100644 static/js/quay.run.ts create mode 100644 static/js/services/page/page.service.impl.spec.ts create mode 100644 static/js/services/page/page.service.impl.ts create mode 100644 static/js/services/page/page.service.ts diff --git a/external_libraries.py b/external_libraries.py index f2f9c3832..ef1a9d03f 100644 --- a/external_libraries.py +++ b/external_libraries.py @@ -7,7 +7,7 @@ LOCAL_DIRECTORY = '/static/ldn/' EXTERNAL_JS = [ 'code.jquery.com/jquery.js', 'netdna.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js', - 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js', + 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-route.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-sanitize.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-animate.min.js', diff --git a/package.json b/package.json index 7a471e930..e26efe744 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/react": "0.14.39", "@types/react-dom": "0.14.17", "angular-mocks": "^1.5.3", + "angular-ts-decorators": "0.0.19", "css-loader": "0.25.0", "jasmine-core": "^2.5.2", "jasmine-ts": "0.0.3", diff --git a/static/directives/manage-trigger-githost.html b/static/directives/manage-trigger-githost.html index 4912b5c2f..4645a2f2f 100644 --- a/static/directives/manage-trigger-githost.html +++ b/static/directives/manage-trigger-githost.html @@ -184,10 +184,10 @@
Examples: heads/master, tags/tagname, heads/.+
-
+ diff --git a/static/directives/regex-match-view.html b/static/directives/regex-match-view.html deleted file mode 100644 index da9316838..000000000 --- a/static/directives/regex-match-view.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
- Invalid Regular Expression! -
-
- - - - - - - - - -
Matching: -
    -
  • - {{ item.title }} -
  • -
-
Not Matching: -
    -
  • - {{ item.title }} -
  • -
-
-
-
\ No newline at end of file diff --git a/static/js/constants/name-patterns.constant.ts b/static/js/constants/name-patterns.constant.ts index 422887c3e..942028ee1 100644 --- a/static/js/constants/name-patterns.constant.ts +++ b/static/js/constants/name-patterns.constant.ts @@ -1,7 +1,7 @@ /** * Regex patterns to for validating account names. */ -export default { +export const NAME_PATTERNS: any = { TEAM_PATTERN: '^[a-z][a-z0-9]+$', ROBOT_PATTERN: '^[a-z][a-z0-9_]{1,254}$', USERNAME_PATTERN: '^(?=.{2,255}$)([a-z0-9]+(?:[._-][a-z0-9]+)*)$', diff --git a/static/js/constants/pages.constant.ts b/static/js/constants/pages.constant.ts deleted file mode 100644 index 6d0452b8c..000000000 --- a/static/js/constants/pages.constant.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Manages the creation and retrieval of pages (route + controller) - * TODO: Convert to class/Angular service - */ -export default { - _pages: {}, - - /** - * Create a page. - * @param pageName The name of the page. - * @param templateName The file name of the template. - * @param controller Controller for the page. - * @param flags Additional flags passed to route provider. - * @param profiles Available profiles. - */ - create: function(pageName: string, templateName: string, controller?: Object, flags = {}, profiles = ['old-layout', 'layout']) { - for (var i = 0; i < profiles.length; ++i) { - this._pages[profiles[i] + ':' + pageName] = { - 'name': pageName, - 'controller': controller, - 'templateName': templateName, - 'flags': flags - }; - } - }, - - /** - * Retrieve a registered page. - * @param pageName The name of the page. - * @param profiles Available profiles to search. - */ - get: function(pageName: string, profiles: any[]) { - for (var i = 0; i < profiles.length; ++i) { - var current = profiles[i]; - var key = current.id + ':' + pageName; - var page = this._pages[key]; - if (page) { - return [current, page]; - } - } - - return null; - } -}; diff --git a/static/js/directives/ui/regex-match-view.js b/static/js/directives/ui/regex-match-view.js deleted file mode 100644 index 6bd2380a5..000000000 --- a/static/js/directives/ui/regex-match-view.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * An element which displays the matches and non-matches for a regular expression against a set of - * items. - */ -angular.module('quay').directive('regexMatchView', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/regex-match-view.html', - replace: false, - transclude: true, - restrict: 'C', - scope: { - 'regex': '=regex', - 'items': '=items' - }, - controller: function($scope, $element) { - $scope.filterMatches = function(regexstr, items, shouldMatch) { - regexstr = regexstr || '.+'; - - try { - var regex = new RegExp(regexstr); - } catch (ex) { - return null; - } - - return items.filter(function(item) { - var value = item['value']; - var m = value.match(regex); - var matches = !!(m && m[0].length == value.length); - return matches == shouldMatch; - }); - }; - } - }; - return directiveDefinitionObject; -}); \ No newline at end of file diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts b/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts new file mode 100644 index 000000000..86e18b99c --- /dev/null +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts @@ -0,0 +1,14 @@ +import { RegexMatchViewComponent } from './regex-match-view.component'; + + +describe("RegexMatchViewComponent", () => { + var component: RegexMatchViewComponent; + + beforeEach(() => { + component = new RegexMatchViewComponent(); + }); + + describe("filterMatches", () => { + + }); +}); diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.ts b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts new file mode 100644 index 000000000..2c987906c --- /dev/null +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts @@ -0,0 +1,67 @@ +import { Input, Component } from 'angular-ts-decorators'; + + +/** + * An element which displays the matches and non-matches for a regular expression against a set of + * items. + */ +@Component({ + selector: 'regexMatchView', + template: ` +
+
+ Invalid Regular Expression! +
+
+ + + + + + + + + +
Matching: +
    +
  • + {{ item.title }} +
  • +
+
Not Matching: +
    +
  • + {{ item.title }} +
  • +
+
+
+
+ ` +}) +export class RegexMatchViewComponent implements ng.IComponentController { + + @Input('=') private regex: string; + @Input('=') private items: any[]; + + constructor() { + + } + + public filterMatches(regexstr: string, items: any[], shouldMatch: boolean): any[] { + regexstr = regexstr || '.+'; + + try { + var regex = new RegExp(regexstr); + } catch (ex) { + return null; + } + + return items.filter(function(item) { + var value = item['value']; + var m = value.match(regex); + var matches: boolean = !!(m && m[0].length == value.length); + return matches == shouldMatch; + }); + } +} diff --git a/static/js/quay-pages.module.ts b/static/js/quay-pages.module.ts index 871cb78f0..23ecc26a0 100644 --- a/static/js/quay-pages.module.ts +++ b/static/js/quay-pages.module.ts @@ -1,12 +1,28 @@ import * as angular from 'angular'; import { rpHeaderDirective, rpBodyDirective, rpSidebarDirective } from './directives/components/pages/repo-page/main'; -import pages from './constants/pages.constant'; +import { PageServiceImpl } from './services/page/page.service.impl'; +import { NgModule } from 'angular-ts-decorators'; -export default angular - .module('quayPages', []) - .constant('pages', pages) +/** + * Module containing registered application page/view components. + */ +@NgModule({ + imports: [], + declarations: [], + providers: [ + PageServiceImpl, + ] +}) +export class quayPages { + +} + + +// TODO: Move component registration to @NgModule and remove this. +angular + .module(quayPages.name) + .constant('pages', new PageServiceImpl()) .directive('rpHeader', rpHeaderDirective) .directive('rpSidebar', rpSidebarDirective) - .directive('rpBody', rpBodyDirective) - .name; + .directive('rpBody', rpBodyDirective); diff --git a/static/js/quay-routes.module.ts b/static/js/quay-routes.module.ts new file mode 100644 index 000000000..13715a264 --- /dev/null +++ b/static/js/quay-routes.module.ts @@ -0,0 +1,146 @@ +import { RouteBuilderImpl } from './services/route-builder/route-builder.service.impl'; +import { RouteBuilder } from './services/route-builder/route-builder.service'; +import { PageService } from './services/page/page.service'; +import * as ng from '@types/angular'; +import { NgModule } from 'angular-ts-decorators'; +import { INJECTED_FEATURES } from './constants/injected-values.constant'; +import { quayPages } from './quay-pages.module'; + + +/** + * Module containing client-side routing configuration. + */ +@NgModule({ + imports: [ + quayPages, + 'ngRoute', + ], + declarations: [], + providers: [], +}) +export class QuayRoutes { + + public config($routeProvider: ng.route.IRouteProvider, + $locationProvider: ng.ILocationProvider, + pages: PageService): void { + $locationProvider.html5Mode(true); + + // WARNING WARNING WARNING + // If you add a route here, you must add a corresponding route in thr endpoints/web.py + // index rule to make sure that deep links directly deep into the app continue to work. + // WARNING WARNING WARNING + + var routeBuilder: RouteBuilder = new RouteBuilderImpl($routeProvider, pages.$get()); + + if (INJECTED_FEATURES.SUPER_USERS) { + // QE Management + routeBuilder.route('/superuser/', 'superuser') + // QE Setup + .route('/setup/', 'setup'); + } + + routeBuilder + // Repository View + .route('/repository/:namespace/:name', 'repo-view') + .route('/repository/:namespace/:name/tag/:tag', 'repo-view') + + // Image View + .route('/repository/:namespace/:name/image/:image', 'image-view') + + // Repo Build View + .route('/repository/:namespace/:name/build/:buildid', 'build-view') + + // Repo Trigger View + .route('/repository/:namespace/:name/trigger/:triggerid', 'trigger-setup') + + // Create repository notification + .route('/repository/:namespace/:name/create-notification', 'create-repository-notification') + + // Repo List + .route('/repository/', 'repo-list') + + // Organizations + .route('/organizations/', 'organizations') + + // New Organization + .route('/organizations/new/', 'new-organization') + + // View Organization + .route('/organization/:orgname', 'org-view') + + // View Organization Team + .route('/organization/:orgname/teams/:teamname', 'team-view') + + // Organization View Application + .route('/organization/:orgname/application/:clientid', 'manage-application') + + // View Organization Billing + .route('/organization/:orgname/billing', 'billing') + + // View Organization Billing Invoices + .route('/organization/:orgname/billing/invoices', 'invoices') + + // View User + .route('/user/:username', 'user-view') + + // View User Billing + .route('/user/:username/billing', 'billing') + + // View User Billing Invoices + .route('/user/:username/billing/invoices', 'invoices') + + // Sign In + .route('/signin/', 'signin') + + // New Repository + .route('/new/', 'new-repo') + + // Plans + .route('/plans/', 'plans') + + // Tutorial + .route('/tutorial/', 'tutorial') + + // Contact + .route('/contact/', 'contact') + + // About + .route('/about/', 'about') + + // Security + .route('/security/', 'security') + + // TOS + .route('/tos', 'tos') + + // Privacy + .route('/privacy', 'privacy') + + // Change username + .route('/updateuser', 'update-user') + + // Landing Page + .route('/', 'landing') + + // Tour + .route('/tour/', 'tour') + .route('/tour/features', 'tour') + .route('/tour/organizations', 'tour') + .route('/tour/enterprise', 'tour') + + // Confirm Invite + .route('/confirminvite', 'confirm-invite') + + // Enterprise marketing page + .route('/enterprise', 'enterprise') + + // Public Repo Experiments + .route('/__exp/publicRepo', 'public-repo-exp') + + // 404/403 + .route('/:catchall', 'error-view') + .route('/:catch/:all', 'error-view') + .route('/:catch/:all/:things', 'error-view') + .route('/:catch/:all/:things/:here', 'error-view'); + } +} diff --git a/static/js/quay.config.ts b/static/js/quay.config.ts deleted file mode 100644 index af2a36af9..000000000 --- a/static/js/quay.config.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as Raven from 'raven-js'; - - -quayConfig.$inject = [ - '$provide', - '$injector', - 'INJECTED_CONFIG', - 'cfpLoadingBarProvider', - '$tooltipProvider', - '$compileProvider', - 'RestangularProvider', -]; - -export function quayConfig( - $provide: ng.auto.IProvideService, - $injector: ng.auto.IInjectorService, - INJECTED_CONFIG: any, - cfpLoadingBarProvider: any, - $tooltipProvider: any, - $compileProvider: ng.ICompileProvider, - RestangularProvider: any) { - cfpLoadingBarProvider.includeSpinner = false; - - // decorate the tooltip getter - var tooltipFactory: any = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; - $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window: ng.IWindowService) { - if ('ontouchstart' in $window) { - var existing: any = tooltipFactory.apply(this, arguments); - return function(element) { - // Note: We only disable bs-tooltip's themselves. $tooltip is used for other things - // (such as the datepicker), so we need to be specific when canceling it. - if (element !== undefined && element.attr('bs-tooltip') == null) { - return existing.apply(this, arguments); - } - }; - } - - return tooltipFactory.apply(this, arguments); - }; - - if (!INJECTED_CONFIG['DEBUG']) { - $compileProvider.debugInfoEnabled(false); - } - - // Configure compile provider to add additional URL prefixes to the sanitization list. We use - // these on the Contact page. - $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|irc):/); - - // Configure the API provider. - RestangularProvider.setBaseUrl('/api/v1/'); - - // Configure analytics. - if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { - let $analyticsProvider: any = $injector.get('$analyticsProvider'); - $analyticsProvider.virtualPageviews(true); - } - - // Configure sentry. - if (INJECTED_CONFIG && INJECTED_CONFIG.SENTRY_PUBLIC_DSN) { - $provide.decorator("$exceptionHandler", function($delegate) { - return function(ex, cause) { - $delegate(ex, cause); - Raven.captureException(ex, {extra: {cause: cause}}); - }; - }); - } -} diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index c149f1ba3..6e37c162f 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -1,16 +1,15 @@ -import * as angular from 'angular'; -import { quayConfig } from './quay.config'; -import quayPages from './quay-pages.module'; -import quayRun from './quay.run'; -import { ViewArrayImpl } from './services/view-array/view-array.impl'; -import NAME_PATTERNS from './constants/name-patterns.constant'; -import { routeConfig } from './quay.routes'; -import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from './constants/injected-values.constant'; +import * as angular from "angular"; +import * as Raven from "raven-js"; +import { ViewArrayImpl } from "./services/view-array/view-array.impl"; +import { NAME_PATTERNS } from "./constants/name-patterns.constant"; +import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant"; +import { RegexMatchViewComponent } from "./directives/ui/regex-match-view/regex-match-view.component"; +import { NgModule } from "angular-ts-decorators"; +import { QuayRoutes } from "./quay-routes.module"; -var quayDependencies: string[] = [ - quayPages, - 'ngRoute', +var quayDependencies: any[] = [ + QuayRoutes, 'chieffancypants.loadingBar', 'cfp.hotkeys', 'angular-tour', @@ -46,14 +45,198 @@ if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { quayDependencies.push('vcRecaptcha'); } -export default angular - .module('quay', quayDependencies) - .config(quayConfig) - .config(routeConfig) + +/** + * Main application module. + */ +@NgModule({ + imports: quayDependencies, + declarations: [ + RegexMatchViewComponent, + ], + providers: [ + ViewArrayImpl, + ], +}) +export class quay { + + public config($provide: ng.auto.IProvideService, + $injector: ng.auto.IInjectorService, + INJECTED_CONFIG: any, + cfpLoadingBarProvider: any, + $tooltipProvider: any, + $compileProvider: ng.ICompileProvider, + RestangularProvider: any): void { + cfpLoadingBarProvider.includeSpinner = false; + + // decorate the tooltip getter + var tooltipFactory: any = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; + $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window: ng.IWindowService) { + if ('ontouchstart' in $window) { + var existing: any = tooltipFactory.apply(this, arguments); + return function(element) { + // Note: We only disable bs-tooltip's themselves. $tooltip is used for other things + // (such as the datepicker), so we need to be specific when canceling it. + if (element !== undefined && element.attr('bs-tooltip') == null) { + return existing.apply(this, arguments); + } + }; + } + + return tooltipFactory.apply(this, arguments); + }; + + if (!INJECTED_CONFIG['DEBUG']) { + $compileProvider.debugInfoEnabled(false); + } + + // Configure compile provider to add additional URL prefixes to the sanitization list. We use + // these on the Contact page. + $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|irc):/); + + // Configure the API provider. + RestangularProvider.setBaseUrl('/api/v1/'); + + // Configure analytics. + if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { + let $analyticsProvider: any = $injector.get('$analyticsProvider'); + $analyticsProvider.virtualPageviews(true); + } + + // Configure sentry. + if (INJECTED_CONFIG && INJECTED_CONFIG.SENTRY_PUBLIC_DSN) { + $provide.decorator("$exceptionHandler", function($delegate) { + return function(ex, cause) { + $delegate(ex, cause); + Raven.captureException(ex, {extra: {cause: cause}}); + }; + }); + } + } + + public run($rootScope: QuayRunScope, + Restangular: any, + PlanService: any, + $http: ng.IHttpService, + CookieService: any, + Features: any, + $anchorScroll: ng.IAnchorScrollService, + MetaService: any, + INJECTED_CONFIG: any): void { + var defaultTitle = INJECTED_CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; + + // Handle session security. + Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], + {'_csrf_token': (window).__token || ''}); + + // Handle session expiration. + Restangular.setErrorInterceptor(function(response) { + if (response !== undefined && response.status == 503) { + ($('#cannotContactService')).modal({}); + return false; + } + + if (response !== undefined && response.status == 500) { + window.location.href = '/500'; + return false; + } + + if (response !== undefined && !response.data) { + return true; + } + + var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; + if (response !== undefined && response.status == 401 && + invalid_token && + response.data['session_required'] !== false) { + ($('#sessionexpiredModal')).modal({}); + return false; + } + + return true; + }); + + // Check if we need to redirect based on a previously chosen plan. + var result = PlanService.handleNotedPlan(); + + // Check to see if we need to show a redirection page. + var redirectUrl = CookieService.get('quay.redirectAfterLoad'); + CookieService.clear('quay.redirectAfterLoad'); + + if (!result && redirectUrl && redirectUrl.indexOf((window).location.href) == 0) { + (window).location = redirectUrl; + return; + } + + $rootScope.$watch('description', function(description: string) { + if (!description) { + description = `Hosted private docker repositories. Includes full user management and history. + Free for public repositories.`; + } + + // Note: We set the content of the description tag manually here rather than using Angular binding + // because we need the tag to have a default description that is not of the form "{{ description }}", + // we read by tools that do not properly invoke the Angular code. + $('#descriptionTag').attr('content', description); + }); + + // Listen for scope changes and update the title and description accordingly. + $rootScope.$watch(function() { + var title = MetaService.getTitle($rootScope.currentPage) || defaultTitle; + $rootScope.title = title; + + var description = MetaService.getDescription($rootScope.currentPage) || ''; + if ($rootScope.description != description) { + $rootScope.description = description; + } + }); + + $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + $rootScope.current = current.$$route; + $rootScope.currentPage = current; + $rootScope.pageClass = ''; + + if (!current.$$route) { return; } + + var pageClass = current.$$route.pageClass || ''; + if (typeof pageClass != 'string') { + pageClass = pageClass(Features); + } + + $rootScope.pageClass = pageClass; + $rootScope.newLayout = !!current.$$route.newLayout; + $rootScope.fixFooter = !!current.$$route.fixFooter; + + $anchorScroll(); + }); + + var initallyChecked: boolean = false; + (window).__isLoading = function() { + if (!initallyChecked) { + initallyChecked = true; + return true; + } + return $http.pendingRequests.length > 0; + }; + } +} + + +interface QuayRunScope extends ng.IRootScopeService { + currentPage: any; + current: any; + title: any; + description: string, + pageClass: any; + newLayout: any; + fixFooter: any; +} + + +// TODO: Move component registration to @NgModule and remove this. +angular + .module(quay.name) .constant('NAME_PATTERNS', NAME_PATTERNS) .constant('INJECTED_CONFIG', INJECTED_CONFIG) .constant('INJECTED_FEATURES', INJECTED_FEATURES) - .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS) - .service('ViewArray', ViewArrayImpl) - .run(quayRun) - .name; + .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS); diff --git a/static/js/quay.routes.ts b/static/js/quay.routes.ts deleted file mode 100644 index 1714c51ba..000000000 --- a/static/js/quay.routes.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { RouteBuilderImpl } from './services/route-builder/route-builder.service.impl'; -import { RouteBuilder } from './services/route-builder/route-builder.service'; -import pages from './constants/pages.constant'; -import * as ng from '@types/angular'; - - -routeConfig.$inject = [ - 'pages', - '$routeProvider', - '$locationProvider', - 'INJECTED_FEATURES', -]; - -export function routeConfig( - pages: any, - $routeProvider: ng.route.IRouteProvider, - $locationProvider: ng.ILocationProvider, - INJECTED_FEATURES) { - $locationProvider.html5Mode(true); - - // WARNING WARNING WARNING - // If you add a route here, you must add a corresponding route in thr endpoints/web.py - // index rule to make sure that deep links directly deep into the app continue to work. - // WARNING WARNING WARNING - - var routeBuilder: RouteBuilder = new RouteBuilderImpl($routeProvider, pages); - - if (INJECTED_FEATURES.SUPER_USERS) { - // QE Management - routeBuilder.route('/superuser/', 'superuser') - // QE Setup - .route('/setup/', 'setup'); - } - - routeBuilder - // Repository View - .route('/repository/:namespace/:name', 'repo-view') - .route('/repository/:namespace/:name/tag/:tag', 'repo-view') - - // Image View - .route('/repository/:namespace/:name/image/:image', 'image-view') - - // Repo Build View - .route('/repository/:namespace/:name/build/:buildid', 'build-view') - - // Repo Trigger View - .route('/repository/:namespace/:name/trigger/:triggerid', 'trigger-setup') - - // Create repository notification - .route('/repository/:namespace/:name/create-notification', 'create-repository-notification') - - // Repo List - .route('/repository/', 'repo-list') - - // Organizations - .route('/organizations/', 'organizations') - - // New Organization - .route('/organizations/new/', 'new-organization') - - // View Organization - .route('/organization/:orgname', 'org-view') - - // View Organization Team - .route('/organization/:orgname/teams/:teamname', 'team-view') - - // Organization View Application - .route('/organization/:orgname/application/:clientid', 'manage-application') - - // View Organization Billing - .route('/organization/:orgname/billing', 'billing') - - // View Organization Billing Invoices - .route('/organization/:orgname/billing/invoices', 'invoices') - - // View User - .route('/user/:username', 'user-view') - - // View User Billing - .route('/user/:username/billing', 'billing') - - // View User Billing Invoices - .route('/user/:username/billing/invoices', 'invoices') - - // Sign In - .route('/signin/', 'signin') - - // New Repository - .route('/new/', 'new-repo') - - // Plans - .route('/plans/', 'plans') - - // Tutorial - .route('/tutorial/', 'tutorial') - - // Contact - .route('/contact/', 'contact') - - // About - .route('/about/', 'about') - - // Security - .route('/security/', 'security') - - // TOS - .route('/tos', 'tos') - - // Privacy - .route('/privacy', 'privacy') - - // Change username - .route('/updateuser', 'update-user') - - // Landing Page - .route('/', 'landing') - - // Tour - .route('/tour/', 'tour') - .route('/tour/features', 'tour') - .route('/tour/organizations', 'tour') - .route('/tour/enterprise', 'tour') - - // Confirm Invite - .route('/confirminvite', 'confirm-invite') - - // Enterprise marketing page - .route('/enterprise', 'enterprise') - - // Public Repo Experiments - .route('/__exp/publicRepo', 'public-repo-exp') - - // 404/403 - .route('/:catchall', 'error-view') - .route('/:catch/:all', 'error-view') - .route('/:catch/:all/:things', 'error-view') - .route('/:catch/:all/:things/:here', 'error-view'); -} diff --git a/static/js/quay.run.ts b/static/js/quay.run.ts deleted file mode 100644 index 9ebcfd262..000000000 --- a/static/js/quay.run.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as $ from 'jquery'; -import * as ng from '@types/angular'; - - -quayRun.$inject = [ - '$location', - '$rootScope', - 'Restangular', - 'UserService', - 'PlanService', - '$http', - '$timeout', - 'CookieService', - 'Features', - '$anchorScroll', - 'UtilService', - 'MetaService', - 'INJECTED_CONFIG', -]; - -export default function quayRun( - $location: ng.ILocationService, - $rootScope: QuayRunScope, - Restangular: any, - UserService: any, - PlanService: any, - $http: ng.IHttpService, - $timeout: ng.ITimeoutService, - CookieService: any, - Features: any, - $anchorScroll: ng.IAnchorScrollService, - UtilService: any, - MetaService: any, - INJECTED_CONFIG: any) { - var defaultTitle = INJECTED_CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; - - // Handle session security. - Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], - {'_csrf_token': (window).__token || ''}); - - // Handle session expiration. - Restangular.setErrorInterceptor(function(response) { - if (response !== undefined && response.status == 503) { - ($('#cannotContactService')).modal({}); - return false; - } - - if (response !== undefined && response.status == 500) { - window.location.href = '/500'; - return false; - } - - if (response !== undefined && !response.data) { - return true; - } - - var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; - if (response !== undefined && response.status == 401 && - invalid_token && - response.data['session_required'] !== false) { - ($('#sessionexpiredModal')).modal({}); - return false; - } - - return true; - }); - - // Check if we need to redirect based on a previously chosen plan. - var result = PlanService.handleNotedPlan(); - - // Check to see if we need to show a redirection page. - var redirectUrl = CookieService.get('quay.redirectAfterLoad'); - CookieService.clear('quay.redirectAfterLoad'); - - if (!result && redirectUrl && redirectUrl.indexOf((window).location.href) == 0) { - (window).location = redirectUrl; - return; - } - - $rootScope.$watch('description', function(description: string) { - if (!description) { - description = `Hosted private docker repositories. Includes full user management and history. - Free for public repositories.`; - } - - // Note: We set the content of the description tag manually here rather than using Angular binding - // because we need the tag to have a default description that is not of the form "{{ description }}", - // we read by tools that do not properly invoke the Angular code. - $('#descriptionTag').attr('content', description); - }); - - // Listen for scope changes and update the title and description accordingly. - $rootScope.$watch(function() { - var title = MetaService.getTitle($rootScope.currentPage) || defaultTitle; - $rootScope.title = title; - - var description = MetaService.getDescription($rootScope.currentPage) || ''; - if ($rootScope.description != description) { - $rootScope.description = description; - } - }); - - $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { - $rootScope.current = current.$$route; - $rootScope.currentPage = current; - - $rootScope.pageClass = ''; - - if (!current.$$route) { return; } - - var pageClass = current.$$route.pageClass || ''; - if (typeof pageClass != 'string') { - pageClass = pageClass(Features); - } - - - $rootScope.pageClass = pageClass; - $rootScope.newLayout = !!current.$$route.newLayout; - $rootScope.fixFooter = !!current.$$route.fixFooter; - - $anchorScroll(); - }); - - var initallyChecked = false; - (window).__isLoading = function() { - if (!initallyChecked) { - initallyChecked = true; - return true; - } - return $http.pendingRequests.length > 0; - }; -} - - -interface QuayRunScope extends ng.IRootScopeService { - currentPage: any; - current: any; - title: any; - description: string, - pageClass: any; - newLayout: any; - fixFooter: any; -} \ No newline at end of file diff --git a/static/js/services/page/page.service.impl.spec.ts b/static/js/services/page/page.service.impl.spec.ts new file mode 100644 index 000000000..61f9e51f1 --- /dev/null +++ b/static/js/services/page/page.service.impl.spec.ts @@ -0,0 +1,22 @@ +import { PageServiceImpl } from './page.service.impl'; + + +describe("Service: PageServiceImpl", () => { + var pageServiceImpl: PageServiceImpl; + + beforeEach(() => { + pageServiceImpl = new PageServiceImpl(); + }); + + describe("create", () => { + + }); + + describe("get", () => { + + }); + + describe("$get", () => { + + }); +}); \ No newline at end of file diff --git a/static/js/services/page/page.service.impl.ts b/static/js/services/page/page.service.impl.ts new file mode 100644 index 000000000..d52818899 --- /dev/null +++ b/static/js/services/page/page.service.impl.ts @@ -0,0 +1,45 @@ +import { Injectable } from 'angular-ts-decorators'; +import { PageService } from './page.service'; + + +@Injectable(PageService.name) +export class PageServiceImpl implements ng.IServiceProvider { + + private pages: any = {}; + + constructor() { + + } + + public create(pageName: string, + templateName: string, + controller?: any, + flags: any = {}, + profiles: string[] = ['old-layout', 'layout']): void { + for (var i = 0; i < profiles.length; ++i) { + this.pages[profiles[i] + ':' + pageName] = { + 'name': pageName, + 'controller': controller, + 'templateName': templateName, + 'flags': flags + }; + } + } + + public get(pageName: string, profiles: any[]): any[] | null { + for (var i = 0; i < profiles.length; ++i) { + var current = profiles[i]; + var key = current.id + ':' + pageName; + var page = this.pages[key]; + if (page) { + return [current, page]; + } + } + + return null; + } + + public $get(): PageService { + return this; + } +} diff --git a/static/js/services/page/page.service.ts b/static/js/services/page/page.service.ts new file mode 100644 index 000000000..829959d2a --- /dev/null +++ b/static/js/services/page/page.service.ts @@ -0,0 +1,32 @@ +/** + * Manages the creation and retrieval of pages (route + controller) + */ +export abstract class PageService implements ng.IServiceProvider { + + /** + * Create a page. + * @param pageName The name of the page. + * @param templateName The file name of the template. + * @param controller Controller for the page. + * @param flags Additional flags passed to route provider. + * @param profiles Available profiles. + */ + public abstract create(pageName: string, + templateName: string, + controller?: any, + flags?: any, + profiles?: string[]): void; + + /** + * Retrieve a registered page. + * @param pageName The name of the page. + * @param profiles Available profiles to search. + */ + public abstract get(pageName: string, profiles: any[]): any[] | null; + + /** + * Provide the service instance. + * @return pageService The singleton service instance. + */ + public abstract $get(): PageService; +} diff --git a/static/js/services/view-array/view-array.impl.ts b/static/js/services/view-array/view-array.impl.ts index 8cc3f75c4..939d50e5c 100644 --- a/static/js/services/view-array/view-array.impl.ts +++ b/static/js/services/view-array/view-array.impl.ts @@ -1,7 +1,9 @@ import { ViewArray } from './view-array'; import { Inject } from '../../decorators/inject/inject.decorator'; +import { Injectable } from 'angular-ts-decorators'; +@Injectable(ViewArray.name) export class ViewArrayImpl implements ViewArray { public entries: any[]; From 389a4cb1c4c855acf7eef62b44a300e05bc5af49 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Fri, 17 Feb 2017 13:40:05 -0800 Subject: [PATCH 03/29] fixed tests --- .../regex-match-view.component.spec.ts | 30 ++++++++++++++++++ .../regex-match-view.component.ts | 6 ++-- test/data/test.db | Bin 1306624 -> 1306624 bytes 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts b/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts index 86e18b99c..a4a5c0aaf 100644 --- a/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts @@ -9,6 +9,36 @@ describe("RegexMatchViewComponent", () => { }); describe("filterMatches", () => { + var items: ({value: string})[]; + beforeEach(() => { + items = [{value: "master"}, {value: "develop"}, {value: "production"}]; + }); + + it("returns null if given invalid regex expression", () => { + var regexstr: string = "\\asfd\\"; + + expect(component.filterMatches(regexstr, items, true)).toBe(null); + }); + + it("returns a subset of given items matching the given regex expression if given 'shouldMatch' as true", () => { + var regexstr: string = `^${items[0].value}$`; + var matches: ({value: string})[] = component.filterMatches(regexstr, items, true); + + expect(matches.length).toBeGreaterThan(0); + matches.forEach((match) => { + expect(items).toContain(match); + }); + }); + + it("returns a subset of given items not matching the given regex expression if given 'shouldMatch' as false", () => { + var regexstr: string = `^${items[0].value}$`; + var nonMatches: ({value: string})[] = component.filterMatches(regexstr, items, false); + + expect(nonMatches.length).toBeGreaterThan(0); + nonMatches.forEach((nonMatch) => { + expect(items).toContain(nonMatch); + }); + }); }); }); diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.ts b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts index 2c987906c..946771a95 100644 --- a/static/js/directives/ui/regex-match-view/regex-match-view.component.ts +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts @@ -48,7 +48,7 @@ export class RegexMatchViewComponent implements ng.IComponentController { } - public filterMatches(regexstr: string, items: any[], shouldMatch: boolean): any[] { + public filterMatches(regexstr: string, items: ({value: string})[], shouldMatch: boolean): ({value: string})[] | null { regexstr = regexstr || '.+'; try { @@ -58,8 +58,8 @@ export class RegexMatchViewComponent implements ng.IComponentController { } return items.filter(function(item) { - var value = item['value']; - var m = value.match(regex); + var value: string = item.value; + var m: RegExpMatchArray = value.match(regex); var matches: boolean = !!(m && m[0].length == value.length); return matches == shouldMatch; }); diff --git a/test/data/test.db b/test/data/test.db index 5d0c6c4c78967925ead613b565730fe9727aff1e..9c7a22b34ab4c1da80f7bd8882a9f159c5241908 100644 GIT binary patch delta 320 zcmZ9_u};H43y7*QtDB2sb`MPh3y#>S5G$luGr-@94Rp_zYruRc%tc9sd8zvZX@|If|7ovnfGAOCWe1RjCy rEFYME@iQ@SZo9z#pTGToKMxS|0x=&DgZKhKEC|Fx+yD0qGbR84FJCz3 From 38e40665a761b6a8110ecc91d564d1f08a1d31eb Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Fri, 17 Feb 2017 15:46:43 -0800 Subject: [PATCH 04/29] refactored DockerfilePathSelectComponent --- static/directives/dockerfile-path-select.html | 32 -------- static/directives/manage-trigger-githost.html | 9 +- .../directives/ui/dockerfile-path-select.js | 54 ------------ .../dockerfile-path-select.component.spec.ts | 59 +++++++++++++ .../dockerfile-path-select.component.ts | 82 +++++++++++++++++++ .../regex-match-view.component.spec.ts | 2 +- .../regex-match-view.component.ts | 1 + static/js/quay.module.ts | 2 + 8 files changed, 151 insertions(+), 90 deletions(-) delete mode 100644 static/directives/dockerfile-path-select.html delete mode 100644 static/js/directives/ui/dockerfile-path-select.js create mode 100644 static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts create mode 100644 static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts diff --git a/static/directives/dockerfile-path-select.html b/static/directives/dockerfile-path-select.html deleted file mode 100644 index 699336bb7..000000000 --- a/static/directives/dockerfile-path-select.html +++ /dev/null @@ -1,32 +0,0 @@ -
- - -
-
- Path entered for folder containing Dockerfile is invalid: Must start with a '/'. -
-
-
\ No newline at end of file diff --git a/static/directives/manage-trigger-githost.html b/static/directives/manage-trigger-githost.html index 4645a2f2f..6746085b4 100644 --- a/static/directives/manage-trigger-githost.html +++ b/static/directives/manage-trigger-githost.html @@ -220,11 +220,14 @@

Select Dockerfile

- Please select the location of the Dockerfile to build when this trigger is invoked + Please select the location of the Dockerfile to build when this trigger is invoked {{ local.paths }} -
+
diff --git a/static/js/directives/ui/dockerfile-path-select.js b/static/js/directives/ui/dockerfile-path-select.js deleted file mode 100644 index b968e20f8..000000000 --- a/static/js/directives/ui/dockerfile-path-select.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * An element which displays a list of selectable paths containing Dockerfiles. - */ -angular.module('quay').directive('dockerfilePathSelect', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/dockerfile-path-select.html', - replace: false, - transclude: true, - restrict: 'C', - scope: { - 'currentPath': '=currentPath', - 'isValidPath': '=?isValidPath', - 'paths': '=paths', - 'supportsFullListing': '=supportsFullListing' - }, - controller: function($scope, $element) { - $scope.isUnknownPath = true; - $scope.selectedPath = null; - - var checkPath = function() { - $scope.isUnknownPath = false; - $scope.isValidPath = false; - - var path = $scope.currentPath || ''; - if (path.length == 0 || path[0] != '/') { - return; - } - - $scope.isValidPath = true; - - if (!$scope.paths) { - return; - } - - $scope.isUnknownPath = $scope.supportsFullListing && $scope.paths.indexOf(path) < 0; - }; - - $scope.setPath = function(path) { - $scope.currentPath = path; - $scope.selectedPath = null; - }; - - $scope.setSelectedPath = function(path) { - $scope.currentPath = path; - $scope.selectedPath = path; - }; - - $scope.$watch('currentPath', checkPath); - $scope.$watch('paths', checkPath); - } - }; - return directiveDefinitionObject; -}); \ No newline at end of file diff --git a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts new file mode 100644 index 000000000..487f99de8 --- /dev/null +++ b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts @@ -0,0 +1,59 @@ +import { DockerfilePathSelectComponent } from './dockerfile-path-select.component'; + + +describe("DockerfilePathSelectComponent", () => { + var component: DockerfilePathSelectComponent; + var currentPath: string; + var isValidPath: boolean; + var paths: string[]; + var supportsFullListing: boolean; + + beforeEach(() => { + component = new DockerfilePathSelectComponent(); + }); + + describe("$onChanges", () => { + + it("sets valid path flag to true if current path is valid", () => { + + }); + + it("sets valid path flag to false if current path is invalid", () => { + + }); + }); + + describe("setPath", () => { + + it("sets current path to given path", () => { + + }); + + it("sets selected path to null", () => { + + }); + + it("sets valid path flag to true if given path is valid", () => { + + }); + + it("sets valid path flag to false if given path is invalid", () => { + + }); + }); + + describe("setCurrentPath", () => { + + it("sets current path and selected path to given path", () => { + + }); + + it("sets valid path flag to true if given path is valid", () => { + + }); + + it("sets valid path flag to false if given path is invalid", () => { + + }); + }); +}); \ No newline at end of file diff --git a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts new file mode 100644 index 000000000..ab3c05756 --- /dev/null +++ b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts @@ -0,0 +1,82 @@ +import { Input, Component } from 'angular-ts-decorators'; + + +@Component({ + selector: 'dockerfilePathSelect', + template: ` +
+ + +
+
+ Path entered for folder containing Dockerfile is invalid: Must start with a '/'. +
+
+
+ `, +}) +export class DockerfilePathSelectComponent implements ng.IComponentController { + + // FIXME: Use one-way data binding + @Input('=') public currentPath: string; + @Input('=') public isValidPath: boolean; + @Input('=') public paths: string[]; + @Input('=') public supportsFullListing: boolean; + private isUnknownPath: boolean = true; + private selectedPath: string | null = null; + + public $onChanges(changes: ng.IOnChangesObject): void { + this.isValidPath = this.checkPath(this.currentPath, this.paths, this.supportsFullListing); + } + + public setPath(path: string): void { + this.currentPath = path; + this.selectedPath = null; + this.isValidPath = this.checkPath(path, this.paths, this.supportsFullListing); + } + + public setSelectedPath(path: string): void { + this.currentPath = path; + this.selectedPath = path; + this.isValidPath = this.checkPath(path, this.paths, this.supportsFullListing); + } + + private checkPath(path: string = '', paths: string[] = [], supportsFullListing: boolean): boolean { + this.isUnknownPath = false; + var isValidPath: boolean = false; + + if (path.length > 0 && path[0] === '/') { + isValidPath = true; + this.isUnknownPath = supportsFullListing && paths.indexOf(path) < 0; + } + return isValidPath; + } +} diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts b/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts index a4a5c0aaf..6bc05a768 100644 --- a/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.spec.ts @@ -12,7 +12,7 @@ describe("RegexMatchViewComponent", () => { var items: ({value: string})[]; beforeEach(() => { - items = [{value: "master"}, {value: "develop"}, {value: "production"}]; + items = [{value: "heads/master"}, {value: "heads/develop"}, {value: "heads/production"}]; }); it("returns null if given invalid regex expression", () => { diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.ts b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts index 946771a95..45995908d 100644 --- a/static/js/directives/ui/regex-match-view/regex-match-view.component.ts +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts @@ -41,6 +41,7 @@ import { Input, Component } from 'angular-ts-decorators'; }) export class RegexMatchViewComponent implements ng.IComponentController { + // FIXME: Use one-way data binding @Input('=') private regex: string; @Input('=') private items: any[]; diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 6e37c162f..76ac3f207 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -6,6 +6,7 @@ import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./consta import { RegexMatchViewComponent } from "./directives/ui/regex-match-view/regex-match-view.component"; import { NgModule } from "angular-ts-decorators"; import { QuayRoutes } from "./quay-routes.module"; +import { DockerfilePathSelectComponent } from './directives/ui/dockerfile-path-select/dockerfile-path-select.component'; var quayDependencies: any[] = [ @@ -53,6 +54,7 @@ if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { imports: quayDependencies, declarations: [ RegexMatchViewComponent, + DockerfilePathSelectComponent, ], providers: [ ViewArrayImpl, From 5ef4357dec4f7e56165fc40a09ca4c691d44d6eb Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Fri, 17 Feb 2017 15:58:52 -0800 Subject: [PATCH 05/29] unit tests for DockerfilePathSelectComponent --- .../dockerfile-path-select.component.spec.ts | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts index 487f99de8..431252faa 100644 --- a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts +++ b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.spec.ts @@ -10,50 +10,81 @@ describe("DockerfilePathSelectComponent", () => { beforeEach(() => { component = new DockerfilePathSelectComponent(); + currentPath = '/'; + isValidPath = false; + paths = ['/']; + supportsFullListing = true; + component.currentPath = currentPath; + component.isValidPath = isValidPath; + component.paths = paths; + component.supportsFullListing = supportsFullListing; }); describe("$onChanges", () => { it("sets valid path flag to true if current path is valid", () => { + component.$onChanges({}); + expect(component.isValidPath).toBe(true); }); it("sets valid path flag to false if current path is invalid", () => { + component.currentPath = "asdfdsf"; + component.$onChanges({}); + expect(component.isValidPath).toBe(false); }); }); describe("setPath", () => { + var newPath: string; - it("sets current path to given path", () => { - + beforeEach(() => { + newPath = '/conf'; }); - it("sets selected path to null", () => { + it("sets current path to given path", () => { + component.setPath(newPath); + expect(component.currentPath).toEqual(newPath); }); it("sets valid path flag to true if given path is valid", () => { + component.setPath(newPath); + expect(component.isValidPath).toBe(true); }); it("sets valid path flag to false if given path is invalid", () => { + component.setPath("asdfsadfs"); + expect(component.isValidPath).toBe(false); }); }); describe("setCurrentPath", () => { + var newPath: string; - it("sets current path and selected path to given path", () => { + beforeEach(() => { + newPath = '/conf'; + }); + it("sets current path to given path", () => { + component.setSelectedPath(newPath); + + expect(component.currentPath).toEqual(newPath); }); it("sets valid path flag to true if given path is valid", () => { + component.setSelectedPath(newPath); + expect(component.isValidPath).toBe(true); }); it("sets valid path flag to false if given path is invalid", () => { + component.setSelectedPath("a;lskjdf;ldsa"); + expect(component.isValidPath).toBe(false); }); }); }); \ No newline at end of file From 00b1f0e3ccff190b7711e6ec3ab8738bee0d075a Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Fri, 17 Feb 2017 17:08:33 -0800 Subject: [PATCH 06/29] starting ManageTriggerCustomGitComponent --- ...anage-trigger-custom-git.component.spec.ts | 0 .../manage-trigger-custom-git.component.ts | 69 ++++++++++++++++++ static/js/quay.module.ts | 2 + static/partials/trigger-setup.html | 5 +- test/data/test.db | Bin 1306624 -> 1306624 bytes 5 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.spec.ts create mode 100644 static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts diff --git a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.spec.ts b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts new file mode 100644 index 000000000..3f4f55b83 --- /dev/null +++ b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts @@ -0,0 +1,69 @@ +import { Input, Output, Component } from 'angular-ts-decorators'; + + +@Component({ + selector: 'manageTriggerCustomGit', + template: ` +
+
+ +
+ +
+

Enter repository

+ + Please enter the HTTP or SSH style URL used to clone your git repository: + + +
+ +
+ + +
+ +
+

Select build context directory

+ Please select the build context directory under the git repository: + +
+ + +
+
+ ` +}) +export class ManageTriggerCustomGitComponent implements ng.IComponentController { + + // FIXME: Use one-way data binding + @Input('=') public trigger: {config: any}; + @Output() public activateTrigger: any; + private config: any = {}; + private currentState: any | null; + private gitUrlRegEx: string = "((ssh|http(s)?)|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)?"; + + public $onChanges(changes: ng.IOnChangesObject): void { + if (changes['trigger'] !== undefined) { + this.config = Object.assign({}, changes['trigger'].currentValue.config); + } + } +} diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 76ac3f207..229c16a09 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -7,6 +7,7 @@ import { RegexMatchViewComponent } from "./directives/ui/regex-match-view/regex- import { NgModule } from "angular-ts-decorators"; import { QuayRoutes } from "./quay-routes.module"; import { DockerfilePathSelectComponent } from './directives/ui/dockerfile-path-select/dockerfile-path-select.component'; +import { ManageTriggerCustomGitComponent } from './directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component'; var quayDependencies: any[] = [ @@ -55,6 +56,7 @@ if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { declarations: [ RegexMatchViewComponent, DockerfilePathSelectComponent, + ManageTriggerCustomGitComponent, ], providers: [ ViewArrayImpl, diff --git a/static/partials/trigger-setup.html b/static/partials/trigger-setup.html index 6ccb87bda..79f676508 100644 --- a/static/partials/trigger-setup.html +++ b/static/partials/trigger-setup.html @@ -44,8 +44,9 @@
-
+
diff --git a/test/data/test.db b/test/data/test.db index 9c7a22b34ab4c1da80f7bd8882a9f159c5241908..5b1af4074de1911cda2ad7635c99851d7c652caf 100644 GIT binary patch delta 196 zcmWN=u@1pd6adis-lLRqm6603G*nx3lUOC4B*q)mY_~An7(~KkHTeONEr-19w%3OFhb9h zMHmZV0@wh7JDp9vc;<9ir%G0i8trBCYK3J7fDbp+f lfW$B07k`QU$i49NZmEor+cdKDl3vzPujsa3P12}lod56bJC*16S!Iv zxVI+oER$z5WZ>`RH{2}fpvoT=#l+1J%E`zX%OJ?WV4j?oVrgQUtea?_W~ggoY+|Bo znVMpzo0OCYK}5DNma(Dq;c!fXowux&x| From 39c18eb21628340877bdd5b2ed2064f94fe3f950 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Fri, 17 Feb 2017 21:35:33 -0800 Subject: [PATCH 07/29] moved component templates to separate files in order to support HTML syntax highlighting in certain editors --- grunt/Gruntfile.js | 5 +- .../dockerfile-path-select.component.html | 37 +++++++++++++ .../dockerfile-path-select.component.ts | 43 ++------------- .../ui/manage-trigger-custom-git.js | 27 ---------- .../manage-trigger-custom-git.component.html} | 24 +++++---- .../manage-trigger-custom-git.component.ts | 53 ++----------------- .../regex-match-view.component.html | 29 ++++++++++ .../regex-match-view.component.ts | 34 +----------- 8 files changed, 93 insertions(+), 159 deletions(-) create mode 100644 static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.html delete mode 100644 static/js/directives/ui/manage-trigger-custom-git.js rename static/{directives/manage-trigger-custom-git.html => js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html} (71%) create mode 100644 static/js/directives/ui/regex-match-view/regex-match-view.component.html diff --git a/grunt/Gruntfile.js b/grunt/Gruntfile.js index 29ead6454..f0f8c35f2 100644 --- a/grunt/Gruntfile.js +++ b/grunt/Gruntfile.js @@ -70,8 +70,9 @@ module.exports = function(grunt) { } }, quay: { - src: ['../static/partials/*.html', '../static/directives/*.html', '../static/directives/*.html' - , '../static/directives/config/*.html', '../static/tutorial/*.html'], + src: ['../static/partials/*.html', '../static/directives/*.html', '../static/directives/*.html', + '../static/directives/config/*.html', '../static/tutorial/*.html', + '../static/js/directives/ui/**/*.html'], dest: '../static/dist/template-cache.js' } }, diff --git a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.html b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.html new file mode 100644 index 000000000..ef20c83c7 --- /dev/null +++ b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.html @@ -0,0 +1,37 @@ +
+ + +
+
+ Path entered for folder containing Dockerfile is invalid: Must start with a '/'. +
+
+
diff --git a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts index ab3c05756..be0109ef4 100644 --- a/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts +++ b/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.ts @@ -1,47 +1,12 @@ import { Input, Component } from 'angular-ts-decorators'; +/** + * A component that allows the user to select the location of the Dockerfile in their source code repository. + */ @Component({ selector: 'dockerfilePathSelect', - template: ` -
- - -
-
- Path entered for folder containing Dockerfile is invalid: Must start with a '/'. -
-
-
- `, + templateUrl: '/static/js/directives/ui/dockerfile-path-select/dockerfile-path-select.component.html' }) export class DockerfilePathSelectComponent implements ng.IComponentController { diff --git a/static/js/directives/ui/manage-trigger-custom-git.js b/static/js/directives/ui/manage-trigger-custom-git.js deleted file mode 100644 index e039e11c0..000000000 --- a/static/js/directives/ui/manage-trigger-custom-git.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * An element which displays the setup and management workflow for a custom git trigger. - */ -angular.module('quay').directive('manageTriggerCustomGit', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/manage-trigger-custom-git.html', - replace: false, - transclude: true, - restrict: 'C', - scope: { - 'trigger': '=trigger', - 'activateTrigger': '&activateTrigger' - }, - controller: function($scope, $element) { - $scope.config = {}; - $scope.currentState = null; - - $scope.$watch('trigger', function(trigger) { - if (trigger) { - $scope.config = trigger['config'] || {}; - } - }); - } - }; - return directiveDefinitionObject; -}); \ No newline at end of file diff --git a/static/directives/manage-trigger-custom-git.html b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html similarity index 71% rename from static/directives/manage-trigger-custom-git.html rename to static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html index 96fb0819a..563ac04f9 100644 --- a/static/directives/manage-trigger-custom-git.html +++ b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html @@ -1,11 +1,13 @@
-
+
+ section-valid="$ctrl.config.build_source">

Enter repository

@@ -13,7 +15,8 @@ Please enter the HTTP or SSH style URL used to clone your git repository: + ng-model="$ctrl.config.build_source" + ng-pattern="/(((http|https):\/\/)(.+)|\w+@(.+):(.+))/">
\ No newline at end of file +
+
diff --git a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts index 3f4f55b83..4addf1a24 100644 --- a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts +++ b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts @@ -1,56 +1,12 @@ import { Input, Output, Component } from 'angular-ts-decorators'; +/** + * A component that lets the user set up a build trigger for a custom Git repository. + */ @Component({ selector: 'manageTriggerCustomGit', - template: ` -
-
- -
- -
-

Enter repository

- - Please enter the HTTP or SSH style URL used to clone your git repository: - - -
- -
- - -
- -
-

Select build context directory

- Please select the build context directory under the git repository: - -
- - -
-
- ` + templateUrl: '/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html' }) export class ManageTriggerCustomGitComponent implements ng.IComponentController { @@ -59,7 +15,6 @@ export class ManageTriggerCustomGitComponent implements ng.IComponentController @Output() public activateTrigger: any; private config: any = {}; private currentState: any | null; - private gitUrlRegEx: string = "((ssh|http(s)?)|(git@[\w\.]+))(:(//)?)([\w\.@\:/\-~]+)(\.git)(/)?"; public $onChanges(changes: ng.IOnChangesObject): void { if (changes['trigger'] !== undefined) { diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.html b/static/js/directives/ui/regex-match-view/regex-match-view.component.html new file mode 100644 index 000000000..bdfaa597a --- /dev/null +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.html @@ -0,0 +1,29 @@ +
+
+ Invalid Regular Expression! +
+
+ + + + + + + + + +
Matching: +
    +
  • + {{ item.title }} +
  • +
+
Not Matching: +
    +
  • + {{ item.title }} +
  • +
+
+
+
diff --git a/static/js/directives/ui/regex-match-view/regex-match-view.component.ts b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts index 45995908d..256a2d99e 100644 --- a/static/js/directives/ui/regex-match-view/regex-match-view.component.ts +++ b/static/js/directives/ui/regex-match-view/regex-match-view.component.ts @@ -2,42 +2,12 @@ import { Input, Component } from 'angular-ts-decorators'; /** - * An element which displays the matches and non-matches for a regular expression against a set of + * A component that displays the matches and non-matches for a regular expression against a set of * items. */ @Component({ selector: 'regexMatchView', - template: ` -
-
- Invalid Regular Expression! -
-
- - - - - - - - - -
Matching: -
    -
  • - {{ item.title }} -
  • -
-
Not Matching: -
    -
  • - {{ item.title }} -
  • -
-
-
-
- ` + templateUrl: '/static/js/directives/ui/regex-match-view/regex-match-view.component.html' }) export class RegexMatchViewComponent implements ng.IComponentController { From 14222be9feea549ff8063e4a9d23473f6d4785fe Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sat, 18 Feb 2017 01:45:00 -0800 Subject: [PATCH 08/29] working on ManageTriggerGithostComponent --- .../manage-trigger-custom-git.component.ts | 2 +- .../directives/ui/manage-trigger-githost.js | 2 +- .../manage-trigger-githost.component.html | 355 ++++++++++++++++++ .../manage-trigger-githost.component.spec.ts | 47 +++ .../manage-trigger-githost.component.ts | 148 ++++++++ static/js/quay.module.ts | 2 + test/data/test.db | Bin 1306624 -> 1314816 bytes 7 files changed, 554 insertions(+), 2 deletions(-) create mode 100644 static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html create mode 100644 static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.spec.ts create mode 100644 static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts diff --git a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts index 4addf1a24..c4e88f7cf 100644 --- a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts +++ b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.ts @@ -12,7 +12,7 @@ export class ManageTriggerCustomGitComponent implements ng.IComponentController // FIXME: Use one-way data binding @Input('=') public trigger: {config: any}; - @Output() public activateTrigger: any; + @Output() public activateTrigger: (trigger: {config: any}) => void; private config: any = {}; private currentState: any | null; diff --git a/static/js/directives/ui/manage-trigger-githost.js b/static/js/directives/ui/manage-trigger-githost.js index c63c86199..3a4a24f4b 100644 --- a/static/js/directives/ui/manage-trigger-githost.js +++ b/static/js/directives/ui/manage-trigger-githost.js @@ -11,7 +11,6 @@ angular.module('quay').directive('manageTriggerGithost', function () { scope: { 'repository': '=repository', 'trigger': '=trigger', - 'activateTrigger': '&activateTrigger' }, controller: function($scope, $element, ApiService, TableService, TriggerService, RolesService) { @@ -52,6 +51,7 @@ angular.module('quay').directive('manageTriggerGithost', function () { }; $scope.createTrigger = function() { + console.log($scope.local); var config = { 'build_source': $scope.local.selectedRepository.full_name, 'subdir': $scope.local.dockerfilePath.substr(1) // Remove starting / diff --git a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html new file mode 100644 index 000000000..ad35aa864 --- /dev/null +++ b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html @@ -0,0 +1,355 @@ +
+
+ + +
+
+

Select {{ $ctrl.namespaceTitle }}

+ + Please select the {{ $ctrl.namespaceTitle }} under which the repository lives + + +
+
+ + +
+
+ + + + + + + + + + + + + +
+ {{ $ctrl.namespaceTitle }} +
+ + + + {{ $ctrl.namespace.id }} +
+
+
No matching {{ $ctrl.namespaceTitle }} found.
+
Try expanding your filtering terms.
+
+
+ +
+ Retrieving {{ $ctrl.namespaceTitle }}s +
+ +
+ + +
+ +
+

Select Repository

+ + Select a repository in + + {{ $ctrl.local.selectedNamespace.id }} + + +
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + +
+ Repository Name + + Last Updated +
+ + + + + + + {{ repository.name }} + + +
+
+
No matching repositories found.
+
Try expanding your filtering terms.
+
+
+ +
+ Retrieving repositories +
+ + +
+ + +
+
+

Configure Trigger

+ + Configure trigger options for + + {{ $ctrl.local.selectedNamespace.id }}/{{ $ctrl.local.selectedRepository.name }} + + +
+ +
+
+ +
+
+ +
+ Retrieving repository refs +
+ +
+ + +
+
+
+ {{ $ctrl.local.dockerfileLocations.message }} +
+
+ +
+

Select Dockerfile

+ + Please select the location of the Dockerfile to build when this trigger is invoked {{ $ctrl.local.paths }} + + + +
+ +
+ Retrieving Dockerfile locations +
+ +
+ + +
+ +
+

Verification Error

+ + There was an error when verifying the state of + {{ $ctrl.local.selectedNamespace.id }}/{{ $ctrl.local.selectedRepository.name }} + + + {{ $ctrl.local.triggerAnalysis.message }} +
+ + +
+

Verification Warning

+ {{ $ctrl.local.triggerAnalysis.message }} +
+ + +
+

Ready to go!

+ Click "Create Trigger" to complete setup of this build trigger +
+ + +
+

Robot Account Required

+

The selected Dockerfile in the selected repository depends upon a private base image

+

A robot account with access to the base image is required to setup this trigger, but you are not the administrator of this namespace.

+

Administrative access is required to continue to ensure security of the robot credentials.

+
+ + +
+

Select Robot Account

+ + The selected Dockerfile in the selected repository depends upon a private base image. Select a robot account with access: + + +
+
+ + +
+
+ + + + + + + + + + + + + +
+ Robot Account + + Has Read Access +
+ + + + + Can Read + Read access will be added if selected +
+
+
No matching robot accounts found.
+
Try expanding your filtering terms.
+
+
+ + +
+ +
\ No newline at end of file diff --git a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.spec.ts b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.spec.ts new file mode 100644 index 000000000..e7cdeacf8 --- /dev/null +++ b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.spec.ts @@ -0,0 +1,47 @@ +import { ManageTriggerGithostComponent, Local, Trigger } from './manage-trigger-githost.component'; + + +describe("ManageTriggerGithostComponent", () => { + var component: ManageTriggerGithostComponent; + var apiServiceMock: any; + var tableServiceMock: any; + var triggerServiceMock: any; + var rolesServiceMock: any; + var repository: any; + var trigger: Trigger; + + beforeEach(() => { + apiServiceMock = jasmine.createSpyObj('apiServiceMock', ['listTriggerBuildSourceNamespaces']); + tableServiceMock = jasmine.createSpyObj('tableServiceMock', ['buildOrderedItems']); + triggerServiceMock = jasmine.createSpyObj('triggerServiceMock', ['getIcon']); + rolesServiceMock = jasmine.createSpyObj('rolesServiceMock', ['setRepositoryRole']); + component = new ManageTriggerGithostComponent(apiServiceMock, tableServiceMock, triggerServiceMock, rolesServiceMock); + trigger = {service: "serviceMock", id: 1}; + component.trigger = trigger; + }); + + describe("constructor", () => { + + }); + + describe("$onInit", () => { + + }); + + describe("$onChanges", () => { + + }); + + describe("getTriggerIcon", () => { + + it("calls trigger service to get icon", () => { + component.getTriggerIcon(); + + expect(triggerServiceMock.getIcon.calls.argsFor(0)[0]).toEqual(component.trigger.service); + }); + }); + + describe("createTrigger", () => { + + }); +}); \ No newline at end of file diff --git a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts new file mode 100644 index 000000000..473bde6eb --- /dev/null +++ b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts @@ -0,0 +1,148 @@ +import { Input, Output, Component } from 'angular-ts-decorators'; + + +@Component({ + selector: 'manageTriggerGithost', + templateUrl: '/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html' +}) +export class ManageTriggerGithostComponent implements ng.IComponentController { + + @Input('=') public repository: any; + @Input('=') public trigger: Trigger; + @Output() public activateTrigger: (trigger: {config: any, pull_robot: any}) => void; + private config: any; + private local: Local = { + namespaceOptions: { + filter: '', + predicate: 'score', + reverse: false, + page: 0 + }, + repositoryOptions: { + filter: '', + predicate: 'score', + reverse: false, + page: 0, + hideStale: true + }, + robotOptions: { + filter: '', + predicate: 'score', + reverse: false, + page: 0 + } + }; + private currentState: any | null; + private namespacesPerPage: number = 10; + private repositoriesPerPage: number = 10; + private robotsPerPage: number = 10; + + constructor(private ApiService: any, + private TableService: any, + private TriggerService: any, + private RolesService: any) { + + } + + public $onInit(): void { + + } + + public $onChanges(changes: ng.IOnChangesObject): void { + + } + + public getTriggerIcon(): any { + return this.TriggerService.getIcon(this.trigger.service); + } + + public createTrigger(): void { + var config: any = { + build_source: this.local.selectedRepository.full_name, + subdir: this.local.dockerfilePath.substr(1) // Remove starting / + }; + + if (this.local.triggerOptions.hasBranchTagFilter && + this.local.triggerOptions.branchTagFilter) { + config['branchtag_regex'] = this.local.triggerOptions.branchTagFilter; + } + + var activate = () => { + this.activateTrigger({'config': config, 'pull_robot': this.local.robotAccount}); + }; + + if (this.local.robotAccount) { + if (this.local.robotAccount.can_read) { + activate(); + } else { + // Add read permission onto the base repository for the robot and then activate the + // trigger. + var robot_name = this.local.robotAccount.name; + this.RolesService.setRepositoryRole(this.repository, 'read', 'robot', robot_name, activate); + } + } else { + activate(); + } + } + + private buildOrderedNamespaces(): void { + + } + + private loadNamespaces(): void { + + } + + private buildOrderedRepositories(): void { + + } + + private loadRepositories(): void { + + } + + private loadRepositoryRefs(repository: any): void { + + } + + private loadDockerfileLocations(repository: any): void { + + } + + private buildOrderedRobotAccounts(): void { + + } + + private checkDockerfilePath(repository: any, path: string): void { + + } +} + + +export type Local = { + namespaceOptions: { + filter: string; + predicate: string; + reverse: boolean; + page: number; + }; + repositoryOptions: { + filter: string; + predicate: string; + reverse: boolean; + page: number; + hideStale: boolean; + }; + robotOptions: { + filter: string; + predicate: string; + reverse: boolean; + page: number; + }; +} + + +export type Trigger = { + id: number; + service: any; +} \ No newline at end of file diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 229c16a09..a318c4a38 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -8,6 +8,7 @@ import { NgModule } from "angular-ts-decorators"; import { QuayRoutes } from "./quay-routes.module"; import { DockerfilePathSelectComponent } from './directives/ui/dockerfile-path-select/dockerfile-path-select.component'; import { ManageTriggerCustomGitComponent } from './directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component'; +import { ManageTriggerGithostComponent } from './directives/ui/manage-trigger-githost/manage-trigger-githost.component'; var quayDependencies: any[] = [ @@ -57,6 +58,7 @@ if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { RegexMatchViewComponent, DockerfilePathSelectComponent, ManageTriggerCustomGitComponent, + ManageTriggerGithostComponent, ], providers: [ ViewArrayImpl, diff --git a/test/data/test.db b/test/data/test.db index 5b1af4074de1911cda2ad7635c99851d7c652caf..b089c3c80ea413126624be51bd6269ce9088a0e9 100644 GIT binary patch delta 4217 zcmcgvTZkjsd6ueYrl++%Q!{ZckY4ZhHp?cq(oWYCz9m^JI{ZbFdpF4%zuV?rPh;(o9n4EvNfgCG6i4S9$|9%2HXCG0{{nvNk1 z%MwV~5})Aw=l}lmpL6P~^Pk5b)E|GR@z%-p_j9>C^Y-bN^Y4H4!QVf~f9F4MU(aPG z@T2`d1AcUQ_rcA71>bI5`<;kz;O-aD!8PFJlb!n_@a=0Spr6~_fq|3jG3bNqzj)ynzj^rwrvMCIzjknx zKiS0)@b%MXC$c=y98@Z4{FB5(JPKJen@Pp7~a_O$nQhaqrY zIDYTe2e-Dj{_xi4v$4HTrGM~e8U3IB2zY5ve`k027&y(pbNR|+AU=NS^jeaA?3n>ltlnmVU?G#U)t=+)D~ zXfa$`oWi1=D%PyR98uOqw4`WNri5$Vs-d-Mh2&cmt*V!BMeVg5^1{UFpgX2IAx*dX34Y9J=dWf)Wm5McL4+?C>$niv_xQDiL@-5 zuo2k79h!z=k0wZuv|P(F&-V{2lx6j_r7@?CmfT@<;j}9Bp7sthtF%)rIj|?UFsxL04L#>F66IjzcDCNJ-0B z2%5LVYU|N*Ni2Q4qzualrj*%=VU4kR(?jNj5$RknHaHTNDc0K-I3Gg>?zAnW&CS># zn-q)DSU_W5!ft#ORxx%^lK5e38V~Am+LCHI&rDEy$kkLeZV!|Kp13uB5<}`(tB=FE zX&Vy=>koo)z2whfC#DEiqSC=48TzfwrcYQMrXTwzGMT&ma9b`_3M)SmFxRjLWs)Jw6kgtG>SJ& zVLD$&JUSpOUGj*IP?MEmRH2$O&omj939{uDM}CjBP^`IK58{vu3P={H6qXgMDQ@~g zBUJ;k)9rZKocuXi>_H=uH@XBFBGW}3(&1@ou#{v*uMPDj(@?rxA>>EFq9!wLqdQq{ z*gn1})sS{ng;}v)r(M<+=1^rZ>@5%!H8{Tyw`Y!B8g5;V>~`vUIxN%-xioiZQcG0E znyre;EEYqU>-)_W*ILh3>^kc4SUQ1LjHcmAY2IA)5!{ofvVt_c)}%09ki`Yl8*0{Q z;?=_be7+Wo3y71vk($I#n+>9<`co1LNkpx}7^h_BAzI8^$UX5*%}8IOnX2 z<7#(Mt)Rv@6vc+!l++?w3kkQ<=c}pOSPhHKK7*|+!<<8oT^mkhX)|V$!e)`^oe9hh zdIAFn=-8kcs58NAxmxb?VShAgo2(q;tw2&CB`M*Xtr)c+UMO|6npAZ)r(kv}gYlRX z`vz652lJVzGMEs^62!!9y8?LuQm2QOUz7Y!t!E4e;j}&EY_~6}oY%F6%Yq&Ts}@66 zTu*MPwgRO}jJ9GtnkX|rg*eN?L!-&HV;2=XM%dCO$<#1Mwx8xqDfXmbrt4+A>__QP zZ%uT2X$c*y1SiE>g%(*(s3_@L45XFV!B;xM_F$LmEHs;^YPRYnhA0;#DhVrS$3xp~ zttUvB7807;rPWzIAOf_)FRB*b)nOMWk*!;6q)^(Fp*lI`wGjmciEI}5UbUPM;Zh%k zvN3elx`l5POOiW+)umKjPK0H@N9}WF5voJi0%%lT*0@%A(F#hwIW1rfcWn7ny%Eu6 zVpdDL0}(g;5+UnNrz4fyONF+aMANoCp~;UMFzcAG4i9)3A{qlP!kB_vgAFxcvbVd} zpixTbb~|02HAUHIw8gqf_f~b=Vg_1e+_jQLe+8+l?huDqlxvFGjP4dpgXI=N8<_>uXt(IZ>4r9S7oUd&9y-y^w*j#4#q-v#4Q+!M?G^`z*ZDqDiMsf zNH|oVdaUD0s9=V(qJx-iMr;_{MTPCv$%$vpC}Ww@ro{_GQi4oUvFV{$NEJ9_f-=+e z^%d3(yKsGm@d#JNSiwwD8%oB$YSys67)qq02NY6kLb|gqPlaS?@s(LYoBDN3_lxy< zU+5)>E66j)T0w3H4SkjGZ>yq?&n0-8s2W-y*R@XS6w{t5|!}kYQyrw*|P@l)qGXdcFuM|B2_$!?T$gFWl(D zingY256@mbJqx42S=x(H;Gg~8>6zn((;&T=xaq~n$+}U5q)XY6mvzNU%Na#`%HDAp z1d}Tl1Vt#CqS4G~N3P@gi;+2vGrzAqJoC+&yJu*cblO;2(~*5zb<;h6o0!w()4n(! zo=35H%483VsT-3+QX3Wa&8zQrW@egvnX4$UEJ_AGVCXWv;znalq$a-HZh76VP;K=> zMQaF%w-_%li>wZbPB!6lKpvIkv< zR(gEHtNBGi^Lqog(`7Zbwo?0nW<=$+h0lm@RvF8m*7YH`wE=@=c`-6?mz0s zp98>uzam_IcySEgx%p%t<;j)t=U0Be4rGO0?&vB`*8k)x&W{g&4u0*)o5vY$Ww@PT z-@cRKZiZjGeDdb!3J=}`?iBVVVgC3hx#OQ?<={UYi+}pXlR_?kbd+aaeBiv)O^~jRsl!e`xs`DQ`b(Ek7gW?Psm!XQaISthM}%l((O?mYwtaeCk8>6BfViJ?52F1idP!O7>P!QYDnt~Xl8l*}r zMnu|5(?4t4tF`5;8q5Eq3A)K~zEf=A&5fBC!V=^KAKg`${( zr9Z3QjCYXsl$cGWi7$8FeHy<;jXzF zOov1x4~E#j1^eZvJdD^Q9|WI;u+!7_JoHR>2360ZDcUPAEWbX0Lw`$35R@Y&xMPEYn zcLhcLvyqotMwckweK#&k?IZ6YbvIwTgKJgY!Eu~m8OPP|6|~}DkTFVKg!l=uVpG#o z@!WLyTqZ^;x!qIsjuX*H*{SeR`MR z59~^RhkZlYoiy?Ps{ Date: Sun, 19 Feb 2017 18:17:12 -0800 Subject: [PATCH 09/29] converted ManageTriggerGithostComponent to TypeScript --- static/directives/manage-trigger-githost.html | 333 ------------------ .../linear-workflow-section.component.html | 0 .../linear-workflow-section.component.spec.ts | 0 .../linear-workflow-section.component.ts | 0 .../linear-workflow.component.html | 0 .../linear-workflow.component.spec.ts | 0 .../linear-workflow.component.ts | 0 .../directives/ui/manage-trigger-githost.js | 306 ---------------- .../manage-trigger-githost.component.html | 34 +- .../manage-trigger-githost.component.spec.ts | 12 +- .../manage-trigger-githost.component.ts | 212 ++++++++++- static/partials/trigger-setup.html | 6 +- test/data/test.db | Bin 1314816 -> 1314816 bytes 13 files changed, 238 insertions(+), 665 deletions(-) delete mode 100644 static/directives/manage-trigger-githost.html create mode 100644 static/js/directives/ui/linear-workflow/linear-workflow-section.component.html create mode 100644 static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts create mode 100644 static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts create mode 100644 static/js/directives/ui/linear-workflow/linear-workflow.component.html create mode 100644 static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts create mode 100644 static/js/directives/ui/linear-workflow/linear-workflow.component.ts delete mode 100644 static/js/directives/ui/manage-trigger-githost.js diff --git a/static/directives/manage-trigger-githost.html b/static/directives/manage-trigger-githost.html deleted file mode 100644 index 6746085b4..000000000 --- a/static/directives/manage-trigger-githost.html +++ /dev/null @@ -1,333 +0,0 @@ -
-
- - -
-
-

Select {{ namespaceTitle }}

- - Please select the {{ namespaceTitle }} under which the repository lives - - -
-
- - -
-
- - - - - - - - - - - - - -
- {{ namespaceTitle }} -
- - - - {{ namespace.id }} -
-
-
No matching {{ namespaceTitle }} found.
-
Try expanding your filtering terms.
-
-
- -
- Retrieving {{ namespaceTitle }}s -
- -
- - -
- -
-

Select Repository

- - Select a repository in - - {{ local.selectedNamespace.id }} - - -
-
- - -
- -
-
-
- - - - - - - - - - - - - - - -
- Repository Name - - Last Updated -
- - - - - - - {{ repository.name }} - - -
-
-
No matching repositories found.
-
Try expanding your filtering terms.
-
-
- -
- Retrieving repositories -
- - -
- - -
-
-

Configure Trigger

- - Configure trigger options for - - {{ local.selectedNamespace.id }}/{{ local.selectedRepository.name }} - - -
- -
-
- -
-
- -
- Retrieving repository refs -
- -
- - -
-
-
- {{ local.dockerfileLocations.message }} -
-
- -
-

Select Dockerfile

- - Please select the location of the Dockerfile to build when this trigger is invoked {{ local.paths }} - - - -
- -
- Retrieving Dockerfile locations -
- -
- - -
- -
-

Verification Error

- - There was an error when verifying the state of - {{ local.selectedNamespace.id }}/{{ local.selectedRepository.name }} - - - {{ local.triggerAnalysis.message }} -
- - -
-

Verification Warning

- {{ local.triggerAnalysis.message }} -
- - -
-

Ready to go!

- Click "Create Trigger" to complete setup of this build trigger -
- - -
-

Robot Account Required

-

The selected Dockerfile in the selected repository depends upon a private base image

-

A robot account with access to the base image is required to setup this trigger, but you are not the administrator of this namespace.

-

Administrative access is required to continue to ensure security of the robot credentials.

-
- - -
-

Select Robot Account

- - The selected Dockerfile in the selected repository depends upon a private base image. Select a robot account with access: - - -
-
- - -
-
- - - - - - - - - - - - - -
- Robot Account - - Has Read Access -
- - - - - Can Read - Read access will be added if selected -
-
-
No matching robot accounts found.
-
Try expanding your filtering terms.
-
-
- - -
- -
\ No newline at end of file diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts new file mode 100644 index 000000000..e69de29bb diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.html b/static/js/directives/ui/linear-workflow/linear-workflow.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts new file mode 100644 index 000000000..e69de29bb diff --git a/static/js/directives/ui/manage-trigger-githost.js b/static/js/directives/ui/manage-trigger-githost.js deleted file mode 100644 index 3a4a24f4b..000000000 --- a/static/js/directives/ui/manage-trigger-githost.js +++ /dev/null @@ -1,306 +0,0 @@ -/** - * An element which displays the setup and management workflow for a normal SCM git trigger. - */ -angular.module('quay').directive('manageTriggerGithost', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/manage-trigger-githost.html', - replace: false, - transclude: true, - restrict: 'C', - scope: { - 'repository': '=repository', - 'trigger': '=trigger', - 'activateTrigger': '&activateTrigger' - }, - controller: function($scope, $element, ApiService, TableService, TriggerService, RolesService) { - $scope.TableService = TableService; - - $scope.config = {}; - $scope.local = {}; - $scope.currentState = null; - - $scope.namespacesPerPage = 10; - $scope.repositoriesPerPage = 10; - $scope.robotsPerPage = 10; - - $scope.local.namespaceOptions = { - 'filter': '', - 'predicate': 'score', - 'reverse': false, - 'page': 0 - }; - - $scope.local.repositoryOptions = { - 'filter': '', - 'predicate': 'last_updated', - 'reverse': false, - 'page': 0, - 'hideStale': true - }; - - $scope.local.robotOptions = { - 'filter': '', - 'predicate': 'can_read', - 'reverse': false, - 'page': 0 - }; - - $scope.getTriggerIcon = function() { - return TriggerService.getIcon($scope.trigger.service); - }; - - $scope.createTrigger = function() { - console.log($scope.local); - var config = { - 'build_source': $scope.local.selectedRepository.full_name, - 'subdir': $scope.local.dockerfilePath.substr(1) // Remove starting / - }; - - if ($scope.local.triggerOptions.hasBranchTagFilter && - $scope.local.triggerOptions.branchTagFilter) { - config['branchtag_regex'] = $scope.local.triggerOptions.branchTagFilter; - } - - var activate = function() { - $scope.activateTrigger({'config': config, 'pull_robot': $scope.local.robotAccount}); - }; - - if ($scope.local.robotAccount) { - if ($scope.local.robotAccount.can_read) { - activate(); - } else { - // Add read permission onto the base repository for the robot and then activate the - // trigger. - var robot_name = $scope.local.robotAccount.name; - RolesService.setRepositoryRole($scope.repository, 'read', 'robot', robot_name, activate); - } - } else { - activate(); - } - }; - - var buildOrderedNamespaces = function() { - if (!$scope.local.namespaces) { - return; - } - - var namespaces = $scope.local.namespaces || []; - $scope.local.orderedNamespaces = TableService.buildOrderedItems(namespaces, - $scope.local.namespaceOptions, - ['id'], - ['score']) - - $scope.local.maxScore = 0; - namespaces.forEach(function(namespace) { - $scope.local.maxScore = Math.max(namespace.score, $scope.local.maxScore); - }); - }; - - var loadNamespaces = function() { - $scope.local.namespaces = null; - $scope.local.selectedNamespace = null; - $scope.local.orderedNamespaces = null; - - $scope.local.selectedRepository = null; - $scope.local.orderedRepositories = null; - - var params = { - 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'trigger_uuid': $scope.trigger.id - }; - - ApiService.listTriggerBuildSourceNamespaces(null, params).then(function(resp) { - $scope.local.namespaces = resp['namespaces']; - $scope.local.repositories = null; - buildOrderedNamespaces(); - }, ApiService.errorDisplay('Could not retrieve the list of ' + $scope.namespaceTitle)) - }; - - var buildOrderedRepositories = function() { - if (!$scope.local.repositories) { - return; - } - - var repositories = $scope.local.repositories || []; - repositories.forEach(function(repository) { - repository['last_updated_datetime'] = new Date(repository['last_updated'] * 1000); - }); - - if ($scope.local.repositoryOptions.hideStale) { - var existingRepositories = repositories; - - repositories = repositories.filter(function(repository) { - var older_date = moment(repository['last_updated_datetime']).add(1, 'months'); - return !moment().isAfter(older_date); - }); - - if (existingRepositories.length > 0 && repositories.length == 0) { - repositories = existingRepositories; - } - } - - $scope.local.orderedRepositories = TableService.buildOrderedItems(repositories, - $scope.local.repositoryOptions, - ['name', 'description'], - []); - }; - - var loadRepositories = function(namespace) { - $scope.local.repositories = null; - $scope.local.selectedRepository = null; - $scope.local.repositoryRefs = null; - $scope.local.triggerOptions = { - 'hasBranchTagFilter': false - }; - - $scope.local.orderedRepositories = null; - - var params = { - 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'trigger_uuid': $scope.trigger.id - }; - - var data = { - 'namespace': namespace.id - }; - - ApiService.listTriggerBuildSources(data, params).then(function(resp) { - if (namespace == $scope.local.selectedNamespace) { - $scope.local.repositories = resp['sources']; - buildOrderedRepositories(); - } - }, ApiService.errorDisplay('Could not retrieve repositories')); - }; - - var loadRepositoryRefs = function(repository) { - $scope.local.repositoryRefs = null; - $scope.local.triggerOptions = { - 'hasBranchTagFilter': false - }; - - var params = { - 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'trigger_uuid': $scope.trigger.id, - 'field_name': 'refs' - }; - - var config = { - 'build_source': repository.full_name - }; - - ApiService.listTriggerFieldValues(config, params).then(function(resp) { - if (repository == $scope.local.selectedRepository) { - $scope.local.repositoryRefs = resp['values']; - $scope.local.repositoryFullRefs = resp['values'].map(function(ref) { - var kind = ref.kind == 'branch' ? 'heads' : 'tags'; - var icon = ref.kind == 'branch' ? 'fa-code-fork' : 'fa-tag'; - return { - 'value': kind + '/' + ref.name, - 'icon': icon, - 'title': ref.name - }; - }); - } - }, ApiService.errorDisplay('Could not retrieve repository refs')); - }; - - var loadDockerfileLocations = function(repository) { - $scope.local.dockerfilePath = null; - - var params = { - 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'trigger_uuid': $scope.trigger.id - }; - - var config = { - 'build_source': repository.full_name - }; - - ApiService.listBuildTriggerSubdirs(config, params).then(function(resp) { - if (repository == $scope.local.selectedRepository) { - $scope.local.dockerfileLocations = resp; - } - }, ApiService.errorDisplay('Could not retrieve Dockerfile locations')); - }; - - var buildOrderedRobotAccounts = function() { - if (!$scope.local.triggerAnalysis || !$scope.local.triggerAnalysis.robots) { - return; - } - - var robots = $scope.local.triggerAnalysis.robots; - $scope.local.orderedRobotAccounts = TableService.buildOrderedItems(robots, - $scope.local.robotOptions, - ['name'], - []); - }; - - var checkDockerfilePath = function(repository, path) { - $scope.local.triggerAnalysis = null; - $scope.local.robotAccount = null; - - var params = { - 'repository': $scope.repository.namespace + '/' + $scope.repository.name, - 'trigger_uuid': $scope.trigger.id - }; - - var config = { - 'build_source': repository.full_name, - 'subdir': path.substr(1) - }; - - var data = { - 'config': config - }; - - ApiService.analyzeBuildTrigger(data, params).then(function(resp) { - $scope.local.triggerAnalysis = resp; - buildOrderedRobotAccounts(); - }, ApiService.errorDisplay('Could not analyze trigger')); - }; - - $scope.$watch('trigger', function(trigger) { - if (trigger && $scope.repository) { - $scope.config = trigger['config'] || {}; - $scope.namespaceTitle = 'organization'; - $scope.local.selectedNamespace = null; - loadNamespaces(); - } - }); - - $scope.$watch('local.selectedNamespace', function(namespace) { - if (namespace) { - loadRepositories(namespace); - } - }); - - $scope.$watch('local.selectedRepository', function(repository) { - if (repository) { - loadRepositoryRefs(repository); - loadDockerfileLocations(repository); - } - }); - - $scope.$watch('local.dockerfilePath', function(path) { - if (path && $scope.local.selectedRepository) { - checkDockerfilePath($scope.local.selectedRepository, path); - } - }); - - $scope.$watch('local.namespaceOptions.predicate', buildOrderedNamespaces); - $scope.$watch('local.namespaceOptions.reverse', buildOrderedNamespaces); - $scope.$watch('local.namespaceOptions.filter', buildOrderedNamespaces); - - $scope.$watch('local.repositoryOptions.predicate', buildOrderedRepositories); - $scope.$watch('local.repositoryOptions.reverse', buildOrderedRepositories); - $scope.$watch('local.repositoryOptions.filter', buildOrderedRepositories); - $scope.$watch('local.repositoryOptions.hideStale', buildOrderedRepositories); - - $scope.$watch('local.robotOptions.predicate', buildOrderedRobotAccounts); - $scope.$watch('local.robotOptions.reverse', buildOrderedRobotAccounts); - $scope.$watch('local.robotOptions.filter', buildOrderedRobotAccounts); - } - }; - return directiveDefinitionObject; -}); \ No newline at end of file diff --git a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html index ad35aa864..de8dc9bc9 100644 --- a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html +++ b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html @@ -8,7 +8,7 @@
+ section-valid="$ctrl.local.selectedNamespace">

Select {{ $ctrl.namespaceTitle }}

@@ -22,21 +22,21 @@ current-page="$ctrl.local.namespaceOptions.page" page-size="$ctrl.namespacesPerPage">
- - - + + + + ng-value="namespace"> @@ -133,7 +133,7 @@ - diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts index 34ddedb62..855fd9197 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts @@ -1,5 +1,6 @@ import { LinearWorkflowComponent, SectionInfo } from './linear-workflow.component'; import { LinearWorkflowSectionComponent } from './linear-workflow-section.component'; +import Spy = jasmine.Spy; describe("LinearWorkflowComponent", () => { @@ -10,10 +11,103 @@ describe("LinearWorkflowComponent", () => { }); describe("addSection", () => { + var newSection: LinearWorkflowSectionComponent; + beforeEach(() => { + newSection = new LinearWorkflowSectionComponent; + }); + + it("does not set 'sectionVisible' or 'isCurrentSection' of given section if not the first section added", () => { + component.addSection(new LinearWorkflowSectionComponent); + component.addSection(newSection); + + expect(newSection.sectionVisible).toBe(false); + expect(newSection.isCurrentSection).toBe(false); + }); + + it("sets 'sectionVisible' of given section to true if it is the first section added", () => { + component.addSection(newSection); + + expect(newSection.sectionVisible).toBe(true); + }); + + it("sets 'isCurrentSection' of given section to true if it is the first section added", () => { + component.addSection(newSection); + + expect(newSection.isCurrentSection).toBe(true); + }); }); describe("onNextSection", () => { + var currentSection: LinearWorkflowSectionComponent; + beforeEach(() => { + component.onWorkflowComplete = jasmine.createSpy("onWorkflowComplete").and.returnValue(null); + currentSection = new LinearWorkflowSectionComponent; + currentSection.sectionValid = true; + component.addSection(currentSection); + }); + + it("does not complete workflow or change current section if current section is invalid", () => { + currentSection.sectionValid = false; + component.onNextSection(); + + expect(component.onWorkflowComplete).not.toHaveBeenCalled(); + expect(currentSection.isCurrentSection).toBe(true); + }); + + it("calls workflow completed output callback if current section is the last section and is valid", () => { + component.onNextSection(); + + expect(component.onWorkflowComplete).toHaveBeenCalled(); + }); + + it("sets the current section to the next section if there are remaining sections and current section valid", () => { + var nextSection: LinearWorkflowSectionComponent = new LinearWorkflowSectionComponent(); + component.addSection(nextSection); + component.onNextSection(); + + expect(currentSection.isCurrentSection).toBe(false); + expect(nextSection.isCurrentSection).toBe(true); + expect(nextSection.sectionVisible).toBe(true); + }); + }); + + describe("onSectionInvalid", () => { + var invalidSection: LinearWorkflowSectionComponent; + var sections: LinearWorkflowSectionComponent[]; + + beforeEach(() => { + invalidSection = new LinearWorkflowSectionComponent(); + invalidSection.sectionId = "Git Repository"; + invalidSection.sectionValid = false; + component.addSection(invalidSection); + + sections = [ + new LinearWorkflowSectionComponent(), + new LinearWorkflowSectionComponent(), + new LinearWorkflowSectionComponent(), + ]; + sections.forEach((section) => { + section.sectionVisible = true; + section.isCurrentSection = true; + component.addSection(section); + }); + }); + + it("sets the section with the given id to be the current section", () => { + component.onSectionInvalid(invalidSection.sectionId); + + expect(invalidSection.isCurrentSection).toBe(true); + }); + + it("hides all sections after the section with the given id", () => { + component.onSectionInvalid(invalidSection.sectionId); + + sections.forEach((section) => { + expect(section.sectionVisible).toBe(false); + expect(section.isCurrentSection).toBe(false); + }); + }); }); }); diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts index bba8cd9b6..69e43dc20 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts @@ -18,15 +18,6 @@ export class LinearWorkflowComponent implements ng.IComponentController { private sections: SectionInfo[] = []; private currentSection: SectionInfo; - - constructor() { - - } - - public $onInit(): void { - - } - public addSection(component: LinearWorkflowSectionComponent): void { this.sections.push({ index: this.sections.length, @@ -34,20 +25,34 @@ export class LinearWorkflowComponent implements ng.IComponentController { }); if (this.sections.length == 1) { - this.sections[0].component.sectionVisible = true; this.currentSection = this.sections[0]; + this.currentSection.component.sectionVisible = true; + this.currentSection.component.isCurrentSection = true; } } public onNextSection(): void { - if (this.currentSection.component.sectionValid) { - if (this.currentSection.index + 1 >= this.sections.length) { - this.onWorkflowComplete({}); - } - else { - this.currentSection = this.sections[this.currentSection.index]; - } + if (this.currentSection.component.sectionValid && this.currentSection.index + 1 >= this.sections.length) { + this.onWorkflowComplete({}); } + else if (this.currentSection.component.sectionValid && this.currentSection.index + 1 < this.sections.length) { + this.currentSection.component.isCurrentSection = false; + this.currentSection = this.sections[this.currentSection.index + 1]; + this.currentSection.component.sectionVisible = true; + this.currentSection.component.isCurrentSection = true; + } + } + + public onSectionInvalid(sectionId: string): void { + var invalidSection = this.sections.filter(section => section.component.sectionId == sectionId)[0]; + invalidSection.component.isCurrentSection = true; + this.currentSection = invalidSection; + this.sections.forEach((section) => { + if (section.index > invalidSection.index) { + section.component.sectionVisible = false; + section.component.isCurrentSection = false; + } + }); } } diff --git a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html index a82102122..d75adcb07 100644 --- a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html +++ b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html @@ -4,11 +4,10 @@ done-title="Create Trigger" workflow-complete="$ctrl.activateTrigger({'config': $ctrl.config})"> - + section-valid="$ctrl.config.build_source !== undefined">

Enter repository

@@ -25,8 +24,7 @@ - @@ -45,53 +43,3 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/data/test.db b/test/data/test.db index 415776f78184bea83328ba45eaeaeb7ba35d8714..cfce4f8698f2c58334b74b10f740a96982258200 100644 GIT binary patch delta 125 zcmZoz5YVt7V1hK`g^4oGj29XcS`!#s6PQ{Pm|GKAS`%1X6WCf4*jp1gS`#>16S!Iv zxVI+ou<0wCCt8{s8yV}Sn3^T(nj|Ns=vt&2n(HQ+85^V;r5IQwCMULc>hl0GFA(zq UF+UIs0I?tt3vKVz7cPGQ0Q1f#^8f$< delta 125 zcmZoz5YVt7V1hK`xrs8)jOQ8?S`!#s6PQ{Pm|GKAS`%1X6WCf4*jp1gS`#>16S!Iv zxVI+ou<0wO8Jid-S{UgXS|(fSnj{*V=vt;GrRf@^nVOqh8k(9VS*Epj>hl0GFA(zq UF+UIs0I?tt3vKVz7cPGQ0N;QoeE Date: Wed, 22 Feb 2017 16:33:34 -0800 Subject: [PATCH 15/29] ignore invalid linear workflow sections that are after the current section --- .../linear-workflow.component.html | 2 +- .../linear-workflow.component.spec.ts | 18 ++++++++++++++++-- .../linear-workflow.component.ts | 18 ++++++++++-------- .../manage-trigger-custom-git.component.html | 2 +- ...anage-trigger-custom-git.component.spec.ts | 14 ++++++++++++++ test/data/test.db | Bin 1314816 -> 1314816 bytes 6 files changed, 42 insertions(+), 12 deletions(-) diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.html b/static/js/directives/ui/linear-workflow/linear-workflow.component.html index bbb0d3868..899dc4c2d 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.html +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.html @@ -23,7 +23,7 @@
- {{ $ctrl.namespaceTitle }} - + {{ $ctrl.namespaceTitle }} +
- - {{ $ctrl.namespace.id }} + + {{ namespace.id }}
@@ -200,7 +200,7 @@
Regular Expression: - +
Examples: heads/master, tags/tagname, heads/.+

Select Dockerfile

- Please select the location of the Dockerfile to build when this trigger is invoked {{ $ctrl.local.paths }} + Please select the location of the Dockerfile to build when this trigger is invoked { var rolesServiceMock: any; var repository: any; var trigger: Trigger; + var $scope: ng.IScope; - beforeEach(() => { + beforeEach(inject(($injector: ng.auto.IInjectorService) => { apiServiceMock = jasmine.createSpyObj('apiServiceMock', ['listTriggerBuildSourceNamespaces']); tableServiceMock = jasmine.createSpyObj('tableServiceMock', ['buildOrderedItems']); triggerServiceMock = jasmine.createSpyObj('triggerServiceMock', ['getIcon']); rolesServiceMock = jasmine.createSpyObj('rolesServiceMock', ['setRepositoryRole']); - component = new ManageTriggerGithostComponent(apiServiceMock, tableServiceMock, triggerServiceMock, rolesServiceMock); + $scope = $injector.get('$rootScope'); + component = new ManageTriggerGithostComponent(apiServiceMock, + tableServiceMock, + triggerServiceMock, + rolesServiceMock, + $scope); trigger = {service: "serviceMock", id: 1}; component.trigger = trigger; - }); + })); describe("constructor", () => { diff --git a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts index 473bde6eb..a6815d8ad 100644 --- a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts +++ b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts @@ -1,6 +1,10 @@ import { Input, Output, Component } from 'angular-ts-decorators'; +import * as moment from 'moment'; +/** + * A component that lets the user set up a build trigger for a public Git repository host service. + */ @Component({ selector: 'manageTriggerGithost', templateUrl: '/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html' @@ -11,7 +15,7 @@ export class ManageTriggerGithostComponent implements ng.IComponentController { @Input('=') public trigger: Trigger; @Output() public activateTrigger: (trigger: {config: any, pull_robot: any}) => void; private config: any; - private local: Local = { + private local: any = { namespaceOptions: { filter: '', predicate: 'score', @@ -36,16 +40,65 @@ export class ManageTriggerGithostComponent implements ng.IComponentController { private namespacesPerPage: number = 10; private repositoriesPerPage: number = 10; private robotsPerPage: number = 10; + private namespaceTitle: string; + private namespace: any; constructor(private ApiService: any, private TableService: any, private TriggerService: any, - private RolesService: any) { - + private RolesService: any, + private $scope: ng.IScope) { + // FIXME: Here binding methods to class context in order to pass them as arguments to $scope.$watch + this.buildOrderedNamespaces = this.buildOrderedNamespaces.bind(this); + this.loadNamespaces = this.loadNamespaces.bind(this); + this.buildOrderedRepositories = this.buildOrderedRepositories.bind(this); + this.loadRepositories = this.loadRepositories.bind(this); + this.loadRepositoryRefs = this.loadRepositoryRefs.bind(this); + this.buildOrderedRobotAccounts = this.buildOrderedRobotAccounts.bind(this); + this.loadDockerfileLocations = this.loadDockerfileLocations.bind(this); + this.checkDockerfilePath = this.checkDockerfilePath.bind(this); } public $onInit(): void { + // TODO: Replace $scope.$watch with @Output methods for child component mutations or $onChanges for parent mutations + this.$scope.$watch(() => this.trigger, (trigger) => { + if (trigger && this.repository) { + this.config = trigger['config'] || {}; + this.namespaceTitle = 'organization'; + this.local.selectedNamespace = null; + this.loadNamespaces(); + } + }); + this.$scope.$watch(() => this.local.selectedNamespace, (namespace) => { + if (namespace) { + this.loadRepositories(namespace); + } + }); + + this.$scope.$watch(() => this.local.selectedRepository, (repository) => { + if (repository) { + this.loadRepositoryRefs(repository); + this.loadDockerfileLocations(repository); + } + }); + + this.$scope.$watch(() => this.local.dockerfilePath, (path) => { + if (path && this.local.selectedRepository) { + this.checkDockerfilePath(this.local.selectedRepository, path); + } + }); + + this.$scope.$watch(() => this.local.namespaceOptions.predicate, this.buildOrderedNamespaces); + this.$scope.$watch(() => this.local.namespaceOptions.reverse, this.buildOrderedNamespaces); + this.$scope.$watch(() => this.local.namespaceOptions.filter, this.buildOrderedNamespaces); + this.$scope.$watch(() => this.local.repositoryOptions.predicate, this.buildOrderedRepositories); + this.$scope.$watch(() => this.local.repositoryOptions.reverse, this.buildOrderedRepositories); + this.$scope.$watch(() => this.local.repositoryOptions.filter, this.buildOrderedRepositories); + this.$scope.$watch(() => this.local.repositoryOptions.hideStale, this.buildOrderedRepositories); + this.$scope.$watch(() => this.local.robotOptions.predicate, this.buildOrderedRobotAccounts); + this.$scope.$watch(() => this.local.robotOptions.reverse, this.buildOrderedRobotAccounts); + this.$scope.$watch(() => this.local.robotOptions.filter, this.buildOrderedRobotAccounts); } public $onChanges(changes: ng.IOnChangesObject): void { @@ -86,35 +139,186 @@ export class ManageTriggerGithostComponent implements ng.IComponentController { } private buildOrderedNamespaces(): void { + if (!this.local.namespaces) { + return; + } + var namespaces = this.local.namespaces || []; + this.local.orderedNamespaces = this.TableService.buildOrderedItems(namespaces, + this.local.namespaceOptions, + ['id'], + ['score']); + + this.local.maxScore = 0; + namespaces.forEach((namespace) => { + this.local.maxScore = Math.max(namespace.score, this.local.maxScore); + }); } private loadNamespaces(): void { + this.local.namespaces = null; + this.local.selectedNamespace = null; + this.local.orderedNamespaces = null; + this.local.selectedRepository = null; + this.local.orderedRepositories = null; + + var params = { + 'repository': this.repository.namespace + '/' + this.repository.name, + 'trigger_uuid': this.trigger.id + }; + + this.ApiService.listTriggerBuildSourceNamespaces(null, params) + .then((resp) => { + this.local.namespaces = resp['namespaces']; + this.local.repositories = null; + this.buildOrderedNamespaces(); + }, this.ApiService.errorDisplay('Could not retrieve the list of ' + this.namespaceTitle)); } private buildOrderedRepositories(): void { + if (!this.local.repositories) { + return; + } + var repositories = this.local.repositories || []; + repositories.forEach((repository) => { + repository['last_updated_datetime'] = new Date(repository['last_updated'] * 1000); + }); + + if (this.local.repositoryOptions.hideStale) { + var existingRepositories = repositories; + + repositories = repositories.filter((repository) => { + var older_date = moment(repository['last_updated_datetime']).add(1, 'months'); + return !moment().isAfter(older_date); + }); + + if (existingRepositories.length > 0 && repositories.length == 0) { + repositories = existingRepositories; + } + } + + this.local.orderedRepositories = this.TableService.buildOrderedItems(repositories, + this.local.repositoryOptions, + ['name', 'description'], + []); } - private loadRepositories(): void { + private loadRepositories(namespace: any): void { + this.local.repositories = null; + this.local.selectedRepository = null; + this.local.repositoryRefs = null; + this.local.triggerOptions = { + 'hasBranchTagFilter': false + }; + this.local.orderedRepositories = null; + + var params = { + 'repository': this.repository.namespace + '/' + this.repository.name, + 'trigger_uuid': this.trigger.id + }; + + var data = { + 'namespace': namespace.id + }; + + this.ApiService.listTriggerBuildSources(data, params).then((resp) => { + if (namespace == this.local.selectedNamespace) { + this.local.repositories = resp['sources']; + this.buildOrderedRepositories(); + } + }, this.ApiService.errorDisplay('Could not retrieve repositories')); } private loadRepositoryRefs(repository: any): void { + this.local.repositoryRefs = null; + this.local.triggerOptions = { + 'hasBranchTagFilter': false + }; + var params = { + 'repository': this.repository.namespace + '/' + this.repository.name, + 'trigger_uuid': this.trigger.id, + 'field_name': 'refs' + }; + + var config = { + 'build_source': repository.full_name + }; + + this.ApiService.listTriggerFieldValues(config, params).then((resp) => { + if (repository == this.local.selectedRepository) { + this.local.repositoryRefs = resp['values']; + this.local.repositoryFullRefs = resp['values'].map((ref) => { + var kind = ref.kind == 'branch' ? 'heads' : 'tags'; + var icon = ref.kind == 'branch' ? 'fa-code-fork' : 'fa-tag'; + return { + 'value': kind + '/' + ref.name, + 'icon': icon, + 'title': ref.name + }; + }); + } + }, this.ApiService.errorDisplay('Could not retrieve repository refs')); } private loadDockerfileLocations(repository: any): void { + this.local.dockerfilePath = null; + var params = { + 'repository': this.repository.namespace + '/' + this.repository.name, + 'trigger_uuid': this.trigger.id + }; + + var config = { + 'build_source': repository.full_name + }; + + this.ApiService.listBuildTriggerSubdirs(config, params) + .then((resp) => { + if (repository == this.local.selectedRepository) { + this.local.dockerfileLocations = resp; + } + }, this.ApiService.errorDisplay('Could not retrieve Dockerfile locations')); } private buildOrderedRobotAccounts(): void { + if (!this.local.triggerAnalysis || !this.local.triggerAnalysis.robots) { + return; + } + var robots = this.local.triggerAnalysis.robots; + this.local.orderedRobotAccounts = this.TableService.buildOrderedItems(robots, + this.local.robotOptions, + ['name'], + []); } private checkDockerfilePath(repository: any, path: string): void { + this.local.triggerAnalysis = null; + this.local.robotAccount = null; + var params = { + 'repository': this.repository.namespace + '/' + this.repository.name, + 'trigger_uuid': this.trigger.id + }; + + var config = { + 'build_source': repository.full_name, + 'subdir': path.substr(1) + }; + + var data = { + 'config': config + }; + + this.ApiService.analyzeBuildTrigger(data, params) + .then((resp) => { + this.local.triggerAnalysis = resp; + this.buildOrderedRobotAccounts(); + }, this.ApiService.errorDisplay('Could not analyze trigger')); } } diff --git a/static/partials/trigger-setup.html b/static/partials/trigger-setup.html index 79f676508..7f1fad28d 100644 --- a/static/partials/trigger-setup.html +++ b/static/partials/trigger-setup.html @@ -51,8 +51,10 @@
-
+
diff --git a/test/data/test.db b/test/data/test.db index b089c3c80ea413126624be51bd6269ce9088a0e9..203537eee25924398dc0f5497646dfd60349ae45 100644 GIT binary patch delta 3618 zcmeH~%a7xB9l&=oyPchEX0xRu1nN$AJAJ6dOyl=sg=lTZar}tm#C8&!ExU1S$Btid zGI`Q!wi0axEvSHyC0YarIB@_8!ANl6#(@(Oe*lkvfR_*lIN)6#qOF7wTsR=>6K^v#P$Rp&x}4J?^9#{geWYwtpt?Jv=FIPYN-G^WJNVR-%cYo*T{^@$Ye7^Mg!Z^H; zUsbOj?ft15SI5=i`7E`ZBmD}e}-z~wF{GF(D6@I_=#>M@2hkcLtytvQ5FLd&~X{lDJe7fQs{Nmv3 z;FAYOMc&8%vG{&M$LBwOsr2#tb~E3*S0uiznCH*jE1gvGx8E#P$@iyw^WqM@Q~Kv^ zdQZL&7UaLI96oyf!#7L9efN!g@2$ekRBoI<^H%BiS915A(zTDh&;8`l@ufokuk24< z|Lc+m{?}i7r&O?8aIxT0!9xXm1(yr<3(DuOy>sRJ%qL`{K?(wbG*kiXL3mTtn?1U% z^aQcpZfNMBqYJFT8XZM3B#NYIL2CE8K7}#{%NUa0eb)m&=nIH?4udF&K=SLjT&RBTp*vUqT$no#z4%3-ba27g-`RihE02HX z!#fu|3UWOPqfRK2#7@u=GCCna8#-~25j=7o7lK{4Chod15VjhOs z2x2?9ZI3(%_uL_xfA#4Kcm~6*$#50cfs|FYYJ!n`qp{tRji3R{H>-`h3h=4s8Z|BP zHLb>wWhtw7Y^jZmd1G;k`nt}rj%N!hc?mgB26fm zZmjNPTy}xgN^e;J&$wePZ3bZtn}E&8XbPr+&D$p4S@nV#b%9fH7=^)z-h$#X<2}B; zSX<^GD`Ub?T!iqr);V*zI;GDLd<6j#so=D0)AngdC$TtbsgX3Dj8;=Hiph2C@P5$3 z6Vve+v7C0t-G)Kg1Y$soRhtRaWDs<{aYNcPakrn*iJ&bzLY?l=44n!LO=p&`)YKS5 zxzp2{U51G!J9H$N8~5)A(_F zNm5B|G%e2;+XQ75r@w^f3zlvuYrnPBlVL=UiD=XDR{d$mQ|W9%vK!TGZYC44&30TCvXF6Dk67G8)^s{1*Ve4FaZ^S#d>v4l%#!P7 z8+v7hE?}0Ml789)YVAg##YB7MIn%Kx^_L1<*9}}jOlQ7Kz}_h~P$-L4@GxpkEMTmu z5Vn<>etlj>&H{xN;XsAQWXkFRM|W%17Fuj)M0;Y)rkmQxW!9-XX`>xRb$Y0ZH2jQM zFcYv(SaoexHozqsOCdWpIU3bi-y!>3M74uay}e?9c8uVPw)6qj$i`->!U4cKIC@H` zRC^K=ICsXE8&X#unaxl$=Sa==x81luGa_W2tvGZW)X}0*2mG9w+iB1masu818qrq5 zIUQcuOGK)b!J(wpeKNIlbdpS#v}>9336@A9=BI4Rl1@5UyH4@n*RPE|4f4^wpZ*(#MP&cBmMbjTQh?$E1vvL3!4~P8&AXq-|}ea)a7RROHxJ z&XkpnqT!_6Rs9x})Thg^rKT}&f>GP197!TJGBY71Zs<+%$hp7IbaN2SrPS2pz)X8< z`m~G91FdX>hRAr(6bpim(%1qbr=@17Npczqj<6`oYJh~Yo)cu&=||=ym|4B`{EYJW z#0o78)ZLZeLCf7w$J-b2@D~j->fgTNhY{1qIbJlYtHLNG&b_fCzZ$Z zZ&weI4>|5MN;h{xcXKCjj&I$Bi#ky_gywfoDo2-KkRq}C4+n>L&TMm|%-D{Kj|HR! zc*)8%2CBO13j=&B*M%A@XOW2Ml;k!vnf}YBfh*UrNM?Z!-i@XIkq7H5)i1ja^Bv#cIdxbw>UeiC5TU zT4(x_?Grc{WqnOt)zwZ6q!wx7L?iOuS?H%Rt`F-JgLYKtc6;f~>H~DPLQ)2sGYh!G zx@O#30Y(-pTQ<@9_LjH6Aunt*$A?HR_IZZu>CkfN=zW4lc>!=-u>sJ=Oz@`IFl9%+ zL><5U;zw`hZyy{Uz5Yh6QgBdkwV+yXt>EE;51hYIdt~#|^6oxLFkN=tW zU|xRtmCE(Mm-`j|sdtt8-7`J}7k@p)0e$Lt=1$Y%wLYE)@ic$_caJ=qvj>My?Yw;c Kv&!Mg?q2|e7DVm< delta 2272 zcmbuA%dgy49mf;jHaBsmnHw6hNt-@|K<%{nZI3~M_&N5BALFrS?6Hc7pW}Jio|k7l zLb_qmR!9N0RKik;M3K5eLPA0?h_Z`>#15%{0D>(GDz>n|$p-c;KAZFX{=VmXj*fI5 ze$s#VN&kbpSD$?N5_k2<_aD6b;OcK40G~F`?l!+$onHX=PH$WQZ#F-A_5MZm1$6dY z6}$rc{JFc$^h$LB19zX_G}GqcqkCU``NX~oyx#nTm4%vpZ%-3JiT=vApR$Mr2`+G-EBOn&ierM>)ZF=Z2K8 zQ_D}Q^M}Cer?(#hZ#}h$)%X$6tp47(bNlq!BS3y?iK_Dto?1Qt{&};ubgNe$0~gOM z@4xpLs5z^7uIBlgYc=OJ*K2OnfT#B!-~26mpHo?M)WeJ(%#j^SLaZ`b>-@nrQ;kEm>{#_@d*DikC1zxx^ym9r$cRqgY2lmw~)h`=& zzVZfupky0|$p9rtEG8)5r^x_;Lp-Eu7>;R@iYY(xeXsh%R~zu*MN5OKNjX^HOkV&){3Z-$JUD}k$4MBkJHNwIilj?xLIXxe5tQ9-Jq+}2r!`rV$3T)rEVeq#5i1EO5x^XkrNwAJoS1Fi_Jqaiu;$ST>044vjs!(k zlXbUk$UI{(ER)GBBTmyX>!X-)m~3(`1Cct1$UB#+Z-_@%=$BeXjhs8lP0=KM6@oxJs;yt&8BXypUC-9}ni>VoSBQ%s4Uq={#0)ceOf*tu4e!@l-1|;gBYk zwY}6;%vukOsE=_gOIw9P8>hX&k?{$zxWut=iF4t;HS0N(UKh1zxhVDnL(*E5l9N%_ z<$I;p-zP1@Ww5<(dn+gmlw>YT#~D+A$8BMa<}f!I3k;m0Gn;0h(HslpUdQEgcRC$< zth~VonWRB#LEuMmJ%D(D7@3OHi_FUq^hVvuY{rSM-R|kxYAI?ACSBLVJDIF7ahguaA=MN2{Uuj)Ert{>KU1#U4Gm1dB=hy zoI)a{UqYoJLprtK&C6*U$_m*7`EjpPkonG<=CYlH2g}EgsxQeS!FSr4B^ScZ9k-vB zW|8)YRx)VX*(uzhvmIna>McO5AIS9GZAq+B}mya!jaS&?o`?j3$w0g z@UuS5h8}FelS>|k$o?c=V+GTs@=j7f1RwU9YyYY?4)ym<{fm+Gl6@Uax|)U zjv}-tWjDPHRy~fS<=zn%5E5+p!`V7C2eTFCOsJ7z@|&2(Rcy28rM){SgIw;(lCoZz zlR|J0W*|;xf-~i|jKrg{mw?#5jS~dPnA?Iwtwqd+k%S6fzHEhvH)O=Vz3sANotnq~ zvTg6mOWN~!A)zG5qtt*-M6guhoXI+jky?Aq$Q@W;Vm!k2FjnwNG=Pd(s(A|Lin&CE zR@O!o1G2(HXCV}c&v%!gxkz=)N?W?_3gZHa1bG?ydng*Ad8+YlPqgrr1TPBBMD7d=wu;H;U!|HJr z2=LgDtFOJ?c&*By#!IK)y>O=ky#EMl)Vxr0tEO3VQFFWIOQ%QBi-$j7yW05tO5^kT PEBjO9^m*gX+h_j)q`J?H From c86b09e26e6b7af4e6c4217747e2fca23934f8c3 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sun, 19 Feb 2017 18:35:46 -0800 Subject: [PATCH 10/29] moved config to seperate module --- .../linear-workflow.component.ts | 10 ++ static/js/quay.config.ts | 116 ++++++++++++++++++ static/js/quay.module.ts | 101 ++------------- 3 files changed, 135 insertions(+), 92 deletions(-) create mode 100644 static/js/quay.config.ts diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts index e69de29bb..a82e01b54 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts @@ -0,0 +1,10 @@ +import { Component, Output, Input } from 'angular-ts-decorators'; + + +@Component({ + selector: 'linearWorkflow', + templateUrl: '/static/js/directives/ui/linear-workflow/linear-workflow.component.html' +}) +export class LinearWorkflowComponent implements ng.IComponentController { + +} diff --git a/static/js/quay.config.ts b/static/js/quay.config.ts new file mode 100644 index 000000000..164c98692 --- /dev/null +++ b/static/js/quay.config.ts @@ -0,0 +1,116 @@ +import { NgModule } from 'angular-ts-decorators'; +import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant"; +import { NAME_PATTERNS } from "./constants/name-patterns.constant"; +import * as Raven from "raven-js"; +import * as angular from 'angular'; + + +var quayDependencies: any[] = [ + 'chieffancypants.loadingBar', + 'cfp.hotkeys', + 'angular-tour', + 'restangular', + 'angularMoment', + 'mgcrea.ngStrap', + 'ngCookies', + 'ngSanitize', + 'angular-md5', + 'pasvaz.bindonce', + 'ansiToHtml', + 'core-ui', + 'core-config-setup', + 'infinite-scroll', + 'react' +]; + +if (INJECTED_CONFIG && (INJECTED_CONFIG.MIXPANEL_KEY || + INJECTED_CONFIG.MUNCHKIN_KEY || + INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY)) { + quayDependencies.push('angulartics'); +} +if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { + quayDependencies.push('angulartics.mixpanel'); +} +if (INJECTED_CONFIG && INJECTED_CONFIG.MUNCHKIN_KEY) { + quayDependencies.push('angulartics.marketo'); +} +if (INJECTED_CONFIG && INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY) { + quayDependencies.push('angulartics.google.analytics'); +} +if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { + quayDependencies.push('vcRecaptcha'); +} + + +/** + * Module for application-wide configuration. + */ +@NgModule({ + imports: quayDependencies, + declarations: [], + providers: [] +}) +export class QuayConfig { + + public config($provide: ng.auto.IProvideService, + $injector: ng.auto.IInjectorService, + INJECTED_CONFIG: any, + cfpLoadingBarProvider: any, + $tooltipProvider: any, + $compileProvider: ng.ICompileProvider, + RestangularProvider: any): void { + cfpLoadingBarProvider.includeSpinner = false; + + // decorate the tooltip getter + var tooltipFactory: any = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; + $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window: ng.IWindowService) { + if ('ontouchstart' in $window) { + var existing: any = tooltipFactory.apply(this, arguments); + return function(element) { + // Note: We only disable bs-tooltip's themselves. $tooltip is used for other things + // (such as the datepicker), so we need to be specific when canceling it. + if (element !== undefined && element.attr('bs-tooltip') == null) { + return existing.apply(this, arguments); + } + }; + } + + return tooltipFactory.apply(this, arguments); + }; + + if (!INJECTED_CONFIG['DEBUG']) { + $compileProvider.debugInfoEnabled(false); + } + + // Configure compile provider to add additional URL prefixes to the sanitization list. We use + // these on the Contact page. + $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|irc):/); + + // Configure the API provider. + RestangularProvider.setBaseUrl('/api/v1/'); + + // Configure analytics. + if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { + let $analyticsProvider: any = $injector.get('$analyticsProvider'); + $analyticsProvider.virtualPageviews(true); + } + + // Configure sentry. + if (INJECTED_CONFIG && INJECTED_CONFIG.SENTRY_PUBLIC_DSN) { + $provide.decorator("$exceptionHandler", function($delegate) { + return function(ex, cause) { + $delegate(ex, cause); + Raven.captureException(ex, {extra: {cause: cause}}); + }; + }); + } + } +} + + +angular + .module(QuayConfig.name) + .constant('NAME_PATTERNS', NAME_PATTERNS) + .constant('INJECTED_CONFIG', INJECTED_CONFIG) + .constant('INJECTED_FEATURES', INJECTED_FEATURES) + .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS); \ No newline at end of file diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index a318c4a38..e4c6099bf 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -1,5 +1,4 @@ import * as angular from "angular"; -import * as Raven from "raven-js"; import { ViewArrayImpl } from "./services/view-array/view-array.impl"; import { NAME_PATTERNS } from "./constants/name-patterns.constant"; import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant"; @@ -9,56 +8,24 @@ import { QuayRoutes } from "./quay-routes.module"; import { DockerfilePathSelectComponent } from './directives/ui/dockerfile-path-select/dockerfile-path-select.component'; import { ManageTriggerCustomGitComponent } from './directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component'; import { ManageTriggerGithostComponent } from './directives/ui/manage-trigger-githost/manage-trigger-githost.component'; - - -var quayDependencies: any[] = [ - QuayRoutes, - 'chieffancypants.loadingBar', - 'cfp.hotkeys', - 'angular-tour', - 'restangular', - 'angularMoment', - 'mgcrea.ngStrap', - 'ngCookies', - 'ngSanitize', - 'angular-md5', - 'pasvaz.bindonce', - 'ansiToHtml', - 'core-ui', - 'core-config-setup', - 'infinite-scroll', - 'react' -]; - -if (INJECTED_CONFIG && (INJECTED_CONFIG.MIXPANEL_KEY || - INJECTED_CONFIG.MUNCHKIN_KEY || - INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY)) { - quayDependencies.push('angulartics'); -} -if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { - quayDependencies.push('angulartics.mixpanel'); -} -if (INJECTED_CONFIG && INJECTED_CONFIG.MUNCHKIN_KEY) { - quayDependencies.push('angulartics.marketo'); -} -if (INJECTED_CONFIG && INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY) { - quayDependencies.push('angulartics.google.analytics'); -} -if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { - quayDependencies.push('vcRecaptcha'); -} +import { LinearWorkflowComponent } from './directives/ui/linear-workflow/linear-workflow.component'; +import { QuayConfig } from './quay.config'; /** * Main application module. */ @NgModule({ - imports: quayDependencies, + imports: [ + QuayRoutes, + QuayConfig, + ], declarations: [ RegexMatchViewComponent, DockerfilePathSelectComponent, ManageTriggerCustomGitComponent, ManageTriggerGithostComponent, + LinearWorkflowComponent, ], providers: [ ViewArrayImpl, @@ -66,58 +33,8 @@ if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { }) export class quay { - public config($provide: ng.auto.IProvideService, - $injector: ng.auto.IInjectorService, - INJECTED_CONFIG: any, - cfpLoadingBarProvider: any, - $tooltipProvider: any, - $compileProvider: ng.ICompileProvider, - RestangularProvider: any): void { - cfpLoadingBarProvider.includeSpinner = false; + constructor() { - // decorate the tooltip getter - var tooltipFactory: any = $tooltipProvider.$get[$tooltipProvider.$get.length - 1]; - $tooltipProvider.$get[$tooltipProvider.$get.length - 1] = function($window: ng.IWindowService) { - if ('ontouchstart' in $window) { - var existing: any = tooltipFactory.apply(this, arguments); - return function(element) { - // Note: We only disable bs-tooltip's themselves. $tooltip is used for other things - // (such as the datepicker), so we need to be specific when canceling it. - if (element !== undefined && element.attr('bs-tooltip') == null) { - return existing.apply(this, arguments); - } - }; - } - - return tooltipFactory.apply(this, arguments); - }; - - if (!INJECTED_CONFIG['DEBUG']) { - $compileProvider.debugInfoEnabled(false); - } - - // Configure compile provider to add additional URL prefixes to the sanitization list. We use - // these on the Contact page. - $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|irc):/); - - // Configure the API provider. - RestangularProvider.setBaseUrl('/api/v1/'); - - // Configure analytics. - if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { - let $analyticsProvider: any = $injector.get('$analyticsProvider'); - $analyticsProvider.virtualPageviews(true); - } - - // Configure sentry. - if (INJECTED_CONFIG && INJECTED_CONFIG.SENTRY_PUBLIC_DSN) { - $provide.decorator("$exceptionHandler", function($delegate) { - return function(ex, cause) { - $delegate(ex, cause); - Raven.captureException(ex, {extra: {cause: cause}}); - }; - }); - } } public run($rootScope: QuayRunScope, @@ -239,7 +156,7 @@ interface QuayRunScope extends ng.IRootScopeService { } -// TODO: Move component registration to @NgModule and remove this. +// TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2 angular .module(quay.name) .constant('NAME_PATTERNS', NAME_PATTERNS) From a83a7fe47af94c22126695b394ac149a2dfd55a3 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Sun, 19 Feb 2017 18:41:48 -0800 Subject: [PATCH 11/29] moved run block to seperate module --- .../{quay.config.ts => quay-config.module.ts} | 1 + static/js/quay-run.module.ts | 179 ++++++++++++++++++ static/js/quay.module.ts | 125 +----------- 3 files changed, 183 insertions(+), 122 deletions(-) rename static/js/{quay.config.ts => quay-config.module.ts} (96%) create mode 100644 static/js/quay-run.module.ts diff --git a/static/js/quay.config.ts b/static/js/quay-config.module.ts similarity index 96% rename from static/js/quay.config.ts rename to static/js/quay-config.module.ts index 164c98692..21bfbed09 100644 --- a/static/js/quay.config.ts +++ b/static/js/quay-config.module.ts @@ -108,6 +108,7 @@ export class QuayConfig { } +// TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2 angular .module(QuayConfig.name) .constant('NAME_PATTERNS', NAME_PATTERNS) diff --git a/static/js/quay-run.module.ts b/static/js/quay-run.module.ts new file mode 100644 index 000000000..710235c7b --- /dev/null +++ b/static/js/quay-run.module.ts @@ -0,0 +1,179 @@ +import { NgModule } from 'angular-ts-decorators'; +import { INJECTED_CONFIG, INJECTED_FEATURES, INJECTED_ENDPOINTS } from "./constants/injected-values.constant"; +import { NAME_PATTERNS } from "./constants/name-patterns.constant"; +import * as angular from 'angular'; + + +var quayDependencies: any[] = [ + 'chieffancypants.loadingBar', + 'cfp.hotkeys', + 'angular-tour', + 'restangular', + 'angularMoment', + 'mgcrea.ngStrap', + 'ngCookies', + 'ngSanitize', + 'angular-md5', + 'pasvaz.bindonce', + 'ansiToHtml', + 'core-ui', + 'core-config-setup', + 'infinite-scroll', + 'react' +]; + +if (INJECTED_CONFIG && (INJECTED_CONFIG.MIXPANEL_KEY || + INJECTED_CONFIG.MUNCHKIN_KEY || + INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY)) { + quayDependencies.push('angulartics'); +} +if (INJECTED_CONFIG && INJECTED_CONFIG.MIXPANEL_KEY) { + quayDependencies.push('angulartics.mixpanel'); +} +if (INJECTED_CONFIG && INJECTED_CONFIG.MUNCHKIN_KEY) { + quayDependencies.push('angulartics.marketo'); +} +if (INJECTED_CONFIG && INJECTED_CONFIG.GOOGLE_ANALYTICS_KEY) { + quayDependencies.push('angulartics.google.analytics'); +} +if (INJECTED_CONFIG && INJECTED_CONFIG.RECAPTCHA_SITE_KEY) { + quayDependencies.push('vcRecaptcha'); +} + + +/** + * Module for application-wide configuration. + */ +@NgModule({ + imports: quayDependencies, + declarations: [], + providers: [] +}) +export class QuayRun { + + public run($rootScope: QuayRunScope, + Restangular: any, + PlanService: any, + $http: ng.IHttpService, + CookieService: any, + Features: any, + $anchorScroll: ng.IAnchorScrollService, + MetaService: any, + INJECTED_CONFIG: any): void { + var defaultTitle = INJECTED_CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; + + // Handle session security. + Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], + {'_csrf_token': (window).__token || ''}); + + // Handle session expiration. + Restangular.setErrorInterceptor(function(response) { + if (response !== undefined && response.status == 503) { + ($('#cannotContactService')).modal({}); + return false; + } + + if (response !== undefined && response.status == 500) { + window.location.href = '/500'; + return false; + } + + if (response !== undefined && !response.data) { + return true; + } + + var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; + if (response !== undefined && response.status == 401 && + invalid_token && + response.data['session_required'] !== false) { + ($('#sessionexpiredModal')).modal({}); + return false; + } + + return true; + }); + + // Check if we need to redirect based on a previously chosen plan. + var result = PlanService.handleNotedPlan(); + + // Check to see if we need to show a redirection page. + var redirectUrl = CookieService.get('quay.redirectAfterLoad'); + CookieService.clear('quay.redirectAfterLoad'); + + if (!result && redirectUrl && redirectUrl.indexOf((window).location.href) == 0) { + (window).location = redirectUrl; + return; + } + + $rootScope.$watch('description', function(description: string) { + if (!description) { + description = `Hosted private docker repositories. Includes full user management and history. + Free for public repositories.`; + } + + // Note: We set the content of the description tag manually here rather than using Angular binding + // because we need the tag to have a default description that is not of the form "{{ description }}", + // we read by tools that do not properly invoke the Angular code. + $('#descriptionTag').attr('content', description); + }); + + // Listen for scope changes and update the title and description accordingly. + $rootScope.$watch(function() { + var title = MetaService.getTitle($rootScope.currentPage) || defaultTitle; + $rootScope.title = title; + + var description = MetaService.getDescription($rootScope.currentPage) || ''; + if ($rootScope.description != description) { + $rootScope.description = description; + } + }); + + $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { + $rootScope.current = current.$$route; + $rootScope.currentPage = current; + $rootScope.pageClass = ''; + + if (!current.$$route) { return; } + + var pageClass = current.$$route.pageClass || ''; + if (typeof pageClass != 'string') { + pageClass = pageClass(Features); + } + + $rootScope.pageClass = pageClass; + $rootScope.newLayout = !!current.$$route.newLayout; + $rootScope.fixFooter = !!current.$$route.fixFooter; + + $anchorScroll(); + }); + + var initallyChecked: boolean = false; + (window).__isLoading = function() { + if (!initallyChecked) { + initallyChecked = true; + return true; + } + return $http.pendingRequests.length > 0; + }; + } +} + + +interface QuayRunScope extends ng.IRootScopeService { + currentPage: any; + current: any; + title: any; + description: string, + pageClass: any; + newLayout: any; + fixFooter: any; +} + + +// TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2 +angular + .module(QuayRun.name) + .constant('NAME_PATTERNS', NAME_PATTERNS) + .constant('INJECTED_CONFIG', INJECTED_CONFIG) + .constant('INJECTED_FEATURES', INJECTED_FEATURES) + .constant('INJECTED_ENDPOINTS', INJECTED_ENDPOINTS); \ No newline at end of file diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index e4c6099bf..60dbe5933 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -9,7 +9,8 @@ import { DockerfilePathSelectComponent } from './directives/ui/dockerfile-path-s import { ManageTriggerCustomGitComponent } from './directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component'; import { ManageTriggerGithostComponent } from './directives/ui/manage-trigger-githost/manage-trigger-githost.component'; import { LinearWorkflowComponent } from './directives/ui/linear-workflow/linear-workflow.component'; -import { QuayConfig } from './quay.config'; +import { QuayConfig } from './quay-config.module'; +import { QuayRun } from './quay-run.module'; /** @@ -19,6 +20,7 @@ import { QuayConfig } from './quay.config'; imports: [ QuayRoutes, QuayConfig, + QuayRun, ], declarations: [ RegexMatchViewComponent, @@ -33,129 +35,8 @@ import { QuayConfig } from './quay.config'; }) export class quay { - constructor() { - - } - - public run($rootScope: QuayRunScope, - Restangular: any, - PlanService: any, - $http: ng.IHttpService, - CookieService: any, - Features: any, - $anchorScroll: ng.IAnchorScrollService, - MetaService: any, - INJECTED_CONFIG: any): void { - var defaultTitle = INJECTED_CONFIG['REGISTRY_TITLE'] || 'Quay Container Registry'; - - // Handle session security. - Restangular.setDefaultRequestParams(['post', 'put', 'remove', 'delete'], - {'_csrf_token': (window).__token || ''}); - - // Handle session expiration. - Restangular.setErrorInterceptor(function(response) { - if (response !== undefined && response.status == 503) { - ($('#cannotContactService')).modal({}); - return false; - } - - if (response !== undefined && response.status == 500) { - window.location.href = '/500'; - return false; - } - - if (response !== undefined && !response.data) { - return true; - } - - var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token'; - if (response !== undefined && response.status == 401 && - invalid_token && - response.data['session_required'] !== false) { - ($('#sessionexpiredModal')).modal({}); - return false; - } - - return true; - }); - - // Check if we need to redirect based on a previously chosen plan. - var result = PlanService.handleNotedPlan(); - - // Check to see if we need to show a redirection page. - var redirectUrl = CookieService.get('quay.redirectAfterLoad'); - CookieService.clear('quay.redirectAfterLoad'); - - if (!result && redirectUrl && redirectUrl.indexOf((window).location.href) == 0) { - (window).location = redirectUrl; - return; - } - - $rootScope.$watch('description', function(description: string) { - if (!description) { - description = `Hosted private docker repositories. Includes full user management and history. - Free for public repositories.`; - } - - // Note: We set the content of the description tag manually here rather than using Angular binding - // because we need the tag to have a default description that is not of the form "{{ description }}", - // we read by tools that do not properly invoke the Angular code. - $('#descriptionTag').attr('content', description); - }); - - // Listen for scope changes and update the title and description accordingly. - $rootScope.$watch(function() { - var title = MetaService.getTitle($rootScope.currentPage) || defaultTitle; - $rootScope.title = title; - - var description = MetaService.getDescription($rootScope.currentPage) || ''; - if ($rootScope.description != description) { - $rootScope.description = description; - } - }); - - $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { - $rootScope.current = current.$$route; - $rootScope.currentPage = current; - $rootScope.pageClass = ''; - - if (!current.$$route) { return; } - - var pageClass = current.$$route.pageClass || ''; - if (typeof pageClass != 'string') { - pageClass = pageClass(Features); - } - - $rootScope.pageClass = pageClass; - $rootScope.newLayout = !!current.$$route.newLayout; - $rootScope.fixFooter = !!current.$$route.fixFooter; - - $anchorScroll(); - }); - - var initallyChecked: boolean = false; - (window).__isLoading = function() { - if (!initallyChecked) { - initallyChecked = true; - return true; - } - return $http.pendingRequests.length > 0; - }; - } } - -interface QuayRunScope extends ng.IRootScopeService { - currentPage: any; - current: any; - title: any; - description: string, - pageClass: any; - newLayout: any; - fixFooter: any; -} - - // TODO: Make injected values into services and move to NgModule.providers, as constants are not supported in Angular 2 angular .module(quay.name) From e59d394491a05bb4bb14b7510aa1a28d13755e6d Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Tue, 21 Feb 2017 15:59:26 -0800 Subject: [PATCH 12/29] refactoring linear workflow directives --- .../linear-workflow-section.component.html | 7 ++ .../linear-workflow-section.component.spec.ts | 25 +++++ .../linear-workflow-section.component.ts | 32 +++++++ .../linear-workflow.component.html | 39 ++++++++ .../linear-workflow.component.spec.ts | 19 ++++ .../linear-workflow.component.ts | 48 +++++++++- .../manage-trigger-custom-git.component.html | 86 ++++++++++++++---- .../manage-trigger-githost.component.html | 4 +- static/js/quay.module.ts | 2 + test/data/test.db | Bin 1314816 -> 1314816 bytes 10 files changed, 240 insertions(+), 22 deletions(-) diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html index e69de29bb..0a6560404 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html +++ b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html @@ -0,0 +1,7 @@ +
+ +
+ +
diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts index e69de29bb..11f514f5b 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts @@ -0,0 +1,25 @@ +import { LinearWorkflowSectionComponent } from './linear-workflow-section.component'; +import { LinearWorkflowComponent } from './linear-workflow.component'; +import Spy = jasmine.Spy; + + +describe("LinearWorkflowSectionComponent", () => { + var component: LinearWorkflowSectionComponent; + var parentMock: LinearWorkflowComponent; + + beforeEach(() => { + component = new LinearWorkflowSectionComponent(); + parentMock = new LinearWorkflowComponent(); + component.parent = parentMock; + }); + + describe("$onInit", () => { + + it("calls parent component to add itself as a section", () => { + var addSectionSpy: Spy = spyOn(parentMock, "addSection").and.returnValue(null); + component.$onInit(); + + expect(addSectionSpy.calls.argsFor(0)[0]).toBe(component); + }); + }); +}); diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts index e69de29bb..6a8c6f144 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts @@ -0,0 +1,32 @@ +import { Component, Output, Input } from 'angular-ts-decorators'; +import { LinearWorkflowComponent } from './linear-workflow.component'; + + +/** + * A component which displays a single section in a linear workflow. + */ +@Component({ + selector: 'linearWorkflowSection', + templateUrl: '/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html', + transclude: true, + require: { + parent: '^linearWorkflow' + } +}) +export class LinearWorkflowSectionComponent implements ng.IComponentController { + + @Input('@') public sectionId: string; + @Input('@') public sectionTitle: string; + @Input() public sectionValid: boolean = false; + public sectionVisible: boolean = false; + public isCurrentSection: boolean = false; + public parent: LinearWorkflowComponent; + + constructor() { + + } + + public $onInit(): void { + this.parent.addSection(this); + } +} diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.html b/static/js/directives/ui/linear-workflow/linear-workflow.component.html index e69de29bb..eec4daa80 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.html +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.html @@ -0,0 +1,39 @@ +
+ +
+ +
+ + + + + +
+ + + + +
+ Next: +
    +
  • + {{ section.title }} +
  • +
+
+
+
+
\ No newline at end of file diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts index e69de29bb..34ddedb62 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.spec.ts @@ -0,0 +1,19 @@ +import { LinearWorkflowComponent, SectionInfo } from './linear-workflow.component'; +import { LinearWorkflowSectionComponent } from './linear-workflow-section.component'; + + +describe("LinearWorkflowComponent", () => { + var component: LinearWorkflowComponent; + + beforeEach(() => { + component = new LinearWorkflowComponent(); + }); + + describe("addSection", () => { + + }); + + describe("onNextSection", () => { + + }); +}); diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts index a82e01b54..a3d421051 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts @@ -1,10 +1,56 @@ import { Component, Output, Input } from 'angular-ts-decorators'; +import { LinearWorkflowSectionComponent } from './linear-workflow-section.component'; +/** + * A component that which displays a linear workflow of sections, each completed in order before the next + * step is made visible. + */ @Component({ selector: 'linearWorkflow', - templateUrl: '/static/js/directives/ui/linear-workflow/linear-workflow.component.html' + templateUrl: '/static/js/directives/ui/linear-workflow/linear-workflow.component.html', + transclude: true }) export class LinearWorkflowComponent implements ng.IComponentController { + @Input('@') public doneTitle: string; + @Output() public onWorkflowComplete: (event: any) => void; + private sections: SectionInfo[] = []; + private currentSection: SectionInfo; + + + constructor() { + + } + + public $onInit(): void { + + } + + public addSection(section: LinearWorkflowSectionComponent): void { + this.sections.push({ + index: this.sections.length, + section: section, + }); + } + + public onNextSection(): void { + if (this.currentSection.section.sectionValid) { + if (this.currentSection.index + 1 >= this.sections.length) { + this.onWorkflowComplete({}); + } + else { + this.currentSection = this.sections[this.currentSection.index]; + } + } + } +} + + +/** + * A type representing a section of the linear workflow. + */ +export type SectionInfo = { + index: number; + section: LinearWorkflowSectionComponent; } diff --git a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html index 563ac04f9..a82102122 100644 --- a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html +++ b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html @@ -1,19 +1,18 @@
-
+ -
+

Enter repository

- - Please enter the HTTP or SSH style URL used to clone your git repository: - + Please enter the HTTP or SSH style URL used to clone your git repository: @@ -23,13 +22,14 @@

It is the responsibility of the git repository to invoke a webhook to tell that a commit has been added.

-
+ -
+

Select build context directory

@@ -42,6 +42,56 @@

The build context directory is the path of the directory containing the Dockerfile and any other files to be made available when the build is triggered.

If the Dockerfile is located at the root of the git repository, enter / as the build context directory.

-
-
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html index de8dc9bc9..6c2f2837d 100644 --- a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html +++ b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html @@ -11,9 +11,7 @@ section-valid="$ctrl.local.selectedNamespace">

Select {{ $ctrl.namespaceTitle }}

- - Please select the {{ $ctrl.namespaceTitle }} under which the repository lives - + Please select the {{ $ctrl.namespaceTitle }} under which the repository lives
diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 60dbe5933..f8d54c5a0 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -9,6 +9,7 @@ import { DockerfilePathSelectComponent } from './directives/ui/dockerfile-path-s import { ManageTriggerCustomGitComponent } from './directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component'; import { ManageTriggerGithostComponent } from './directives/ui/manage-trigger-githost/manage-trigger-githost.component'; import { LinearWorkflowComponent } from './directives/ui/linear-workflow/linear-workflow.component'; +import { LinearWorkflowSectionComponent } from './directives/ui/linear-workflow/linear-workflow-section.component'; import { QuayConfig } from './quay-config.module'; import { QuayRun } from './quay-run.module'; @@ -28,6 +29,7 @@ import { QuayRun } from './quay-run.module'; ManageTriggerCustomGitComponent, ManageTriggerGithostComponent, LinearWorkflowComponent, + LinearWorkflowSectionComponent, ], providers: [ ViewArrayImpl, diff --git a/test/data/test.db b/test/data/test.db index 203537eee25924398dc0f5497646dfd60349ae45..415776f78184bea83328ba45eaeaeb7ba35d8714 100644 GIT binary patch delta 806 zcmZ|NJxo(k6bJBo&wKRs^<8>eX^f=usA!EJR9^d9pp{6gBt{crjWNMMLW^2rEJ|XG z_(3RxAu%OF7)}QU1|n|85OW6xHwT9a|eudL;l z!o0qG^b)Vn7hV?lhXSmlX5%m!0qEkx@>BqhBW?F+Iz(-{(FyN$8^;+190*&F@B!Y# zJ2;LrEB5wbc`*WEr27wfJI(Y!Rn3_s!+|89lodUd$;>o( z4-V{3nvkDfz=hpZKN%G`$@^t@1$=^TRl(oB&n*$1=QvQY3}N}K3a?buYLc-8QLvh_ zdkN|mb*=)2{?A731=UeBMORuBk76iZ#Z-KQ1i9N zG;xICB98c*YU))?xnQxIoSTdV8H<FQr8YGZQ<0rnGcy$8j7dwiDY)Y!!9l*zq%V zUd{`oOczxJYNZwysoF}+q7o}4B!sf)o<$dIkl3Uppx5udCPZoIZxekOvJ8_wECCzdpcKfZrV7Ie&A1ssca$!_E8LIq;q1{p&Gs z=kW1sz=Pvldu@OE0C?eW_W=N%|N7$o^ZS=$;MyUGfe$ZLXMZ{Yu*1s};OuL4pR2~< zPd^8Ka%nO5rw@VeA4(5_;-%$#d-`qQ`laRB!}e|9mzS33_NVUxFCOl_3;g+NZh3M4 z)uB|&T-C3&NDeza!zy3a*8?N;oe7A|B9Uf(Cq^BK6E?(0tnju3;+w%8`qAQ zqq72=1+jCX&jQbLXD?lN!NSk%rct9s5yNV#h;&i1-q7n^Mv=Q>Ls4oP-f!z7XK+Sa zmJO*wGmI!HJ-%1LS%YH@N$&t<1f}jG<-2I*0;OLpIJp!$u6b6ir_894^BLyyk3Cbx$^q;CwWcnD>r@x1J?>h@u>LI55D`0 zZyn`5R=oAX4sfx-Xow({GKLRm3LgvyBtnb`!p1P$A#Hm&LdlUk!0%oTmKQ|%qN6bv zYDercI)5SY=2Hc1i6WnRBFium$FzB-TyBMfRaAvjPTr~sK?}9oc1zcS8a&^uHtH%M zCz`u6N?PJ;T8X7WXZHSzZ zq_La|BSFd5mf4?zgg9tqQQ0fCUA|h;XBfFcVTqPW#5SM0n0CBQ&8XNEW}w+hVFNYk2c3~ulQwnI?M<0P)Rt|r%Cs4lNiXkv zYWZ?W9TPaeSd?tAOnEFhB>id9v6eA08_I;$B>D2D*KERkSMU;4v`e2OfG2>D3rafBqCc$V!&qO|UE9hM0 z4Ot6n?H=O#U2S!q!ac(Q9kg-nhGzZ5X}| z%XN0iccvR=WkoJ*f^|t>bm5Xx3$!t%tVYhn8%e#TT(0T{DPyKHUnWR*LGqpYDEdh6v+!?J0ZKx80kRi92R9S@Um$+(s|4~ayI2tVagj&{=i+O?`4A;AbUk35u3 zwx(%3Eg9~I6>zG~x)nqpjGIU)gTsZUgc=@}*UPEbQwLjPzSHEm#y6t&T+W)kN-`2` z8l@~M;#_eu6il#-WLTW`duqvV=^pDw?aCOQ4KtXn64I1wBF$NgSk!Ey&e+;O<@=>o zLvD_3Szg&V9)(~>^;>9Coh+l4nvMk%i50uzND{SavLU^b_`zsG#?IwEGxdHnmr_$7 z2WHw`Gm8!}4>Zt5j0QVGCqxjm<=Pf*@LFm{n$%2V(Gj!!zZwW7ujdp!ZTDg`3}#k$ zJ+F*}#0)$YE`jMtr_x|L?PE&@cIss9L{S>u zM#Hu^o;tD6$TZM1`_g*28i%EFsM~7R*J+(?ORUGwYpt?08PJ0v@3V;866O8?vt<`< z)s@`4t!JQ5Hx&+#EM8LVDz>J0W0q1ap*x+`Dh^FD{mDoe*Bx%q#kr}~vzAh+ zWTEV8+Xu~eY(-eh@CvQF8o#$N3xt;^%+{n3^^Nqvf-Lu7q;Bk>God4y#S|sxf{Xy|S7H@re F<-Z`z7a#xt From b0cc8d0f19c1a9de920f113c82aba5aaeab4bc39 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Tue, 21 Feb 2017 16:21:54 -0800 Subject: [PATCH 13/29] fixed CSS references to element instead of class --- static/css/directives/ui/linear-workflow.css | 20 +++++++++---------- .../linear-workflow.component.html | 2 +- .../linear-workflow.component.ts | 13 ++++++++---- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/static/css/directives/ui/linear-workflow.css b/static/css/directives/ui/linear-workflow.css index b55d10a63..100b7ce33 100644 --- a/static/css/directives/ui/linear-workflow.css +++ b/static/css/directives/ui/linear-workflow.css @@ -1,51 +1,51 @@ -.linear-workflow-section { +linear-workflow-section { margin-bottom: 10px; } -.linear-workflow-section.row { +linear-workflow-section.row { margin-left: 0px; margin-right: 0px; } -.linear-workflow .upcoming-table { +linear-workflow .upcoming-table { vertical-align: middle; margin-left: 20px; } -.linear-workflow .upcoming-table .fa { +linear-workflow .upcoming-table .fa { margin-right: 8px; } -.linear-workflow .upcoming { +linear-workflow .upcoming { color: #888; vertical-align: middle; margin-left: 10px; } -.linear-workflow .upcoming ul { +linear-workflow .upcoming ul { padding: 0px; display: inline-block; margin: 0px; } -.linear-workflow .upcoming li { +linear-workflow .upcoming li { display: inline-block; margin-right: 6px; margin-left: 2px; } -.linear-workflow .upcoming li:after { +linear-workflow .upcoming li:after { content: "•"; display: inline-block; margin-left: 6px; margin-right: 2px; } -.linear-workflow .upcoming li:last-child:after { +linear-workflow .upcoming li:last-child:after { content: ""; } -.linear-workflow .bottom-controls { +linear-workflow .bottom-controls { padding: 10px; } diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.html b/static/js/directives/ui/linear-workflow/linear-workflow.component.html index eec4daa80..4aa3e2150 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.html +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.html @@ -28,7 +28,7 @@
  • - {{ section.title }} + {{ section.component.sectionTitle }}
diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts index a3d421051..bba8cd9b6 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts @@ -27,15 +27,20 @@ export class LinearWorkflowComponent implements ng.IComponentController { } - public addSection(section: LinearWorkflowSectionComponent): void { + public addSection(component: LinearWorkflowSectionComponent): void { this.sections.push({ index: this.sections.length, - section: section, + component: component, }); + + if (this.sections.length == 1) { + this.sections[0].component.sectionVisible = true; + this.currentSection = this.sections[0]; + } } public onNextSection(): void { - if (this.currentSection.section.sectionValid) { + if (this.currentSection.component.sectionValid) { if (this.currentSection.index + 1 >= this.sections.length) { this.onWorkflowComplete({}); } @@ -52,5 +57,5 @@ export class LinearWorkflowComponent implements ng.IComponentController { */ export type SectionInfo = { index: number; - section: LinearWorkflowSectionComponent; + component: LinearWorkflowSectionComponent; } From ff07533d805b74d7df8a0d377dc4db1f78de4039 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Wed, 22 Feb 2017 15:44:20 -0800 Subject: [PATCH 14/29] added tests for linear workflow components --- .../linear-workflow-section.component.html | 4 +- .../linear-workflow-section.component.spec.ts | 57 +++++++++++ .../linear-workflow-section.component.ts | 18 +++- .../linear-workflow.component.html | 14 +-- .../linear-workflow.component.spec.ts | 94 ++++++++++++++++++ .../linear-workflow.component.ts | 39 ++++---- .../manage-trigger-custom-git.component.html | 58 +---------- test/data/test.db | Bin 1314816 -> 1314816 bytes 8 files changed, 198 insertions(+), 86 deletions(-) diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html index 0a6560404..90c0245ba 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html +++ b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html @@ -1,7 +1,7 @@
-
+
diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts index 11f514f5b..d41ecf194 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.spec.ts @@ -22,4 +22,61 @@ describe("LinearWorkflowSectionComponent", () => { expect(addSectionSpy.calls.argsFor(0)[0]).toBe(component); }); }); + + describe("$onChanges", () => { + var onSectionInvalidSpy: Spy; + var changesObj: ng.IOnChangesObject; + + beforeEach(() => { + onSectionInvalidSpy = spyOn(parentMock, "onSectionInvalid").and.returnValue(null); + changesObj = { + sectionValid: { + currentValue: true, + previousValue: false, + isFirstChange: () => false, + }, + }; + }); + + it("does nothing if 'sectionValid' input not changed", () => { + component.$onChanges({}); + + expect(onSectionInvalidSpy).not.toHaveBeenCalled(); + }); + + it("does nothing if 'sectionValid' input is true", () => { + component.$onChanges(changesObj); + + expect(onSectionInvalidSpy).not.toHaveBeenCalled(); + }); + + it("calls parent method to inform that section is invalid if 'sectionValid' input changed to false", () => { + changesObj['sectionValid'].currentValue = false; + component.$onChanges(changesObj); + + expect(onSectionInvalidSpy.calls.argsFor(0)[0]).toEqual(component.sectionId); + }); + }); + + describe("onSubmitSection", () => { + var onNextSectionSpy: Spy; + + beforeEach(() => { + onNextSectionSpy = spyOn(parentMock, "onNextSection").and.returnValue(null); + }); + + it("does nothing if section is invalid", () => { + component.sectionValid = false; + component.onSubmitSection(); + + expect(onNextSectionSpy).not.toHaveBeenCalled(); + }); + + it("calls parent method to go to next section if section is valid", () => { + component.sectionValid = true; + component.onSubmitSection(); + + expect(onNextSectionSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts index 6a8c6f144..81e2c04ba 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow-section.component.ts @@ -10,7 +10,7 @@ import { LinearWorkflowComponent } from './linear-workflow.component'; templateUrl: '/static/js/directives/ui/linear-workflow/linear-workflow-section.component.html', transclude: true, require: { - parent: '^linearWorkflow' + parent: '^^linearWorkflow' } }) export class LinearWorkflowSectionComponent implements ng.IComponentController { @@ -22,11 +22,19 @@ export class LinearWorkflowSectionComponent implements ng.IComponentController { public isCurrentSection: boolean = false; public parent: LinearWorkflowComponent; - constructor() { - - } - public $onInit(): void { this.parent.addSection(this); } + + public $onChanges(changes: ng.IOnChangesObject): void { + if (changes['sectionValid'] !== undefined && !changes['sectionValid'].currentValue) { + this.parent.onSectionInvalid(this.sectionId); + } + } + + public onSubmitSection(): void { + if (this.sectionValid) { + this.parent.onNextSection(); + } + } } diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.html b/static/js/directives/ui/linear-workflow/linear-workflow.component.html index 4aa3e2150..bbb0d3868 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.html +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.html @@ -8,15 +8,15 @@
+ ng-if="$ctrl.currentSection.index < $ctrl.sections.length - 1"> Next:
  • { new LinearWorkflowSectionComponent(), ]; sections.forEach((section) => { - section.sectionVisible = true; - section.isCurrentSection = true; + section.sectionVisible = false; + section.isCurrentSection = false; component.addSection(section); }); }); + it("does nothing if invalid section is after the current section", () => { + sections[sections.length - 1].sectionValid = false; + sections[sections.length - 1].sectionId = "Some Section"; + component.onSectionInvalid(sections[sections.length - 1].sectionId); + + expect(sections[sections.length - 1].isCurrentSection).toBe(false); + expect(sections[sections.length - 1].sectionVisible).toBe(false); + }); + it("sets the section with the given id to be the current section", () => { component.onSectionInvalid(invalidSection.sectionId); @@ -102,6 +111,11 @@ describe("LinearWorkflowComponent", () => { }); it("hides all sections after the section with the given id", () => { + sections.forEach((section) => { + section.sectionVisible = true; + section.isCurrentSection = true; + component.addSection(section); + }); component.onSectionInvalid(invalidSection.sectionId); sections.forEach((section) => { diff --git a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts index 69e43dc20..960289c15 100644 --- a/static/js/directives/ui/linear-workflow/linear-workflow.component.ts +++ b/static/js/directives/ui/linear-workflow/linear-workflow.component.ts @@ -45,14 +45,16 @@ export class LinearWorkflowComponent implements ng.IComponentController { public onSectionInvalid(sectionId: string): void { var invalidSection = this.sections.filter(section => section.component.sectionId == sectionId)[0]; - invalidSection.component.isCurrentSection = true; - this.currentSection = invalidSection; - this.sections.forEach((section) => { - if (section.index > invalidSection.index) { - section.component.sectionVisible = false; - section.component.isCurrentSection = false; - } - }); + if (invalidSection.index <= this.currentSection.index) { + invalidSection.component.isCurrentSection = true; + this.currentSection = invalidSection; + this.sections.forEach((section) => { + if (section.index > invalidSection.index) { + section.component.sectionVisible = false; + section.component.isCurrentSection = false; + } + }); + } } } diff --git a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html index d75adcb07..43cb23b97 100644 --- a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html +++ b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html @@ -2,7 +2,7 @@ + on-workflow-complete="$ctrl.activateTrigger({'config': $ctrl.config})"> { + var component: ManageTriggerCustomGitComponent; + + beforeEach(() => { + component = new ManageTriggerCustomGitComponent(); + }); + + describe("$onChanges", () => { + + }); +}); diff --git a/test/data/test.db b/test/data/test.db index cfce4f8698f2c58334b74b10f740a96982258200..c70eb4c9ceae00ef2f559ac05c0e392abc9673fe 100644 GIT binary patch delta 3830 zcmeH}Nvz}cea9uf*`D5;;fyoz95V5HeVJ^~%!m^Ai5(9qk`fnj6{#g@Jt8Slq9~H0 zL@jg&niL2O1To;Y2L(+JKD00j^iZHEbjUpja`nll+)@+)Qlo%l6rMvl;{Yw-^yF&@ zd@lU0{QZC6|NHEH_1XLCdtWVn_}6#q#See@IdC7meCg=8c(#A50{mm1 z#NJDbB;Pl?% z^%(fg|LgxE&tDyW@t45&UIg9QUwsCAyLjjD@EP#$Cwt;20Ql+`Yn)#yQv2fH19uPI zp8)@y$DMq%zxp2VE#Ty%!^8K0e>&Me{~2)eD_;n_clJ(h$T`Znlyf=fO3u}sYdOa` zrNi@|om@k%0N|Yh_|wDrdncdX_}lZxukLn0sqp3xuNSG}wE~?^0{6U~OgwLxJUVyM z#WI>b@Fq)1TS%#8B=eI7DZr9MHh61;|)^cNP$r5RmD&wxh`P6 z4q>T^0DSR4h=*hq$8mrF_CNjgNpKAS(*pQ&FwGBf0`C9i*{#dem#(`1`G219dFD?F z;3wc`hg%;X{l_2n=f9QH>mOWs4Bou*X#xBceE!1rTky`Mqe2llIyyeSUMLiRD>n<^ zyH{@Bc%JtJTmkK-?~4Li4J>Q1q$Z zkxI%+^|<+lTt~Pn^<%8Rp4)hJF7kdSTJ(0NHC9uHx2H2EVme-#8MhI5G)=mOOkg!A z-E2ynRq3e%soxGf!xtr=aH_7~+E%J##pc#>rEAxiRG(L|F|yLwbl)t@<6y80}^)Z>%vIl+L$vwc@f%zP6pAo2ZMhZKW!3 z1Ik`7rDn4}npH86^RkAPc5QOg;bh&SjRsfdd(mvmPSZrFX+wJ4#skKjMNoMYaMWp44w0*Co}n~~rlNe_v>y;Is>zio zU^gLOGG}9jx5wVd;g*4_yGUqIWU5NgI*YXW+F!~&#&-;yj!o3_Cb+)wWW3qJMxlW5 zkT@tMNSW>oMUT~n3xSuG{l#{^*{z#v9+K4wug;pPm8mW6dqAvy}Lj;r~nuyw*$ut{Uk&1{UC%4P%5d)_h z-7J@|bViJ7qs4Z!*64)v#_%p!!6xH(W>G>yN<%lbHbICjuFnUH4YhLBWHC_ z$(C9wOE7CdY~Aaz6wLWM7g^8CFpEL>AT-e-Kgnvf;o6KG1EFRe6(czduQ`J(9gR@uWzXnL zXG=pa>&;u+5nq)QmX)THuuI1XX~x5Nh*l-2o2j|4@YIS2$tZ%^1dquB8sgHFm9;h;N7{H4GaG~#Bz(mo zRtd|>1lqzEu-R}zKUi?OxwVEQp%QKl9i~-OX8bwv`JC68qGY44ML~ixowof2ZEB@O zyiUvN&dOSfuanHMx{wHF-JMP8QAJ!DtWd+$1~wH?-?X4itBV>=MAb$E51mabMOIX4 zvh2yNa#@1xm>hLhVN>5m3LE&SRvp#Dsx40&HG8t@peeQ?T1HocsB{_28>7!omDM;c zc|CY%FXV>Y%Vs?z)~zPNdkqfC2Tdy)w!|QEC}1>$TjiwD?MPYVw*)-jjY#WHG%@o^ z_&~D;$&BlG9ila|ptY6Bn8>B|t%&s741?&i?V9S+hLfQ|uvG(xi$YCC*XtoI$xWrS zbGBGdiPP;Ov!^3>j?J4!tCG#KaFZ|$>X^Zlbh9*wNjiZ=bu0H2A0N+zCeF&pqL*1E zNMF}9O{6`I;b)?&P6z#dd0h4CWx3gQ8=JWs%tF@@%=R2v>qZ%ERJ)B)CMaH5A1XV= zDNXFk9QCcTPwLHi5;JaB>2Ikr(oQ3Tiz_Q{u-O__zTB>NWT$O)CE5rhm6O)}5#A-~ zZD~T;^~9+Oks_NI8t5Hk0uPd^!1T=qXE9p2BGsrBo~|V!6s7K*?Sz8lz41or!GpW` zEyK#pAH93t-bba|KiDhJqSF27GP2|C3GuAt78C{Q4qo;WCfI$>a_1&_uba^;SYy)qy zR+se1Xc;IST5Lt>PTgcyt(8fY4vQ|j6im9So`N=#?z}l68_~cTPOY>TV>~q{p>`a! zakw1`nX2}-keOI5NK^@RKn8tFDEGE?j*=MzOT?%`LI&j9q8&EIBTsMP=G2Plblq|s zp=@{OREKHx>*&s1!CcR*awf<3y2yAkv3bM57Ga-88l0M%vN7=6c*E(s%(z@9JZ{-k zLpRGDE#_vGCXKHKTBeD{swZNJ)EStx*7e}-#nb!eZ(oFi+3@1g`9)`FkE3XM&W3Ir zO~U25sY@64&o5SkU68)?7%!Lcqe|r)HDno0hvB0yD{dYKkuwO!(PH@sg&?T1{=9=l zB~BOP%JOp!SE=HAuX1|vy^E)Ro_Axv(06YwqBL<{_LM8T#{+Nj-NC{ixsSdZ45t3V z9lhv#k=pJgdGTeHi>JWh-OB0q@wMLik>dO z6l;`lS4f61tLH-)JB*Lf#yr&d^pYSi@=iJ_-o+KXiAy>x!T5idpmBm8T!p5Y zSGP$0zo4a zY7!o8=w?wpEa^<7ejo^=p4JpI$aoAgJ`*rQI;|aX5@p#Pp+2ewXV&iUF4?+cAH5+D E0PyRn^Z)<= From 8fcd76c0bebf6903756fbbc043cff748edddba55 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Wed, 22 Feb 2017 17:22:34 -0800 Subject: [PATCH 16/29] removed old templates --- .../directives/linear-workflow-section.html | 6 - static/directives/linear-workflow.html | 31 ---- static/js/directives/ui/linear-workflow.js | 141 ------------------ .../manage-trigger-custom-git.component.html | 1 - .../manage-trigger-githost.component.html | 129 +++++++++------- .../manage-trigger-githost.component.spec.ts | 10 +- .../manage-trigger-githost.component.ts | 15 +- test/data/test.db | Bin 1314816 -> 1323008 bytes 8 files changed, 87 insertions(+), 246 deletions(-) delete mode 100644 static/directives/linear-workflow-section.html delete mode 100644 static/directives/linear-workflow.html delete mode 100644 static/js/directives/ui/linear-workflow.js diff --git a/static/directives/linear-workflow-section.html b/static/directives/linear-workflow-section.html deleted file mode 100644 index 7eeafcf63..000000000 --- a/static/directives/linear-workflow-section.html +++ /dev/null @@ -1,6 +0,0 @@ -
    -
    -
    - -
    \ No newline at end of file diff --git a/static/directives/linear-workflow.html b/static/directives/linear-workflow.html deleted file mode 100644 index b1f366f93..000000000 --- a/static/directives/linear-workflow.html +++ /dev/null @@ -1,31 +0,0 @@ -
    - -
    - -
    - - - - - -
    - - - - -
    - Next: -
      -
    • - {{ section.title }} -
    • -
    -
    -
    -
    -
    \ No newline at end of file diff --git a/static/js/directives/ui/linear-workflow.js b/static/js/directives/ui/linear-workflow.js deleted file mode 100644 index 1f24adeb5..000000000 --- a/static/js/directives/ui/linear-workflow.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * An element which displays a linear workflow of sections, each completed in order before the next - * step is made visible. - */ -angular.module('quay').directive('linearWorkflow', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/linear-workflow.html', - replace: false, - transclude: true, - restrict: 'C', - scope: { - 'workflowState': '=?workflowState', - 'workflowComplete': '&workflowComplete', - 'doneTitle': '@doneTitle' - }, - controller: function($scope, $element, $timeout) { - $scope.sections = []; - - $scope.nextSection = function() { - if (!$scope.currentSection.valid) { return; } - - var currentIndex = $scope.currentSection.index; - if (currentIndex + 1 >= $scope.sections.length) { - $scope.workflowComplete(); - return; - } - - $scope.workflowState = $scope.sections[currentIndex + 1].id; - }; - - this.registerSection = function(sectionScope, sectionElement) { - // Add the section to the list. - var sectionInfo = { - 'index': $scope.sections.length, - 'id': sectionScope.sectionId, - 'title': sectionScope.sectionTitle, - 'scope': sectionScope, - 'element': sectionElement - }; - - $scope.sections.push(sectionInfo); - - // Add a watch on the `sectionValid` value on the section itself. If/when this value - // changes, we copy it over to the sectionInfo, so that the overall workflow can watch - // the change. - sectionScope.$watch('sectionValid', function(isValid) { - sectionInfo['valid'] = isValid; - if (!isValid) { - // Reset the sections back to this section. - updateState(); - } - }); - - // Bind the `submitSection` callback to move to the next section when the user hits - // enter (which calls this method on the scope via an ng-submit set on a wrapping - //
    tag). - sectionScope.submitSection = function() { - $scope.nextSection(); - }; - - // Update the state of the workflow to account for the new section. - updateState(); - }; - - var updateState = function() { - // Find the furthest state we can show. - var foundIndex = 0; - var maxValidIndex = -1; - - $scope.sections.forEach(function(section, index) { - if (section.id == $scope.workflowState) { - foundIndex = index; - } - - if (maxValidIndex == index - 1 && section.valid) { - maxValidIndex = index; - } - }); - - var minSectionIndex = Math.min(maxValidIndex + 1, foundIndex); - $scope.sections.forEach(function(section, index) { - section.scope.sectionVisible = index <= minSectionIndex; - section.scope.isCurrentSection = false; - }); - - $scope.workflowState = null; - if (minSectionIndex >= 0 && minSectionIndex < $scope.sections.length) { - $scope.currentSection = $scope.sections[minSectionIndex]; - $scope.workflowState = $scope.currentSection.id; - $scope.currentSection.scope.isCurrentSection = true; - - // Focus to the first input (if any) in the section. - $timeout(function() { - var inputs = $scope.currentSection.element.find('input'); - if (inputs.length == 1) { - inputs.focus(); - } - }, 10); - } - }; - - $scope.$watch('workflowState', updateState); - } - }; - return directiveDefinitionObject; -}); - -/** - * An element which displays a single section in a linear workflow. - */ -angular.module('quay').directive('linearWorkflowSection', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/linear-workflow-section.html', - replace: false, - transclude: true, - restrict: 'C', - require: '^linearWorkflow', - scope: { - 'sectionId': '@sectionId', - 'sectionTitle': '@sectionTitle', - 'sectionValid': '=?sectionValid', - }, - - link: function($scope, $element, $attrs, $ctrl) { - $ctrl.registerSection($scope, $element); - }, - - controller: function($scope, $element) { - $scope.$watch('sectionVisible', function(visible) { - if (visible) { - $element.show(); - } else { - $element.hide(); - } - }); - } - }; - return directiveDefinitionObject; -}); \ No newline at end of file diff --git a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html index 43cb23b97..a4bf31b72 100644 --- a/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html +++ b/static/js/directives/ui/manage-trigger-custom-git/manage-trigger-custom-git.component.html @@ -1,6 +1,5 @@
    diff --git a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html index 6c2f2837d..b3fd10bd7 100644 --- a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html +++ b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.html @@ -1,15 +1,15 @@
    -
    + -
    -
    + +

    Select {{ $ctrl.namespaceTitle }}

    Please select the {{ $ctrl.namespaceTitle }} under which the repository lives @@ -48,7 +48,9 @@
- {{ namespace.id }} + {{ namespace.id }}
-
No matching {{ $ctrl.namespaceTitle }} found.
Try expanding your filtering terms.
-
+
Retrieving {{ $ctrl.namespaceTitle }}s
- + -
+ -
+

Select Repository

Select a repository in @@ -131,12 +134,16 @@ - - {{ repository.name }} + {{ repository.name }} @@ -147,17 +154,20 @@ -
No matching repositories found.
Try expanding your filtering terms.
-
+
Retrieving repositories
- + -
-
+ +

Configure Trigger

Configure trigger options for @@ -183,7 +193,9 @@
@@ -198,7 +210,9 @@ Regular Expression: - +
Examples: heads/master, tags/tagname, heads/.+
-
+
Retrieving repository refs
-
+ -
-
+ +
{{ $ctrl.local.dockerfileLocations.message }}
-
+

Select Dockerfile

Please select the location of the Dockerfile to build when this trigger is invoked @@ -246,22 +262,24 @@ is-valid-path="$ctrl.local.hasValidDockerfilePath">
-
+
Retrieving Dockerfile locations
-
+ -
+ -
+

Verification Error

There was an error when verifying the state of @@ -272,19 +290,19 @@
-
+

Verification Warning

{{ $ctrl.local.triggerAnalysis.message }}
-
+

Ready to go!

Click "Create Trigger" to complete setup of this build trigger
-
+

Robot Account Required

The selected Dockerfile in the selected repository depends upon a private base image

A robot account with access to the base image is required to setup this trigger, but you are not the administrator of this namespace.

@@ -292,7 +310,7 @@
-
+

Select Robot Account

The selected Dockerfile in the selected repository depends upon a private base image. Select a robot account with access: @@ -324,7 +342,9 @@ ng-class="$ctrl.local.robotAccount == robot ? 'checked' : ''" bindonce> - + @@ -348,6 +368,7 @@

In order for the to pull the base image during the build process, a robot account with access must be selected.

Robot accounts that already have access to this base image are listed first. If you select a robot account that does not currently have access, read permission will be granted to that robot account on trigger creation.

-
+ -
\ No newline at end of file + +
\ No newline at end of file diff --git a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.spec.ts b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.spec.ts index d00a3f020..547781352 100644 --- a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.spec.ts +++ b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.spec.ts @@ -27,15 +27,11 @@ describe("ManageTriggerGithostComponent", () => { })); describe("constructor", () => { - + // TODO }); describe("$onInit", () => { - - }); - - describe("$onChanges", () => { - + // TODO }); describe("getTriggerIcon", () => { @@ -48,6 +44,6 @@ describe("ManageTriggerGithostComponent", () => { }); describe("createTrigger", () => { - + // TODO }); }); \ No newline at end of file diff --git a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts index a6815d8ad..a28f1e2f4 100644 --- a/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts +++ b/static/js/directives/ui/manage-trigger-githost/manage-trigger-githost.component.ts @@ -11,6 +11,7 @@ import * as moment from 'moment'; }) export class ManageTriggerGithostComponent implements ng.IComponentController { + // FIXME: Use one-way data binding @Input('=') public repository: any; @Input('=') public trigger: Trigger; @Output() public activateTrigger: (trigger: {config: any, pull_robot: any}) => void; @@ -101,10 +102,6 @@ export class ManageTriggerGithostComponent implements ng.IComponentController { this.$scope.$watch(() => this.local.robotOptions.filter, this.buildOrderedRobotAccounts); } - public $onChanges(changes: ng.IOnChangesObject): void { - - } - public getTriggerIcon(): any { return this.TriggerService.getIcon(this.trigger.service); } @@ -323,6 +320,9 @@ export class ManageTriggerGithostComponent implements ng.IComponentController { } +/** + * A type representing local application data. + */ export type Local = { namespaceOptions: { filter: string; @@ -343,10 +343,13 @@ export type Local = { reverse: boolean; page: number; }; -} +}; +/** + * A type representing a trigger. + */ export type Trigger = { id: number; service: any; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/test/data/test.db b/test/data/test.db index c70eb4c9ceae00ef2f559ac05c0e392abc9673fe..dc4f5960a4ca8e6d01970518b5266faa231d986f 100644 GIT binary patch delta 3617 zcmeHJOROa4Rqn37kGXa4ow}1;Vj;(I#~zGPGgI%Eg=|+>S9MiCs;eLMOomK%*Q2Ye zs~^?XuM5_lNPrc|Q5-gfY=k1jV;Q7~X(=|=2^2v-)Q7$TX;5pA zyngUIL`wxIe?Q%{fNvIx7cZx$D)1U`@$&cXz5c`VwE9)x>K^?H!oGr|4@rtb2&DL> zLi+p!cy8}~75JS(p?LmjdiogndEorh{lmw=U!JF5oB;QrpWy5><3DE37f0#o9H4=V zqy4LM;LrbO=YzuU?*HOffmaXmAEl>HfL{R4KiVTt4vc5s0zP;B6Y|BQJeN<=f(GIq0{bXYTG7^Lv;1?-gHva(VkJ1u#c{{W8Ce7*{v)se!8zzppcMpX<@+*} z{?U_r$KwAsa6vfA&XvA*di{F({rAsnS(Nv4(7!4Uk7V z&prRv`ThT1hX1@7=`WvMOD{4k?Gf|-#q|B->#yAMyKTvrlV-PdvsIXhg5anXXps`lu+=sy z%|_jN6+xA*>+Z<;)SK0U)xZ!`!WhAw>cCW`+tlQ8#HHg?6g17?gvZ^~{m(5wJ&e=h`;zu2-T1U3FG!$!AEuhDyi#8Do z?=4Sbfw1drr+6AyMt=OGDy>v5LhnX$YHmHI>=0fbz)b-%3e4djx@T? zmS+>5B}SgWQp=^Xp6ueDm>BUa98#mG-<|hZ-Kj0@po5sy8lJVLhQ{bU6-`_4#TU{) zgRXz+Eb1>4H-6!bH})?cU;iz5ahbIkaB`J{zIk$$RsQH?fA!ME2WQ~HocS({OU@YMd zSg$`IhpG6|#r@maMlN$)o#)yDw_JKNzR?rLOQz@5dXB;x@}xGl7fQUe*9%z-Mv2nX zrqkuBZ{rjXT5*JqDrnRdv1ZruT2Lut&k&Xp*sqarqRL6U4yq)1d1m;rIjE(tHmsu z%{ZwLm`T#(TMC4kYB(&1(Kf)ul{sS79XH(7gmq#%^|FV$&2{3NWL=W=)^6SI*V=1W zHn*x9H!4{c7YMi==nJYQwPtn{E;Lv-v(BXV^t#(Zk8e>i6pnC!7R!$j$vS;c!|D zZ&vt4(2e!3Oi^ID4+oaF@yxXyYUNdFH`O$)YOR}%U82{=7>YMa*n(O&cW6r)@O*iz zO4y=m`L}H;p7us6C^8}uhbF&KLVep#&~dON-5M-5HJ{vBoXAIgPAW~i<1XHDtKA)5 zX1tLMcenEe4l4^E#6)pZ(r~ArO^WgyZL(O6te7QEk(Le5uGG5x(rA?yQ%Q{Hr54>7 zIyY2jdfSxZv7qzGG6Fk&Wr>B3FyER^0E_0x8;0whwu@%4Vvl7h<~V7NMK{*?rXHAm z#alFumb+tk6z|42<|@kqGj+ED?^^#*MXZS} z-{zuQC*o9WDK1N;M!62sXlFV?>{TdavjCPtZ?3Z&eK>IeJjV(?k zJDq-H;C-nltl$kN=+p2pG0 zvZ;+bTV5E2NTu#N4UmvwX3A|f4VTA6Hh|mgwzBT)W=-<&F$>S8WqE^Rv00k5=Qp>+ zMyyMg?!^9hTAK)E+OWWxqd0M3h&#WeQUVfpctY~)dX=-Avftx)9c}Qvm6qk16ByH% zsYyf$l&8u!I=SoBH(;;Zo6W|W(cW$iHsp|%Mv|Pe*a`Hir;FXG?nLETVyH;#7QtE` zv)&nTWcS>`Le=F4nztS99jYE1q0x+XfB z=~ZOBW4Ez{>YGM97bDQ_dqb1Kredsln?|d~Mz|lg>t(-PF*ozAvDqvGgp_W}l8gE- z9tpvmWDI>piJ>kF<3>dSf^-i-%6EOBYl>zYm_u|>W# zNSYB=t<}{DzOL8ToqAFMhj!buH}Ouj*>MRS)AXQ`$V)sdhb_|YN^-5C5x0ovjyh$y zZma!hWTD1rD0!=JK#<&W6W`Vm6Ry)UgyK=p>73!$A1SsEa9lLX1lA z)|n92wpFIhiav5%_W18#&l>- zJx`p5h{VS_ybK4t+GLBH l2X%Vs>~^x$10NhdL8+wbOBX+rzH@nv|Nryj|G@L3@b4&;Rb~JH delta 709 zcmY+CKWGzS7{=g~lRE@ecthE{YM9 z3|gAt@CpIVAP7!^*bj9TCtb`S4q7*@4hEqYXfpU-4yz8o;rpKFeV*@yulA)>YnK{h zT1Nn4HVO_@C=Bx?X@f&o2maO3sAc$4ht~&){iXkoh7^ z{8NO=Ub;FDYic^iFDXIcnO9U|bYKx&I(!jOo?CBa2md{Z|bFn)1yxm<=qWWYplG%-2Fomi#*~LbF=Zku!JU9b4>=WHbUx z18xN=Q?${7#L!aTX#t}$5k_Nl#$ck1$yjoyWm`vdz)^wsWu{^G91cloH^34FdidE+ zQGU~Y9|c?$_zG8f(-owrHBX~WBUz-EKkbt&ZVTMTzVxc*ccjuhM0AL#ifL^WQFry8 zz#a~y*HwSuXhanXsH$nkg%F^-0@rlc8E`P@(oH41reg&P`T_^&qc6R-x)r7-hJ&FT zRtBYQ^(zj)z#a%&Lj$)Cxc67kyM?m16pOpoZY*E_7c0!&Rv-Ie>^H3Y>oF$II81PM i8IKu}{(8cxI+~Uhu}Sq|~wi From 942d71f95df7d927f1c2dbf59ab30c659b3775d9 Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Thu, 23 Feb 2017 12:39:58 -0800 Subject: [PATCH 17/29] upgraded TypeScript version --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e26efe744..dbc5c0019 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@types/angular": "1.5.16", "@types/angular-mocks": "^1.5.8", "@types/angular-route": "^1.3.3", + "@types/es6-shim": "^0.31.32", "@types/jasmine": "^2.5.41", "@types/react": "0.14.39", "@types/react-dom": "0.14.17", @@ -57,8 +58,8 @@ "sass-loader": "4.0.2", "source-map-loader": "0.1.5", "style-loader": "0.13.1", - "ts-loader": "0.9.5", - "typescript": "2.2.1", + "ts-loader": "^0.9.5", + "typescript": "^2.2.1", "typings": "1.4.0", "webpack": "^2.2" } From a5fc7cba5ff132b9a9c31dde12ff19d1b534e7df Mon Sep 17 00:00:00 2001 From: alecmerdler Date: Mon, 27 Feb 2017 12:36:46 -0800 Subject: [PATCH 18/29] use minified AngularJS --- buildtrigger/githubhandler.py | 2 +- external_libraries.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildtrigger/githubhandler.py b/buildtrigger/githubhandler.py index fa31f21d1..99f90bdd5 100644 --- a/buildtrigger/githubhandler.py +++ b/buildtrigger/githubhandler.py @@ -11,13 +11,13 @@ from github import (Github, UnknownObjectException, GithubException, from jsonschema import validate +from app import github_trigger from buildtrigger.triggerutil import (RepositoryReadException, TriggerActivationException, TriggerDeactivationException, TriggerStartException, EmptyRepositoryException, ValidationRequestException, SkipRequestException, InvalidPayloadException, determine_build_ref, raise_if_skipped_build, find_matching_branches) - from buildtrigger.basehandler import BuildTriggerHandler from endpoints.exception import ExternalServiceError from util.security.ssh import generate_ssh_keypair diff --git a/external_libraries.py b/external_libraries.py index ef1a9d03f..f2f9c3832 100644 --- a/external_libraries.py +++ b/external_libraries.py @@ -7,7 +7,7 @@ LOCAL_DIRECTORY = '/static/ldn/' EXTERNAL_JS = [ 'code.jquery.com/jquery.js', 'netdna.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js', - 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.js', + 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-route.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-sanitize.min.js', 'ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular-animate.min.js', From cfe231f618ac0a0b97adc94e68cb061f7f0fd9bb Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 13 Feb 2017 13:22:59 -0500 Subject: [PATCH 19/29] Add unit testing of custom trigger handler --- buildtrigger/customhandler.py | 10 ++--- buildtrigger/test/test_customhandler.py | 51 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 buildtrigger/test/test_customhandler.py diff --git a/buildtrigger/customhandler.py b/buildtrigger/customhandler.py index 7541995a5..193445ee2 100644 --- a/buildtrigger/customhandler.py +++ b/buildtrigger/customhandler.py @@ -16,9 +16,6 @@ from buildtrigger.bitbuckethandler import (BITBUCKET_WEBHOOK_PAYLOAD_SCHEMA as b from buildtrigger.githubhandler import (GITHUB_WEBHOOK_PAYLOAD_SCHEMA as gh_schema, get_transformed_webhook_payload as gh_payload) -from buildtrigger.bitbuckethandler import (BITBUCKET_WEBHOOK_PAYLOAD_SCHEMA as bb_schema, - get_transformed_webhook_payload as bb_payload) - from buildtrigger.gitlabhandler import (GITLAB_WEBHOOK_PAYLOAD_SCHEMA as gl_schema, get_transformed_webhook_payload as gl_payload) @@ -162,7 +159,7 @@ class CustomBuildTrigger(BuildTriggerHandler): def handle_trigger_request(self, request): payload = request.data if not payload: - raise InvalidPayloadException() + raise InvalidPayloadException('Missing expected payload') logger.debug('Payload %s', payload) @@ -186,7 +183,10 @@ class CustomBuildTrigger(BuildTriggerHandler): 'git_url': config['build_source'], } - return self.prepare_build(metadata, is_manual=True) + try: + return self.prepare_build(metadata, is_manual=True) + except ValidationError as ve: + raise TriggerStartException(ve.message) def activate(self, standard_webhook_url): config = self.config diff --git a/buildtrigger/test/test_customhandler.py b/buildtrigger/test/test_customhandler.py new file mode 100644 index 000000000..6d05cb2b9 --- /dev/null +++ b/buildtrigger/test/test_customhandler.py @@ -0,0 +1,51 @@ +import pytest + +from buildtrigger.customhandler import CustomBuildTrigger +from buildtrigger.triggerutil import (InvalidPayloadException, SkipRequestException, + TriggerStartException) +from endpoints.building import PreparedBuild +from util.morecollections import AttrDict + +@pytest.mark.parametrize('payload, expected_error, expected_message', [ + ('', InvalidPayloadException, 'Missing expected payload'), + ('{}', InvalidPayloadException, "'commit' is a required property"), + + ('{"commit": "foo", "ref": "bar", "default_branch": "baz"}', + InvalidPayloadException, "u'foo' does not match '^([A-Fa-f0-9]{7,})$'"), + + ('{"commit": "11d6fbc", "ref": "refs/heads/something", "default_branch": "baz"}', None, None), + ('''{ + "commit": "11d6fbc", + "ref": "refs/heads/something", + "default_branch": "baz", + "commit_info": { + "message": "[skip build]", + "url": "http://foo.bar", + "date": "NOW" + } + }''', SkipRequestException, ''), +]) +def test_handle_trigger_request(payload, expected_error, expected_message): + trigger = CustomBuildTrigger(None, {'build_source': 'foo'}) + request = AttrDict(dict(data=payload)) + + if expected_error is not None: + with pytest.raises(expected_error) as ipe: + trigger.handle_trigger_request(request) + assert ipe.value.message == expected_message + else: + assert isinstance(trigger.handle_trigger_request(request), PreparedBuild) + +@pytest.mark.parametrize('run_parameters, expected_error, expected_message', [ + ({}, TriggerStartException, 'missing required parameter'), + ({'commit_sha': 'foo'}, TriggerStartException, "'foo' does not match '^([A-Fa-f0-9]{7,})$'"), + ({'commit_sha': '11d6fbc'}, None, None), +]) +def test_manual_start(run_parameters, expected_error, expected_message): + trigger = CustomBuildTrigger(None, {'build_source': 'foo'}) + if expected_error is not None: + with pytest.raises(expected_error) as ipe: + trigger.manual_start(run_parameters) + assert ipe.value.message == expected_message + else: + assert isinstance(trigger.manual_start(run_parameters), PreparedBuild) From c4f873ae969faf002f2b6a0f2f00756de6d0c643 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 13 Feb 2017 13:23:07 -0500 Subject: [PATCH 20/29] Add unit testing of github trigger handler --- buildtrigger/githubhandler.py | 44 ++- buildtrigger/test/test_githubhandler.py | 361 ++++++++++++++++++++++++ 2 files changed, 392 insertions(+), 13 deletions(-) create mode 100644 buildtrigger/test/test_githubhandler.py diff --git a/buildtrigger/githubhandler.py b/buildtrigger/githubhandler.py index 99f90bdd5..a51526507 100644 --- a/buildtrigger/githubhandler.py +++ b/buildtrigger/githubhandler.py @@ -261,13 +261,14 @@ class GithubBuildTrigger(BuildTriggerHandler): raise TriggerDeactivationException(msg) # Remove the webhook. - try: - hook = repo.get_hook(config['hook_id']) - hook.delete() - except GithubException as ghe: - default_msg = 'Unable to remove hook: %s' % config['hook_id'] - msg = GithubBuildTrigger._get_error_message(ghe, default_msg) - raise TriggerDeactivationException(msg) + if 'hook_id' in config: + try: + hook = repo.get_hook(config['hook_id']) + hook.delete() + except GithubException as ghe: + default_msg = 'Unable to remove hook: %s' % config['hook_id'] + msg = GithubBuildTrigger._get_error_message(ghe, default_msg) + raise TriggerDeactivationException(msg) config.pop('hook_id', None) self.config = config @@ -315,12 +316,14 @@ class GithubBuildTrigger(BuildTriggerHandler): gh_client = self._get_client() usr = gh_client.get_user() - if namespace == usr.login: return [repo_view(repo) for repo in usr.get_repos() if repo.owner.login == namespace] - org = gh_client.get_organization(namespace) - if org is None: + try: + org = gh_client.get_organization(namespace) + if org is None: + return [] + except GithubException: return [] return [repo_view(repo) for repo in org.get_repos(type='member')] @@ -479,8 +482,11 @@ class GithubBuildTrigger(BuildTriggerHandler): raise TriggerStartException(msg) def get_branch_sha(branch_name): - branch = repo.get_branch(branch_name) - return branch.commit.sha + try: + branch = repo.get_branch(branch_name) + return branch.commit.sha + except GithubException: + raise TriggerStartException('Could not find branch in repository') def get_tag_sha(tag_name): tags = {tag.name: tag for tag in repo.get_tags()} @@ -515,9 +521,21 @@ class GithubBuildTrigger(BuildTriggerHandler): # This is for GitHub's probing/testing. if 'zen' in payload: - raise ValidationRequestException() + raise SkipRequestException() # Lookup the default branch for the repository. + if 'repository' not in payload: + raise ValidationRequestException("Missing 'repository' on request") + + if 'owner' not in payload['repository']: + raise ValidationRequestException("Missing 'owner' on repository") + + if 'name' not in payload['repository']['owner']: + raise ValidationRequestException("Missing owner 'name' on repository") + + if 'name' not in payload['repository']: + raise ValidationRequestException("Missing 'name' on repository") + default_branch = None lookup_user = None try: diff --git a/buildtrigger/test/test_githubhandler.py b/buildtrigger/test/test_githubhandler.py new file mode 100644 index 000000000..3d6555f81 --- /dev/null +++ b/buildtrigger/test/test_githubhandler.py @@ -0,0 +1,361 @@ +import json +import pytest + +from github import GithubException +from mock import Mock +from datetime import datetime + +from buildtrigger.githubhandler import GithubBuildTrigger +from buildtrigger.triggerutil import (InvalidPayloadException, SkipRequestException, + TriggerStartException, ValidationRequestException) +from endpoints.building import PreparedBuild +from util.morecollections import AttrDict + +def get_mock_github(): + def get_commit_mock(commit_sha): + if commit_sha == 'aaaaaaa': + commit_mock = Mock() + commit_mock.sha = commit_sha + commit_mock.html_url = 'http://url/to/commit' + commit_mock.last_modified = 'now' + + commit_mock.commit = Mock() + commit_mock.commit.message = 'some cool message' + + commit_mock.committer = Mock() + commit_mock.committer.login = 'someuser' + commit_mock.committer.avatar_url = 'avatarurl' + commit_mock.committer.html_url = 'htmlurl' + + commit_mock.author = Mock() + commit_mock.author.login = 'someuser' + commit_mock.author.avatar_url = 'avatarurl' + commit_mock.author.html_url = 'htmlurl' + return commit_mock + + raise GithubException(None, None) + + def get_branch_mock(branch_name): + if branch_name == 'master': + branch_mock = Mock() + branch_mock.commit = Mock() + branch_mock.commit.sha = 'aaaaaaa' + return branch_mock + + raise GithubException(None, None) + + def get_repo_mock(namespace, name): + repo_mock = Mock() + repo_mock.owner = Mock() + repo_mock.owner.login = namespace + + repo_mock.full_name = '%s/%s' % (namespace, name) + repo_mock.name = name + repo_mock.description = 'some %s repo' % (name) + repo_mock.pushed_at = datetime.utcfromtimestamp(0) + repo_mock.html_url = 'http://some/url' + repo_mock.private = name == 'somerepo' + repo_mock.permissions = Mock() + repo_mock.permissions.admin = namespace == 'knownuser' + return repo_mock + + def get_user_repos_mock(): + return [get_repo_mock('knownuser', 'somerepo')] + + def get_org_repos_mock(type='all'): + return [get_repo_mock('someorg', 'somerepo'), get_repo_mock('someorg', 'anotherrepo')] + + def get_orgs_mock(): + return [get_org_mock('someorg')] + + def get_user_mock(username='knownuser'): + if username == 'knownuser': + user_mock = Mock() + user_mock.name = username + user_mock.plan = Mock() + user_mock.plan.private_repos = 1 + user_mock.login = username + user_mock.html_url = 'htmlurl' + user_mock.avatar_url = 'avatarurl' + user_mock.get_repos = Mock(side_effect=get_user_repos_mock) + user_mock.get_orgs = Mock(side_effect=get_orgs_mock) + return user_mock + + raise GithubException(None, None) + + def get_org_mock(namespace): + if namespace == 'someorg': + org_mock = Mock() + org_mock.get_repos = Mock(side_effect=get_org_repos_mock) + org_mock.login = namespace + org_mock.html_url = 'htmlurl' + org_mock.avatar_url = 'avatarurl' + org_mock.name = namespace + org_mock.plan = Mock() + org_mock.plan.private_repos = 2 + return org_mock + + raise GithubException(None, None) + + def get_tags_mock(): + sometag = Mock() + sometag.name = 'sometag' + sometag.commit = get_commit_mock('aaaaaaa') + + someothertag = Mock() + someothertag.name = 'someothertag' + someothertag.commit = get_commit_mock('aaaaaaa') + return [sometag, someothertag] + + def get_branches_mock(): + master = Mock() + master.name = 'master' + master.commit = get_commit_mock('aaaaaaa') + + otherbranch = Mock() + otherbranch.name = 'otherbranch' + otherbranch.commit = get_commit_mock('aaaaaaa') + return [master, otherbranch] + + def get_file_contents_mock(filepath): + if filepath == '/Dockerfile': + m = Mock() + m.content = 'hello world' + return m + + if filepath == 'somesubdir/Dockerfile': + m = Mock() + m.content = 'hi universe' + return m + + raise GithubException(None, None) + + def get_git_tree_mock(commit_sha, recursive=False): + first_file = Mock() + first_file.type = 'blob' + first_file.path = 'Dockerfile' + + second_file = Mock() + second_file.type = 'other' + second_file.path = '/some/Dockerfile' + + third_file = Mock() + third_file.type = 'blob' + third_file.path = 'somesubdir/Dockerfile' + + t = Mock() + + if commit_sha == 'aaaaaaa': + t.tree = [ + first_file, second_file, third_file, + ] + else: + t.tree = [] + + return t + + repo_mock = Mock() + repo_mock.default_branch = 'master' + repo_mock.ssh_url = 'ssh_url' + + repo_mock.get_branch = Mock(side_effect=get_branch_mock) + repo_mock.get_tags = Mock(side_effect=get_tags_mock) + repo_mock.get_branches = Mock(side_effect=get_branches_mock) + repo_mock.get_commit = Mock(side_effect=get_commit_mock) + repo_mock.get_file_contents = Mock(side_effect=get_file_contents_mock) + repo_mock.get_git_tree = Mock(side_effect=get_git_tree_mock) + + gh_mock = Mock() + gh_mock.get_repo = Mock(return_value=repo_mock) + gh_mock.get_user = Mock(side_effect=get_user_mock) + gh_mock.get_organization = Mock(side_effect=get_org_mock) + return gh_mock + +@pytest.fixture +def github_trigger(): + return _get_github_trigger() + +def _get_github_trigger(subdir=''): + trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger')) + trigger = GithubBuildTrigger(trigger_obj, {'build_source': 'foo', 'subdir': subdir}) + trigger._get_client = get_mock_github + return trigger + +@pytest.mark.parametrize('payload, expected_error, expected_message', [ + ('{"zen": true}', SkipRequestException, ""), + + ('{}', ValidationRequestException, "Missing 'repository' on request"), + ('{"repository": "foo"}', ValidationRequestException, "Missing 'owner' on repository"), + + # Valid payload: + ('''{ + "repository": { + "owner": { + "name": "someguy" + }, + "name": "somerepo", + "ssh_url": "someurl" + }, + "ref": "refs/tags/foo", + "head_commit": { + "id": "11d6fbc", + "url": "http://some/url", + "message": "some message", + "timestamp": "NOW" + } + }''', None, None), + + # Skip message: + ('''{ + "repository": { + "owner": { + "name": "someguy" + }, + "name": "somerepo", + "ssh_url": "someurl" + }, + "ref": "refs/tags/foo", + "head_commit": { + "id": "11d6fbc", + "url": "http://some/url", + "message": "[skip build]", + "timestamp": "NOW" + } + }''', SkipRequestException, ''), +]) +def test_handle_trigger_request(github_trigger, payload, expected_error, expected_message): + def get_payload(): + return json.loads(payload) + + request = AttrDict(dict(get_json=get_payload)) + + if expected_error is not None: + with pytest.raises(expected_error) as ipe: + github_trigger.handle_trigger_request(request) + assert ipe.value.message == expected_message + else: + assert isinstance(github_trigger.handle_trigger_request(request), PreparedBuild) + + +@pytest.mark.parametrize('run_parameters, expected_error, expected_message', [ + # No branch or tag specified: use the commit of the default branch. + ({}, None, None), + + # Invalid branch. + ({'refs': {'kind': 'branch', 'name': 'invalid'}}, TriggerStartException, + 'Could not find branch in repository'), + + # Invalid tag. + ({'refs': {'kind': 'tag', 'name': 'invalid'}}, TriggerStartException, + 'Could not find tag in repository'), + + # Valid branch. + ({'refs': {'kind': 'branch', 'name': 'master'}}, None, None), + + # Valid tag. + ({'refs': {'kind': 'tag', 'name': 'sometag'}}, None, None), +]) +def test_manual_start(run_parameters, expected_error, expected_message, github_trigger): + if expected_error is not None: + with pytest.raises(expected_error) as ipe: + github_trigger.manual_start(run_parameters) + assert ipe.value.message == expected_message + else: + assert isinstance(github_trigger.manual_start(run_parameters), PreparedBuild) + + +@pytest.mark.parametrize('username, expected_response', [ + ('unknownuser', None), + ('knownuser', {'html_url': 'htmlurl', 'avatar_url': 'avatarurl'}), +]) +def test_lookup_user(username, expected_response, github_trigger): + assert github_trigger.lookup_user(username) == expected_response + + +@pytest.mark.parametrize('name, expected', [ + ('refs', [ + {'kind': 'branch', 'name': 'master'}, + {'kind': 'branch', 'name': 'otherbranch'}, + {'kind': 'tag', 'name': 'sometag'}, + {'kind': 'tag', 'name': 'someothertag'}, + ]), + ('tag_name', ['sometag', 'someothertag']), + ('branch_name', ['master', 'otherbranch']), + ('invalid', None) +]) +def test_list_field_values(name, expected, github_trigger): + assert github_trigger.list_field_values(name) == expected + + +@pytest.mark.parametrize('subdir, contents', [ + ('', 'hello world'), + ('somesubdir', 'hi universe'), + ('unknownpath', None), +]) +def test_load_dockerfile_contents(subdir, contents): + trigger = _get_github_trigger(subdir) + assert trigger.load_dockerfile_contents() == contents + + +def test_list_build_subdirs(github_trigger): + assert github_trigger.list_build_subdirs() == ['', 'somesubdir'] + + +def test_list_build_source_namespaces(github_trigger): + namespaces_expected = [ + { + 'personal': True, + 'score': 1, + 'avatar_url': 'avatarurl', + 'id': 'knownuser', + 'title': 'knownuser' + }, + { + 'score': 2, + 'title': 'someorg', + 'personal': False, + 'url': 'htmlurl', + 'avatar_url': 'avatarurl', + 'id': 'someorg' + } + ] + assert github_trigger.list_build_source_namespaces() == namespaces_expected + + +@pytest.mark.parametrize('namespace, expected', [ + ('', []), + ('unknown', []), + + ('knownuser', [ + { + 'last_updated': 0, 'name': 'somerepo', 'url': 'http://some/url', 'private': True, + 'full_name': 'knownuser/somerepo', 'has_admin_permissions': True, + 'description': 'some somerepo repo' + }]), + + ('someorg', [ + { + 'last_updated': 0, 'name': 'somerepo', 'url': 'http://some/url', + 'private': True, 'full_name': 'someorg/somerepo', 'has_admin_permissions': False, + 'description': 'some somerepo repo' + }, + { + 'last_updated': 0, 'name': 'anotherrepo', 'url': 'http://some/url', + 'private': False, 'full_name': 'someorg/anotherrepo', 'has_admin_permissions': False, + 'description': 'some anotherrepo repo' + }]), +]) +def test_list_build_sources_for_namespace(namespace, expected, github_trigger): + # TODO: schema validation on the resulting namespaces. + assert github_trigger.list_build_sources_for_namespace(namespace) == expected + + +def test_activate(github_trigger): + config, private_key = github_trigger.activate('http://some/url') + assert 'deploy_key_id' in config + assert 'hook_id' in config + assert 'private_key' in private_key + + +def test_deactivate(github_trigger): + github_trigger.deactivate() From ba301b401b723d192766ea000d180f9c3329add5 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 13 Feb 2017 13:50:14 -0500 Subject: [PATCH 21/29] Break common git hosts tests into their own suite --- buildtrigger/test/__init__.py | 0 buildtrigger/test/githubmock.py | 173 ++++++++++++ buildtrigger/test/test_bitbuckethandler.py | 24 ++ buildtrigger/test/test_githosthandler.py | 125 +++++++++ buildtrigger/test/test_githubhandler.py | 291 +-------------------- 5 files changed, 326 insertions(+), 287 deletions(-) create mode 100644 buildtrigger/test/__init__.py create mode 100644 buildtrigger/test/githubmock.py create mode 100644 buildtrigger/test/test_bitbuckethandler.py create mode 100644 buildtrigger/test/test_githosthandler.py diff --git a/buildtrigger/test/__init__.py b/buildtrigger/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/buildtrigger/test/githubmock.py b/buildtrigger/test/githubmock.py new file mode 100644 index 000000000..a5b22bf87 --- /dev/null +++ b/buildtrigger/test/githubmock.py @@ -0,0 +1,173 @@ +from datetime import datetime +from mock import Mock + +from github import GithubException + +from buildtrigger.githubhandler import GithubBuildTrigger +from util.morecollections import AttrDict + +def get_github_trigger(subdir=''): + trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger')) + trigger = GithubBuildTrigger(trigger_obj, {'build_source': 'foo', 'subdir': subdir}) + trigger._get_client = get_mock_github + return trigger + +def get_mock_github(): + def get_commit_mock(commit_sha): + if commit_sha == 'aaaaaaa': + commit_mock = Mock() + commit_mock.sha = commit_sha + commit_mock.html_url = 'http://url/to/commit' + commit_mock.last_modified = 'now' + + commit_mock.commit = Mock() + commit_mock.commit.message = 'some cool message' + + commit_mock.committer = Mock() + commit_mock.committer.login = 'someuser' + commit_mock.committer.avatar_url = 'avatarurl' + commit_mock.committer.html_url = 'htmlurl' + + commit_mock.author = Mock() + commit_mock.author.login = 'someuser' + commit_mock.author.avatar_url = 'avatarurl' + commit_mock.author.html_url = 'htmlurl' + return commit_mock + + raise GithubException(None, None) + + def get_branch_mock(branch_name): + if branch_name == 'master': + branch_mock = Mock() + branch_mock.commit = Mock() + branch_mock.commit.sha = 'aaaaaaa' + return branch_mock + + raise GithubException(None, None) + + def get_repo_mock(namespace, name): + repo_mock = Mock() + repo_mock.owner = Mock() + repo_mock.owner.login = namespace + + repo_mock.full_name = '%s/%s' % (namespace, name) + repo_mock.name = name + repo_mock.description = 'some %s repo' % (name) + repo_mock.pushed_at = datetime.utcfromtimestamp(0) + repo_mock.html_url = 'http://some/url' + repo_mock.private = name == 'somerepo' + repo_mock.permissions = Mock() + repo_mock.permissions.admin = namespace == 'knownuser' + return repo_mock + + def get_user_repos_mock(): + return [get_repo_mock('knownuser', 'somerepo')] + + def get_org_repos_mock(type='all'): + return [get_repo_mock('someorg', 'somerepo'), get_repo_mock('someorg', 'anotherrepo')] + + def get_orgs_mock(): + return [get_org_mock('someorg')] + + def get_user_mock(username='knownuser'): + if username == 'knownuser': + user_mock = Mock() + user_mock.name = username + user_mock.plan = Mock() + user_mock.plan.private_repos = 1 + user_mock.login = username + user_mock.html_url = 'htmlurl' + user_mock.avatar_url = 'avatarurl' + user_mock.get_repos = Mock(side_effect=get_user_repos_mock) + user_mock.get_orgs = Mock(side_effect=get_orgs_mock) + return user_mock + + raise GithubException(None, None) + + def get_org_mock(namespace): + if namespace == 'someorg': + org_mock = Mock() + org_mock.get_repos = Mock(side_effect=get_org_repos_mock) + org_mock.login = namespace + org_mock.html_url = 'htmlurl' + org_mock.avatar_url = 'avatarurl' + org_mock.name = namespace + org_mock.plan = Mock() + org_mock.plan.private_repos = 2 + return org_mock + + raise GithubException(None, None) + + def get_tags_mock(): + sometag = Mock() + sometag.name = 'sometag' + sometag.commit = get_commit_mock('aaaaaaa') + + someothertag = Mock() + someothertag.name = 'someothertag' + someothertag.commit = get_commit_mock('aaaaaaa') + return [sometag, someothertag] + + def get_branches_mock(): + master = Mock() + master.name = 'master' + master.commit = get_commit_mock('aaaaaaa') + + otherbranch = Mock() + otherbranch.name = 'otherbranch' + otherbranch.commit = get_commit_mock('aaaaaaa') + return [master, otherbranch] + + def get_file_contents_mock(filepath): + if filepath == '/Dockerfile': + m = Mock() + m.content = 'hello world' + return m + + if filepath == 'somesubdir/Dockerfile': + m = Mock() + m.content = 'hi universe' + return m + + raise GithubException(None, None) + + def get_git_tree_mock(commit_sha, recursive=False): + first_file = Mock() + first_file.type = 'blob' + first_file.path = 'Dockerfile' + + second_file = Mock() + second_file.type = 'other' + second_file.path = '/some/Dockerfile' + + third_file = Mock() + third_file.type = 'blob' + third_file.path = 'somesubdir/Dockerfile' + + t = Mock() + + if commit_sha == 'aaaaaaa': + t.tree = [ + first_file, second_file, third_file, + ] + else: + t.tree = [] + + return t + + repo_mock = Mock() + repo_mock.default_branch = 'master' + repo_mock.ssh_url = 'ssh_url' + + repo_mock.get_branch = Mock(side_effect=get_branch_mock) + repo_mock.get_tags = Mock(side_effect=get_tags_mock) + repo_mock.get_branches = Mock(side_effect=get_branches_mock) + repo_mock.get_commit = Mock(side_effect=get_commit_mock) + repo_mock.get_file_contents = Mock(side_effect=get_file_contents_mock) + repo_mock.get_git_tree = Mock(side_effect=get_git_tree_mock) + + gh_mock = Mock() + gh_mock.get_repo = Mock(return_value=repo_mock) + gh_mock.get_user = Mock(side_effect=get_user_mock) + gh_mock.get_organization = Mock(side_effect=get_org_mock) + return gh_mock diff --git a/buildtrigger/test/test_bitbuckethandler.py b/buildtrigger/test/test_bitbuckethandler.py new file mode 100644 index 000000000..b95619bf6 --- /dev/null +++ b/buildtrigger/test/test_bitbuckethandler.py @@ -0,0 +1,24 @@ +import pytest + +from mock import Mock +from datetime import datetime + +from buildtrigger.bitbuckethandler import BitbucketBuildTrigger +from buildtrigger.triggerutil import (InvalidPayloadException, SkipRequestException, + TriggerStartException, ValidationRequestException) +from endpoints.building import PreparedBuild +from util.morecollections import AttrDict + +@pytest.fixture +def bitbucket_trigger(): + return _get_bitbucket_trigger() + +def get_mock_bitbucket(): + client_mock = Mock() + return client_mock + +def _get_bitbucket_trigger(subdir=''): + trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger')) + trigger = BitbucketBuildTrigger(trigger_obj, {'build_source': 'foo/bar', 'subdir': subdir}) + trigger._get_client = get_mock_bitbucket + return trigger diff --git a/buildtrigger/test/test_githosthandler.py b/buildtrigger/test/test_githosthandler.py new file mode 100644 index 000000000..6ed25bf4f --- /dev/null +++ b/buildtrigger/test/test_githosthandler.py @@ -0,0 +1,125 @@ +import pytest + +from buildtrigger.triggerutil import TriggerStartException +from buildtrigger.test.githubmock import get_github_trigger +from endpoints.building import PreparedBuild + +def github_trigger(): + return get_github_trigger() + +@pytest.fixture(params=[github_trigger()]) +def githost_trigger(request): + return request.param + +@pytest.mark.parametrize('run_parameters, expected_error, expected_message', [ + # No branch or tag specified: use the commit of the default branch. + ({}, None, None), + + # Invalid branch. + ({'refs': {'kind': 'branch', 'name': 'invalid'}}, TriggerStartException, + 'Could not find branch in repository'), + + # Invalid tag. + ({'refs': {'kind': 'tag', 'name': 'invalid'}}, TriggerStartException, + 'Could not find tag in repository'), + + # Valid branch. + ({'refs': {'kind': 'branch', 'name': 'master'}}, None, None), + + # Valid tag. + ({'refs': {'kind': 'tag', 'name': 'sometag'}}, None, None), +]) +def test_manual_start(run_parameters, expected_error, expected_message, githost_trigger): + if expected_error is not None: + with pytest.raises(expected_error) as ipe: + githost_trigger.manual_start(run_parameters) + assert ipe.value.message == expected_message + else: + assert isinstance(githost_trigger.manual_start(run_parameters), PreparedBuild) + + +@pytest.mark.parametrize('username, expected_response', [ + ('unknownuser', None), + ('knownuser', {'html_url': 'htmlurl', 'avatar_url': 'avatarurl'}), +]) +def test_lookup_user(username, expected_response, githost_trigger): + assert githost_trigger.lookup_user(username) == expected_response + + +@pytest.mark.parametrize('name, expected', [ + ('refs', [ + {'kind': 'branch', 'name': 'master'}, + {'kind': 'branch', 'name': 'otherbranch'}, + {'kind': 'tag', 'name': 'sometag'}, + {'kind': 'tag', 'name': 'someothertag'}, + ]), + ('tag_name', ['sometag', 'someothertag']), + ('branch_name', ['master', 'otherbranch']), + ('invalid', None) +]) +def test_list_field_values(name, expected, githost_trigger): + assert githost_trigger.list_field_values(name) == expected + + +def test_list_build_subdirs(githost_trigger): + assert githost_trigger.list_build_subdirs() == ['', 'somesubdir'] + + +def test_list_build_source_namespaces(githost_trigger): + namespaces_expected = [ + { + 'personal': True, + 'score': 1, + 'avatar_url': 'avatarurl', + 'id': 'knownuser', + 'title': 'knownuser' + }, + { + 'score': 2, + 'title': 'someorg', + 'personal': False, + 'url': 'htmlurl', + 'avatar_url': 'avatarurl', + 'id': 'someorg' + } + ] + assert githost_trigger.list_build_source_namespaces() == namespaces_expected + + +@pytest.mark.parametrize('namespace, expected', [ + ('', []), + ('unknown', []), + + ('knownuser', [ + { + 'last_updated': 0, 'name': 'somerepo', 'url': 'http://some/url', 'private': True, + 'full_name': 'knownuser/somerepo', 'has_admin_permissions': True, + 'description': 'some somerepo repo' + }]), + + ('someorg', [ + { + 'last_updated': 0, 'name': 'somerepo', 'url': 'http://some/url', + 'private': True, 'full_name': 'someorg/somerepo', 'has_admin_permissions': False, + 'description': 'some somerepo repo' + }, + { + 'last_updated': 0, 'name': 'anotherrepo', 'url': 'http://some/url', + 'private': False, 'full_name': 'someorg/anotherrepo', 'has_admin_permissions': False, + 'description': 'some anotherrepo repo' + }]), +]) +def test_list_build_sources_for_namespace(namespace, expected, githost_trigger): + # TODO: schema validation on the resulting namespaces. + assert githost_trigger.list_build_sources_for_namespace(namespace) == expected + + +def test_activate(githost_trigger): + config, private_key = githost_trigger.activate('http://some/url') + assert 'deploy_key_id' in config + assert 'hook_id' in config + assert 'private_key' in private_key + + +def test_deactivate(githost_trigger): + githost_trigger.deactivate() diff --git a/buildtrigger/test/test_githubhandler.py b/buildtrigger/test/test_githubhandler.py index 3d6555f81..aa2fdd244 100644 --- a/buildtrigger/test/test_githubhandler.py +++ b/buildtrigger/test/test_githubhandler.py @@ -1,185 +1,15 @@ import json import pytest -from github import GithubException -from mock import Mock -from datetime import datetime - -from buildtrigger.githubhandler import GithubBuildTrigger -from buildtrigger.triggerutil import (InvalidPayloadException, SkipRequestException, - TriggerStartException, ValidationRequestException) +from buildtrigger.test.githubmock import get_github_trigger +from buildtrigger.triggerutil import SkipRequestException, ValidationRequestException from endpoints.building import PreparedBuild from util.morecollections import AttrDict -def get_mock_github(): - def get_commit_mock(commit_sha): - if commit_sha == 'aaaaaaa': - commit_mock = Mock() - commit_mock.sha = commit_sha - commit_mock.html_url = 'http://url/to/commit' - commit_mock.last_modified = 'now' - - commit_mock.commit = Mock() - commit_mock.commit.message = 'some cool message' - - commit_mock.committer = Mock() - commit_mock.committer.login = 'someuser' - commit_mock.committer.avatar_url = 'avatarurl' - commit_mock.committer.html_url = 'htmlurl' - - commit_mock.author = Mock() - commit_mock.author.login = 'someuser' - commit_mock.author.avatar_url = 'avatarurl' - commit_mock.author.html_url = 'htmlurl' - return commit_mock - - raise GithubException(None, None) - - def get_branch_mock(branch_name): - if branch_name == 'master': - branch_mock = Mock() - branch_mock.commit = Mock() - branch_mock.commit.sha = 'aaaaaaa' - return branch_mock - - raise GithubException(None, None) - - def get_repo_mock(namespace, name): - repo_mock = Mock() - repo_mock.owner = Mock() - repo_mock.owner.login = namespace - - repo_mock.full_name = '%s/%s' % (namespace, name) - repo_mock.name = name - repo_mock.description = 'some %s repo' % (name) - repo_mock.pushed_at = datetime.utcfromtimestamp(0) - repo_mock.html_url = 'http://some/url' - repo_mock.private = name == 'somerepo' - repo_mock.permissions = Mock() - repo_mock.permissions.admin = namespace == 'knownuser' - return repo_mock - - def get_user_repos_mock(): - return [get_repo_mock('knownuser', 'somerepo')] - - def get_org_repos_mock(type='all'): - return [get_repo_mock('someorg', 'somerepo'), get_repo_mock('someorg', 'anotherrepo')] - - def get_orgs_mock(): - return [get_org_mock('someorg')] - - def get_user_mock(username='knownuser'): - if username == 'knownuser': - user_mock = Mock() - user_mock.name = username - user_mock.plan = Mock() - user_mock.plan.private_repos = 1 - user_mock.login = username - user_mock.html_url = 'htmlurl' - user_mock.avatar_url = 'avatarurl' - user_mock.get_repos = Mock(side_effect=get_user_repos_mock) - user_mock.get_orgs = Mock(side_effect=get_orgs_mock) - return user_mock - - raise GithubException(None, None) - - def get_org_mock(namespace): - if namespace == 'someorg': - org_mock = Mock() - org_mock.get_repos = Mock(side_effect=get_org_repos_mock) - org_mock.login = namespace - org_mock.html_url = 'htmlurl' - org_mock.avatar_url = 'avatarurl' - org_mock.name = namespace - org_mock.plan = Mock() - org_mock.plan.private_repos = 2 - return org_mock - - raise GithubException(None, None) - - def get_tags_mock(): - sometag = Mock() - sometag.name = 'sometag' - sometag.commit = get_commit_mock('aaaaaaa') - - someothertag = Mock() - someothertag.name = 'someothertag' - someothertag.commit = get_commit_mock('aaaaaaa') - return [sometag, someothertag] - - def get_branches_mock(): - master = Mock() - master.name = 'master' - master.commit = get_commit_mock('aaaaaaa') - - otherbranch = Mock() - otherbranch.name = 'otherbranch' - otherbranch.commit = get_commit_mock('aaaaaaa') - return [master, otherbranch] - - def get_file_contents_mock(filepath): - if filepath == '/Dockerfile': - m = Mock() - m.content = 'hello world' - return m - - if filepath == 'somesubdir/Dockerfile': - m = Mock() - m.content = 'hi universe' - return m - - raise GithubException(None, None) - - def get_git_tree_mock(commit_sha, recursive=False): - first_file = Mock() - first_file.type = 'blob' - first_file.path = 'Dockerfile' - - second_file = Mock() - second_file.type = 'other' - second_file.path = '/some/Dockerfile' - - third_file = Mock() - third_file.type = 'blob' - third_file.path = 'somesubdir/Dockerfile' - - t = Mock() - - if commit_sha == 'aaaaaaa': - t.tree = [ - first_file, second_file, third_file, - ] - else: - t.tree = [] - - return t - - repo_mock = Mock() - repo_mock.default_branch = 'master' - repo_mock.ssh_url = 'ssh_url' - - repo_mock.get_branch = Mock(side_effect=get_branch_mock) - repo_mock.get_tags = Mock(side_effect=get_tags_mock) - repo_mock.get_branches = Mock(side_effect=get_branches_mock) - repo_mock.get_commit = Mock(side_effect=get_commit_mock) - repo_mock.get_file_contents = Mock(side_effect=get_file_contents_mock) - repo_mock.get_git_tree = Mock(side_effect=get_git_tree_mock) - - gh_mock = Mock() - gh_mock.get_repo = Mock(return_value=repo_mock) - gh_mock.get_user = Mock(side_effect=get_user_mock) - gh_mock.get_organization = Mock(side_effect=get_org_mock) - return gh_mock - @pytest.fixture def github_trigger(): - return _get_github_trigger() + return get_github_trigger() -def _get_github_trigger(subdir=''): - trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger')) - trigger = GithubBuildTrigger(trigger_obj, {'build_source': 'foo', 'subdir': subdir}) - trigger._get_client = get_mock_github - return trigger @pytest.mark.parametrize('payload, expected_error, expected_message', [ ('{"zen": true}', SkipRequestException, ""), @@ -237,125 +67,12 @@ def test_handle_trigger_request(github_trigger, payload, expected_error, expecte assert isinstance(github_trigger.handle_trigger_request(request), PreparedBuild) -@pytest.mark.parametrize('run_parameters, expected_error, expected_message', [ - # No branch or tag specified: use the commit of the default branch. - ({}, None, None), - - # Invalid branch. - ({'refs': {'kind': 'branch', 'name': 'invalid'}}, TriggerStartException, - 'Could not find branch in repository'), - - # Invalid tag. - ({'refs': {'kind': 'tag', 'name': 'invalid'}}, TriggerStartException, - 'Could not find tag in repository'), - - # Valid branch. - ({'refs': {'kind': 'branch', 'name': 'master'}}, None, None), - - # Valid tag. - ({'refs': {'kind': 'tag', 'name': 'sometag'}}, None, None), -]) -def test_manual_start(run_parameters, expected_error, expected_message, github_trigger): - if expected_error is not None: - with pytest.raises(expected_error) as ipe: - github_trigger.manual_start(run_parameters) - assert ipe.value.message == expected_message - else: - assert isinstance(github_trigger.manual_start(run_parameters), PreparedBuild) - - -@pytest.mark.parametrize('username, expected_response', [ - ('unknownuser', None), - ('knownuser', {'html_url': 'htmlurl', 'avatar_url': 'avatarurl'}), -]) -def test_lookup_user(username, expected_response, github_trigger): - assert github_trigger.lookup_user(username) == expected_response - - -@pytest.mark.parametrize('name, expected', [ - ('refs', [ - {'kind': 'branch', 'name': 'master'}, - {'kind': 'branch', 'name': 'otherbranch'}, - {'kind': 'tag', 'name': 'sometag'}, - {'kind': 'tag', 'name': 'someothertag'}, - ]), - ('tag_name', ['sometag', 'someothertag']), - ('branch_name', ['master', 'otherbranch']), - ('invalid', None) -]) -def test_list_field_values(name, expected, github_trigger): - assert github_trigger.list_field_values(name) == expected - - @pytest.mark.parametrize('subdir, contents', [ ('', 'hello world'), ('somesubdir', 'hi universe'), ('unknownpath', None), ]) def test_load_dockerfile_contents(subdir, contents): - trigger = _get_github_trigger(subdir) + trigger = get_github_trigger(subdir) assert trigger.load_dockerfile_contents() == contents - -def test_list_build_subdirs(github_trigger): - assert github_trigger.list_build_subdirs() == ['', 'somesubdir'] - - -def test_list_build_source_namespaces(github_trigger): - namespaces_expected = [ - { - 'personal': True, - 'score': 1, - 'avatar_url': 'avatarurl', - 'id': 'knownuser', - 'title': 'knownuser' - }, - { - 'score': 2, - 'title': 'someorg', - 'personal': False, - 'url': 'htmlurl', - 'avatar_url': 'avatarurl', - 'id': 'someorg' - } - ] - assert github_trigger.list_build_source_namespaces() == namespaces_expected - - -@pytest.mark.parametrize('namespace, expected', [ - ('', []), - ('unknown', []), - - ('knownuser', [ - { - 'last_updated': 0, 'name': 'somerepo', 'url': 'http://some/url', 'private': True, - 'full_name': 'knownuser/somerepo', 'has_admin_permissions': True, - 'description': 'some somerepo repo' - }]), - - ('someorg', [ - { - 'last_updated': 0, 'name': 'somerepo', 'url': 'http://some/url', - 'private': True, 'full_name': 'someorg/somerepo', 'has_admin_permissions': False, - 'description': 'some somerepo repo' - }, - { - 'last_updated': 0, 'name': 'anotherrepo', 'url': 'http://some/url', - 'private': False, 'full_name': 'someorg/anotherrepo', 'has_admin_permissions': False, - 'description': 'some anotherrepo repo' - }]), -]) -def test_list_build_sources_for_namespace(namespace, expected, github_trigger): - # TODO: schema validation on the resulting namespaces. - assert github_trigger.list_build_sources_for_namespace(namespace) == expected - - -def test_activate(github_trigger): - config, private_key = github_trigger.activate('http://some/url') - assert 'deploy_key_id' in config - assert 'hook_id' in config - assert 'private_key' in private_key - - -def test_deactivate(github_trigger): - github_trigger.deactivate() From 497c90e7ea90e21870af67d8e28c7e49eaacf230 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 13 Feb 2017 15:01:09 -0500 Subject: [PATCH 22/29] Add unit testing of bitbucket trigger handler --- buildtrigger/bitbuckethandler.py | 9 +- buildtrigger/githubhandler.py | 1 + buildtrigger/test/bitbucketmock.py | 161 +++++++++++++++++++++ buildtrigger/test/githubmock.py | 6 +- buildtrigger/test/test_bitbuckethandler.py | 32 ++-- buildtrigger/test/test_githosthandler.py | 48 +++--- buildtrigger/test/test_githubhandler.py | 11 ++ 7 files changed, 217 insertions(+), 51 deletions(-) create mode 100644 buildtrigger/test/bitbucketmock.py diff --git a/buildtrigger/bitbuckethandler.py b/buildtrigger/bitbuckethandler.py index f96a38cee..33c890083 100644 --- a/buildtrigger/bitbuckethandler.py +++ b/buildtrigger/bitbuckethandler.py @@ -412,7 +412,8 @@ class BitbucketBuildTrigger(BuildTriggerHandler): 'id': owner, 'title': owner, 'avatar_url': repo['logo'], - 'score': 0, + 'url': 'https://bitbucket.org/%s' % (owner), + 'score': 1, } return list(namespaces.values()) @@ -464,7 +465,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler): (result, data, err_msg) = repository.get_raw_path_contents(path, revision='master') if not result: - raise RepositoryReadException(err_msg) + return None return data @@ -541,7 +542,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler): # Lookup the commit SHA for the branch. (result, data, _) = repository.get_branch(branch_name) if not result: - raise TriggerStartException('Could not find branch commit SHA') + raise TriggerStartException('Could not find branch in repository') return data['target']['hash'] @@ -549,7 +550,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler): # Lookup the commit SHA for the tag. (result, data, _) = repository.get_tag(tag_name) if not result: - raise TriggerStartException('Could not find tag commit SHA') + raise TriggerStartException('Could not find tag in repository') return data['target']['hash'] diff --git a/buildtrigger/githubhandler.py b/buildtrigger/githubhandler.py index a51526507..3233f5f5d 100644 --- a/buildtrigger/githubhandler.py +++ b/buildtrigger/githubhandler.py @@ -286,6 +286,7 @@ class GithubBuildTrigger(BuildTriggerHandler): 'id': usr.login, 'title': usr.name or usr.login, 'avatar_url': usr.avatar_url, + 'url': usr.html_url, 'score': usr.plan.private_repos if usr.plan else 0, } diff --git a/buildtrigger/test/bitbucketmock.py b/buildtrigger/test/bitbucketmock.py new file mode 100644 index 000000000..c442f20dd --- /dev/null +++ b/buildtrigger/test/bitbucketmock.py @@ -0,0 +1,161 @@ +from datetime import datetime +from mock import Mock + +from buildtrigger.bitbuckethandler import BitbucketBuildTrigger +from util.morecollections import AttrDict + +def get_bitbucket_trigger(subdir=''): + trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger')) + trigger = BitbucketBuildTrigger(trigger_obj, { + 'build_source': 'foo/bar', + 'subdir': subdir, + 'username': 'knownuser' + }) + + trigger._get_client = get_mock_bitbucket + return trigger + +def get_repo_path_contents(path, revision): + if revision != 'master': + return (False, None, None) + + data = { + 'files': [{'path': 'Dockerfile'}], + } + + return (True, data, None) + +def get_raw_path_contents(path, revision): + if path == '/Dockerfile': + return (True, 'hello world', None) + + if path == 'somesubdir/Dockerfile': + return (True, 'hi universe', None) + + return (False, None, None) + +def get_branches_and_tags(): + data = { + 'branches': [{'name': 'master'}, {'name': 'otherbranch'}], + 'tags': [{'name': 'sometag'}, {'name': 'someothertag'}], + } + return (True, data, None) + +def get_branches(): + return (True, {'master': {}, 'otherbranch': {}}, None) + +def get_tags(): + return (True, {'sometag': {}, 'someothertag': {}}, None) + +def get_branch(branch_name): + if branch_name != 'master': + return (False, None, None) + + data = { + 'target': { + 'hash': 'aaaaaaa', + }, + } + + return (True, data, None) + +def get_tag(tag_name): + if tag_name != 'sometag': + return (False, None, None) + + data = { + 'target': { + 'hash': 'aaaaaaa', + }, + } + + return (True, data, None) + +def get_changeset_mock(commit_sha): + if commit_sha != 'aaaaaaa': + return (False, None, 'Not found') + + data = { + 'node': 'aaaaaaa', + 'message': 'some message', + 'timestamp': 'now', + 'raw_author': 'foo@bar.com', + } + + return (True, data, None) + +def get_changesets(): + changesets_mock = Mock() + changesets_mock.get = Mock(side_effect=get_changeset_mock) + return changesets_mock + +def get_deploykeys(): + deploykeys_mock = Mock() + deploykeys_mock.create = Mock(return_value=(True, {'pk': 'someprivatekey'}, None)) + deploykeys_mock.delete = Mock(return_value=(True, {}, None)) + return deploykeys_mock + +def get_webhooks(): + webhooks_mock = Mock() + webhooks_mock.create = Mock(return_value=(True, {'uuid': 'someuuid'}, None)) + webhooks_mock.delete = Mock(return_value=(True, {}, None)) + return webhooks_mock + +def get_repo_mock(name): + if name != 'bar': + return None + + repo_mock = Mock() + repo_mock.get_main_branch = Mock(return_value=(True, {'name': 'master'}, None)) + repo_mock.get_path_contents = Mock(side_effect=get_repo_path_contents) + repo_mock.get_raw_path_contents = Mock(side_effect=get_raw_path_contents) + repo_mock.get_branches_and_tags = Mock(side_effect=get_branches_and_tags) + repo_mock.get_branches = Mock(side_effect=get_branches) + repo_mock.get_tags = Mock(side_effect=get_tags) + repo_mock.get_branch = Mock(side_effect=get_branch) + repo_mock.get_tag = Mock(side_effect=get_tag) + + repo_mock.changesets = Mock(side_effect=get_changesets) + repo_mock.deploykeys = Mock(side_effect=get_deploykeys) + repo_mock.webhooks = Mock(side_effect=get_webhooks) + return repo_mock + +def get_repositories_mock(): + repos_mock = Mock() + repos_mock.get = Mock(side_effect=get_repo_mock) + return repos_mock + +def get_namespace_mock(namespace): + namespace_mock = Mock() + namespace_mock.repositories = Mock(side_effect=get_repositories_mock) + return namespace_mock + +def get_repo(namespace, name): + return { + 'owner': namespace, + 'logo': 'avatarurl', + 'slug': name, + 'description': 'some %s repo' % (name), + 'utc_last_updated': str(datetime.utcfromtimestamp(0)), + 'read_only': namespace != 'knownuser', + 'is_private': name == 'somerepo', + } + +def get_visible_repos(): + repos = [ + get_repo('knownuser', 'somerepo'), + get_repo('someorg', 'somerepo'), + get_repo('someorg', 'anotherrepo'), + ] + return (True, repos, None) + +def get_authed_mock(token, secret): + authed_mock = Mock() + authed_mock.for_namespace = Mock(side_effect=get_namespace_mock) + authed_mock.get_visible_repositories = Mock(side_effect=get_visible_repos) + return authed_mock + +def get_mock_bitbucket(): + bitbucket_mock = Mock() + bitbucket_mock.get_authorized_client = Mock(side_effect=get_authed_mock) + return bitbucket_mock diff --git a/buildtrigger/test/githubmock.py b/buildtrigger/test/githubmock.py index a5b22bf87..77c8a7a1f 100644 --- a/buildtrigger/test/githubmock.py +++ b/buildtrigger/test/githubmock.py @@ -54,7 +54,7 @@ def get_mock_github(): repo_mock.name = name repo_mock.description = 'some %s repo' % (name) repo_mock.pushed_at = datetime.utcfromtimestamp(0) - repo_mock.html_url = 'http://some/url' + repo_mock.html_url = 'https://bitbucket.org/%s/%s' % (namespace, name) repo_mock.private = name == 'somerepo' repo_mock.permissions = Mock() repo_mock.permissions.admin = namespace == 'knownuser' @@ -76,7 +76,7 @@ def get_mock_github(): user_mock.plan = Mock() user_mock.plan.private_repos = 1 user_mock.login = username - user_mock.html_url = 'htmlurl' + user_mock.html_url = 'https://bitbucket.org/%s' % (username) user_mock.avatar_url = 'avatarurl' user_mock.get_repos = Mock(side_effect=get_user_repos_mock) user_mock.get_orgs = Mock(side_effect=get_orgs_mock) @@ -89,7 +89,7 @@ def get_mock_github(): org_mock = Mock() org_mock.get_repos = Mock(side_effect=get_org_repos_mock) org_mock.login = namespace - org_mock.html_url = 'htmlurl' + org_mock.html_url = 'https://bitbucket.org/%s' % (namespace) org_mock.avatar_url = 'avatarurl' org_mock.name = namespace org_mock.plan = Mock() diff --git a/buildtrigger/test/test_bitbuckethandler.py b/buildtrigger/test/test_bitbuckethandler.py index b95619bf6..991d90af1 100644 --- a/buildtrigger/test/test_bitbuckethandler.py +++ b/buildtrigger/test/test_bitbuckethandler.py @@ -1,24 +1,22 @@ import pytest -from mock import Mock -from datetime import datetime - -from buildtrigger.bitbuckethandler import BitbucketBuildTrigger -from buildtrigger.triggerutil import (InvalidPayloadException, SkipRequestException, - TriggerStartException, ValidationRequestException) -from endpoints.building import PreparedBuild -from util.morecollections import AttrDict +from buildtrigger.test.bitbucketmock import get_bitbucket_trigger @pytest.fixture def bitbucket_trigger(): - return _get_bitbucket_trigger() + return get_bitbucket_trigger() -def get_mock_bitbucket(): - client_mock = Mock() - return client_mock -def _get_bitbucket_trigger(subdir=''): - trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger')) - trigger = BitbucketBuildTrigger(trigger_obj, {'build_source': 'foo/bar', 'subdir': subdir}) - trigger._get_client = get_mock_bitbucket - return trigger +def test_list_build_subdirs(bitbucket_trigger): + assert bitbucket_trigger.list_build_subdirs() == [''] + + +@pytest.mark.parametrize('subdir, contents', [ + ('', 'hello world'), + ('somesubdir', 'hi universe'), + ('unknownpath', None), +]) +def test_load_dockerfile_contents(subdir, contents): + trigger = get_bitbucket_trigger(subdir) + assert trigger.load_dockerfile_contents() == contents + diff --git a/buildtrigger/test/test_githosthandler.py b/buildtrigger/test/test_githosthandler.py index 6ed25bf4f..04b87fa3c 100644 --- a/buildtrigger/test/test_githosthandler.py +++ b/buildtrigger/test/test_githosthandler.py @@ -1,13 +1,11 @@ import pytest from buildtrigger.triggerutil import TriggerStartException +from buildtrigger.test.bitbucketmock import get_bitbucket_trigger from buildtrigger.test.githubmock import get_github_trigger from endpoints.building import PreparedBuild -def github_trigger(): - return get_github_trigger() - -@pytest.fixture(params=[github_trigger()]) +@pytest.fixture(params=[get_github_trigger(), get_bitbucket_trigger()]) def githost_trigger(request): return request.param @@ -38,14 +36,6 @@ def test_manual_start(run_parameters, expected_error, expected_message, githost_ assert isinstance(githost_trigger.manual_start(run_parameters), PreparedBuild) -@pytest.mark.parametrize('username, expected_response', [ - ('unknownuser', None), - ('knownuser', {'html_url': 'htmlurl', 'avatar_url': 'avatarurl'}), -]) -def test_lookup_user(username, expected_response, githost_trigger): - assert githost_trigger.lookup_user(username) == expected_response - - @pytest.mark.parametrize('name, expected', [ ('refs', [ {'kind': 'branch', 'name': 'master'}, @@ -53,16 +43,17 @@ def test_lookup_user(username, expected_response, githost_trigger): {'kind': 'tag', 'name': 'sometag'}, {'kind': 'tag', 'name': 'someothertag'}, ]), - ('tag_name', ['sometag', 'someothertag']), - ('branch_name', ['master', 'otherbranch']), + ('tag_name', set(['sometag', 'someothertag'])), + ('branch_name', set(['master', 'otherbranch'])), ('invalid', None) ]) def test_list_field_values(name, expected, githost_trigger): - assert githost_trigger.list_field_values(name) == expected - - -def test_list_build_subdirs(githost_trigger): - assert githost_trigger.list_build_subdirs() == ['', 'somesubdir'] + if expected is None: + assert githost_trigger.list_field_values(name) is None + elif isinstance(expected, set): + assert set(githost_trigger.list_field_values(name)) == set(expected) + else: + assert githost_trigger.list_field_values(name) == expected def test_list_build_source_namespaces(githost_trigger): @@ -72,13 +63,14 @@ def test_list_build_source_namespaces(githost_trigger): 'score': 1, 'avatar_url': 'avatarurl', 'id': 'knownuser', - 'title': 'knownuser' + 'title': 'knownuser', + 'url': 'https://bitbucket.org/knownuser', }, { 'score': 2, 'title': 'someorg', 'personal': False, - 'url': 'htmlurl', + 'url': 'https://bitbucket.org/someorg', 'avatar_url': 'avatarurl', 'id': 'someorg' } @@ -92,20 +84,23 @@ def test_list_build_source_namespaces(githost_trigger): ('knownuser', [ { - 'last_updated': 0, 'name': 'somerepo', 'url': 'http://some/url', 'private': True, + 'last_updated': 0, 'name': 'somerepo', + 'url': 'https://bitbucket.org/knownuser/somerepo', 'private': True, 'full_name': 'knownuser/somerepo', 'has_admin_permissions': True, 'description': 'some somerepo repo' }]), ('someorg', [ { - 'last_updated': 0, 'name': 'somerepo', 'url': 'http://some/url', - 'private': True, 'full_name': 'someorg/somerepo', 'has_admin_permissions': False, + 'last_updated': 0, 'name': 'somerepo', + 'url': 'https://bitbucket.org/someorg/somerepo', 'private': True, + 'full_name': 'someorg/somerepo', 'has_admin_permissions': False, 'description': 'some somerepo repo' }, { - 'last_updated': 0, 'name': 'anotherrepo', 'url': 'http://some/url', - 'private': False, 'full_name': 'someorg/anotherrepo', 'has_admin_permissions': False, + 'last_updated': 0, 'name': 'anotherrepo', + 'url': 'https://bitbucket.org/someorg/anotherrepo', 'private': False, + 'full_name': 'someorg/anotherrepo', 'has_admin_permissions': False, 'description': 'some anotherrepo repo' }]), ]) @@ -117,7 +112,6 @@ def test_list_build_sources_for_namespace(namespace, expected, githost_trigger): def test_activate(githost_trigger): config, private_key = githost_trigger.activate('http://some/url') assert 'deploy_key_id' in config - assert 'hook_id' in config assert 'private_key' in private_key diff --git a/buildtrigger/test/test_githubhandler.py b/buildtrigger/test/test_githubhandler.py index aa2fdd244..5f9a1d786 100644 --- a/buildtrigger/test/test_githubhandler.py +++ b/buildtrigger/test/test_githubhandler.py @@ -76,3 +76,14 @@ def test_load_dockerfile_contents(subdir, contents): trigger = get_github_trigger(subdir) assert trigger.load_dockerfile_contents() == contents + +@pytest.mark.parametrize('username, expected_response', [ + ('unknownuser', None), + ('knownuser', {'html_url': 'https://bitbucket.org/knownuser', 'avatar_url': 'avatarurl'}), +]) +def test_lookup_user(username, expected_response, github_trigger): + assert github_trigger.lookup_user(username) == expected_response + + +def test_list_build_subdirs(github_trigger): + assert github_trigger.list_build_subdirs() == ['', 'somesubdir'] From 84b298f36bc896b94472abbc674c74f1f2aa2d3f Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 13 Feb 2017 16:17:52 -0500 Subject: [PATCH 23/29] Add missing bitbucket test --- buildtrigger/bitbuckethandler.py | 26 ++++---- buildtrigger/test/test_bitbuckethandler.py | 69 ++++++++++++++++++++++ 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/buildtrigger/bitbuckethandler.py b/buildtrigger/bitbuckethandler.py index 33c890083..2f26626a9 100644 --- a/buildtrigger/bitbuckethandler.py +++ b/buildtrigger/bitbuckethandler.py @@ -35,7 +35,7 @@ BITBUCKET_WEBHOOK_PAYLOAD_SCHEMA = { }, }, 'required': ['full_name'], - }, + }, # /Repository 'push': { 'type': 'object', 'properties': { @@ -91,10 +91,10 @@ BITBUCKET_WEBHOOK_PAYLOAD_SCHEMA = { }, }, 'required': ['html', 'avatar'], - }, + }, # /User }, 'required': ['username'], - }, + }, # /Author }, }, 'links': { @@ -111,19 +111,19 @@ BITBUCKET_WEBHOOK_PAYLOAD_SCHEMA = { }, }, 'required': ['html'], - }, + }, # /Links }, 'required': ['hash', 'message', 'date'], - }, + }, # /Target }, - 'required': ['target'], - }, + 'required': ['name', 'target'], + }, # /New }, - }, - }, + }, # /Changes item + }, # /Changes }, 'required': ['changes'], - }, + }, # / Push }, 'actor': { 'type': 'object', @@ -157,9 +157,9 @@ BITBUCKET_WEBHOOK_PAYLOAD_SCHEMA = { }, }, 'required': ['username'], - }, + }, # /Actor 'required': ['push', 'repository'], -} +} # /Root BITBUCKET_COMMIT_INFO_SCHEMA = { 'type': 'object', @@ -242,7 +242,7 @@ def get_transformed_webhook_payload(bb_payload, default_branch=None): config['default_branch'] = default_branch config['git_url'] = 'git@bitbucket.org:%s.git' % repository_name - config['commit_info.url'] = target['links.html.href'] + config['commit_info.url'] = target['links.html.href'] or '' config['commit_info.message'] = target['message'] config['commit_info.date'] = target['date'] diff --git a/buildtrigger/test/test_bitbuckethandler.py b/buildtrigger/test/test_bitbuckethandler.py index 991d90af1..12c653db5 100644 --- a/buildtrigger/test/test_bitbuckethandler.py +++ b/buildtrigger/test/test_bitbuckethandler.py @@ -1,6 +1,11 @@ +import json import pytest from buildtrigger.test.bitbucketmock import get_bitbucket_trigger +from buildtrigger.triggerutil import (SkipRequestException, ValidationRequestException, + InvalidPayloadException) +from endpoints.building import PreparedBuild +from util.morecollections import AttrDict @pytest.fixture def bitbucket_trigger(): @@ -20,3 +25,67 @@ def test_load_dockerfile_contents(subdir, contents): trigger = get_bitbucket_trigger(subdir) assert trigger.load_dockerfile_contents() == contents + +@pytest.mark.parametrize('payload, expected_error, expected_message', [ + ('{}', InvalidPayloadException, "'push' is a required property"), + + # Valid payload: + ('''{ + "push": { + "changes": [{ + "new": { + "name": "somechange", + "target": { + "hash": "aaaaaaa", + "message": "foo", + "date": "now", + "links": { + "html": { + "href": "somelink" + } + } + } + } + }] + }, + "repository": { + "full_name": "foo/bar" + } + }''', None, None), + + # Skip message: + ('''{ + "push": { + "changes": [{ + "new": { + "name": "somechange", + "target": { + "hash": "aaaaaaa", + "message": "[skip build] foo", + "date": "now", + "links": { + "html": { + "href": "somelink" + } + } + } + } + }] + }, + "repository": { + "full_name": "foo/bar" + } + }''', SkipRequestException, ''), +]) +def test_handle_trigger_request(bitbucket_trigger, payload, expected_error, expected_message): + def get_payload(): + return json.loads(payload) + + request = AttrDict(dict(get_json=get_payload)) + + if expected_error is not None: + with pytest.raises(expected_error) as ipe: + bitbucket_trigger.handle_trigger_request(request) + assert ipe.value.message == expected_message + else: + assert isinstance(bitbucket_trigger.handle_trigger_request(request), PreparedBuild) From 57528aa2bc6772d5a6b89b98c3c54232dc648187 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 13 Feb 2017 17:42:37 -0500 Subject: [PATCH 24/29] Add unit testing of gitlab trigger handler --- buildtrigger/gitlabhandler.py | 14 +- buildtrigger/test/gitlabmock.py | 186 +++++++++++++++++++++++ buildtrigger/test/test_githosthandler.py | 6 +- buildtrigger/test/test_gitlabhandler.py | 88 +++++++++++ 4 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 buildtrigger/test/gitlabmock.py create mode 100644 buildtrigger/test/test_gitlabhandler.py diff --git a/buildtrigger/gitlabhandler.py b/buildtrigger/gitlabhandler.py index 19d701ca0..90be9f698 100644 --- a/buildtrigger/gitlabhandler.py +++ b/buildtrigger/gitlabhandler.py @@ -48,6 +48,9 @@ GITLAB_WEBHOOK_PAYLOAD_SCHEMA = { 'items': { 'type': 'object', 'properties': { + 'id': { + 'type': 'string', + }, 'url': { 'type': 'string', }, @@ -67,7 +70,7 @@ GITLAB_WEBHOOK_PAYLOAD_SCHEMA = { 'required': ['email'], }, }, - 'required': ['url', 'message', 'timestamp'], + 'required': ['id', 'url', 'message', 'timestamp'], }, }, }, @@ -282,7 +285,8 @@ class GitLabBuildTrigger(BuildTriggerHandler): 'id': namespace['path'], 'title': namespace['name'], 'avatar_url': repo['owner']['avatar_url'], - 'score': 0, + 'score': 1, + 'url': gl_client.host + '/' + namespace['path'], } return list(namespaces.values()) @@ -486,18 +490,18 @@ class GitLabBuildTrigger(BuildTriggerHandler): def get_tag_sha(tag_name): tags = gl_client.getrepositorytags(repo['id']) if tags is False: - raise TriggerStartException('Could not find tags') + raise TriggerStartException('Could not find tag in repository') for tag in tags: if tag['name'] == tag_name: return tag['commit']['id'] - raise TriggerStartException('Could not find commit') + raise TriggerStartException('Could not find tag in repository') def get_branch_sha(branch_name): branch = gl_client.getbranch(repo['id'], branch_name) if branch is False: - raise TriggerStartException('Could not find branch') + raise TriggerStartException('Could not find branch in repository') return branch['commit']['id'] diff --git a/buildtrigger/test/gitlabmock.py b/buildtrigger/test/gitlabmock.py new file mode 100644 index 000000000..8a2212be9 --- /dev/null +++ b/buildtrigger/test/gitlabmock.py @@ -0,0 +1,186 @@ +from datetime import datetime +from mock import Mock + +from buildtrigger.gitlabhandler import GitLabBuildTrigger +from util.morecollections import AttrDict + +def get_gitlab_trigger(subdir=''): + trigger_obj = AttrDict(dict(auth_token='foobar', id='sometrigger')) + trigger = GitLabBuildTrigger(trigger_obj, { + 'build_source': 'foo/bar', + 'subdir': subdir, + 'username': 'knownuser' + }) + + trigger._get_authorized_client = get_mock_gitlab + return trigger + +def adddeploykey_mock(project_id, name, public_key): + return {'id': 'foo'} + +def addprojecthook_mock(project_id, webhook_url, push=False): + return {'id': 'foo'} + +def get_currentuser_mock(): + return { + 'username': 'knownuser' + } + +def project(namespace, name): + return { + 'id': '%s/%s' % (namespace, name), + 'default_branch': 'master', + 'namespace': { + 'id': namespace, + 'path': namespace, + 'name': namespace, + }, + 'path': name, + 'path_with_namespace': '%s/%s' % (namespace, name), + 'description': 'some %s repo' % name, + 'last_activity_at': str(datetime.utcfromtimestamp(0)), + 'web_url': 'https://bitbucket.org/%s/%s' % (namespace, name), + 'ssh_url_to_repo': 'git://%s/%s' % (namespace, name), + 'public': name != 'somerepo', + 'permissions': { + 'project_access': { + 'access_level': 50 if namespace == 'knownuser' else 0, + } + }, + 'owner': { + 'avatar_url': 'avatarurl', + } + } + +def getprojects_mock(page=1, per_page=100): + return [ + project('knownuser', 'somerepo'), + project('someorg', 'somerepo'), + project('someorg', 'anotherrepo'), + ] + +def getproject_mock(project_name): + if project_name == 'knownuser/somerepo': + return project('knownuser', 'somerepo') + + if project_name == 'foo/bar': + return project('foo', 'bar') + + return False + + +def getbranches_mock(project_id): + return [ + { + 'name': 'master', + 'commit': { + 'id': 'aaaaaaa', + } + }, + { + 'name': 'otherbranch', + 'commit': { + 'id': 'aaaaaaa', + } + }, + ] + +def getrepositorytags_mock(project_id): + return [ + { + 'name': 'sometag', + 'commit': { + 'id': 'aaaaaaa', + } + }, + { + 'name': 'someothertag', + 'commit': { + 'id': 'aaaaaaa', + } + }, + ] + +def getrepositorytree_mock(project_id, ref_name='master'): + return [ + {'name': 'README'}, + {'name': 'Dockerfile'}, + ] + +def getrepositorycommit_mock(project_id, commit_sha): + if commit_sha != 'aaaaaaa': + return False + + return { + 'id': 'aaaaaaa', + 'message': 'some message', + 'committed_date': 'now', + } + +def getusers_mock(search=None): + if search == 'knownuser': + return [ + { + 'username': 'knownuser', + 'avatar_url': 'avatarurl', + } + ] + + return False + +def getbranch_mock(repo_id, branch): + if branch != 'master' and branch != 'otherbranch': + return False + + return { + 'name': branch, + 'commit': { + 'id': 'aaaaaaa', + } + } + +def gettag_mock(repo_id, tag): + if tag != 'sometag' and tag != 'someothertag': + return False + + return { + 'name': tag, + 'commit': { + 'id': 'aaaaaaa', + } + } + +def getrawfile_mock(repo_id, branch_name, path): + if path == '/Dockerfile': + return 'hello world' + + if path == 'somesubdir/Dockerfile': + return 'hi universe' + + return False + +def get_mock_gitlab(): + mock_gitlab = Mock() + mock_gitlab.host = 'https://bitbucket.org' + + mock_gitlab.currentuser = Mock(side_effect=get_currentuser_mock) + mock_gitlab.getusers = Mock(side_effect=getusers_mock) + + mock_gitlab.getprojects = Mock(side_effect=getprojects_mock) + mock_gitlab.getproject = Mock(side_effect=getproject_mock) + mock_gitlab.getbranches = Mock(side_effect=getbranches_mock) + + mock_gitlab.getbranch = Mock(side_effect=getbranch_mock) + mock_gitlab.gettag = Mock(side_effect=gettag_mock) + + mock_gitlab.getrepositorytags = Mock(side_effect=getrepositorytags_mock) + mock_gitlab.getrepositorytree = Mock(side_effect=getrepositorytree_mock) + mock_gitlab.getrepositorycommit = Mock(side_effect=getrepositorycommit_mock) + + mock_gitlab.getrawfile = Mock(side_effect=getrawfile_mock) + + mock_gitlab.adddeploykey = Mock(side_effect=adddeploykey_mock) + mock_gitlab.addprojecthook = Mock(side_effect=addprojecthook_mock) + mock_gitlab.deletedeploykey = Mock(return_value=True) + mock_gitlab.deleteprojecthook = Mock(return_value=True) + return mock_gitlab diff --git a/buildtrigger/test/test_githosthandler.py b/buildtrigger/test/test_githosthandler.py index 04b87fa3c..2cf4f9175 100644 --- a/buildtrigger/test/test_githosthandler.py +++ b/buildtrigger/test/test_githosthandler.py @@ -3,9 +3,12 @@ import pytest from buildtrigger.triggerutil import TriggerStartException from buildtrigger.test.bitbucketmock import get_bitbucket_trigger from buildtrigger.test.githubmock import get_github_trigger +from buildtrigger.test.gitlabmock import get_gitlab_trigger from endpoints.building import PreparedBuild -@pytest.fixture(params=[get_github_trigger(), get_bitbucket_trigger()]) +# Note: This test suite executes a common set of tests against all the trigger types specified +# in this fixture. Each trigger's mock is expected to return the same data for all of these calls. +@pytest.fixture(params=[get_github_trigger(), get_bitbucket_trigger(), get_gitlab_trigger()]) def githost_trigger(request): return request.param @@ -111,7 +114,6 @@ def test_list_build_sources_for_namespace(namespace, expected, githost_trigger): def test_activate(githost_trigger): config, private_key = githost_trigger.activate('http://some/url') - assert 'deploy_key_id' in config assert 'private_key' in private_key diff --git a/buildtrigger/test/test_gitlabhandler.py b/buildtrigger/test/test_gitlabhandler.py new file mode 100644 index 000000000..25170cbca --- /dev/null +++ b/buildtrigger/test/test_gitlabhandler.py @@ -0,0 +1,88 @@ +import json +import pytest + +from buildtrigger.test.gitlabmock import get_gitlab_trigger +from buildtrigger.triggerutil import (SkipRequestException, ValidationRequestException, + InvalidPayloadException) +from endpoints.building import PreparedBuild +from util.morecollections import AttrDict + +@pytest.fixture +def gitlab_trigger(): + return get_gitlab_trigger() + + +def test_list_build_subdirs(gitlab_trigger): + assert gitlab_trigger.list_build_subdirs() == [''] + + +@pytest.mark.parametrize('subdir, contents', [ + ('', 'hello world'), + ('somesubdir', 'hi universe'), + ('unknownpath', None), +]) +def test_load_dockerfile_contents(subdir, contents): + trigger = get_gitlab_trigger(subdir) + assert trigger.load_dockerfile_contents() == contents + + +@pytest.mark.parametrize('email, expected_response', [ + ('unknown@email.com', None), + ('knownuser', {'username': 'knownuser', 'html_url': 'https://bitbucket.org/knownuser', + 'avatar_url': 'avatarurl'}), +]) +def test_lookup_user(email, expected_response, gitlab_trigger): + assert gitlab_trigger.lookup_user(email) == expected_response + + +@pytest.mark.parametrize('payload, expected_error, expected_message', [ + ('{}', SkipRequestException, ''), + + # Valid payload: + ('''{ + "ref": "refs/heads/master", + "checkout_sha": "aaaaaaa", + "repository": { + "git_ssh_url": "foobar" + }, + "commits": [ + { + "id": "aaaaaaa", + "url": "someurl", + "message": "hello there!", + "timestamp": "now" + } + ] + }''', None, None), + + # Skip message: + ('''{ + "ref": "refs/heads/master", + "checkout_sha": "aaaaaaa", + "repository": { + "git_ssh_url": "foobar" + }, + "commits": [ + { + "id": "aaaaaaa", + "url": "someurl", + "message": "[skip build] hello there!", + "timestamp": "now" + } + ] + }''', SkipRequestException, ''), +]) +def test_handle_trigger_request(gitlab_trigger, payload, expected_error, expected_message): + def get_payload(): + return json.loads(payload) + + request = AttrDict(dict(get_json=get_payload)) + + if expected_error is not None: + with pytest.raises(expected_error) as ipe: + gitlab_trigger.handle_trigger_request(request) + assert ipe.value.message == expected_message + else: + assert isinstance(gitlab_trigger.handle_trigger_request(request), PreparedBuild) + + From e025d8c2b28492fd9b25748c20f5f8b31c05f01a Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 13 Feb 2017 18:04:41 -0500 Subject: [PATCH 25/29] Add schema validation of namespaces and sources methods --- buildtrigger/basehandler.py | 84 ++++++++++++++++++++++++ buildtrigger/bitbuckethandler.py | 5 +- buildtrigger/githubhandler.py | 8 ++- buildtrigger/gitlabhandler.py | 5 +- buildtrigger/test/test_githosthandler.py | 3 +- 5 files changed, 96 insertions(+), 9 deletions(-) diff --git a/buildtrigger/basehandler.py b/buildtrigger/basehandler.py index b9f035dc0..f8ed97563 100644 --- a/buildtrigger/basehandler.py +++ b/buildtrigger/basehandler.py @@ -6,6 +6,79 @@ from endpoints.building import PreparedBuild from data import model from buildtrigger.triggerutil import get_trigger_config, InvalidServiceException +NAMESPACES_SCHEMA = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'personal': { + 'type': 'boolean', + 'description': 'True if the namespace is the user\'s personal namespace', + }, + 'score': { + 'type': 'number', + 'description': 'Score of the relevance of the namespace', + }, + 'avatar_url': { + 'type': 'string', + 'description': 'URL of the avatar for this namespace', + }, + 'url': { + 'type': 'string', + 'description': 'URL of the website to view the namespace', + }, + 'id': { + 'type': 'string', + 'description': 'Trigger-internal ID of the namespace', + }, + 'title': { + 'type': 'string', + 'description': 'Human-readable title of the namespace', + }, + }, + 'required': ['personal', 'score', 'avatar_url', 'url', 'id', 'title'], + }, +} + +BUILD_SOURCES_SCHEMA = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string', + 'description': 'The name of the repository, without its namespace', + }, + 'full_name': { + 'type': 'string', + 'description': 'The name of the repository, with its namespace', + }, + 'description': { + 'type': 'string', + 'description': 'The description of the repository. May be an empty string', + }, + 'last_updated': { + 'type': 'number', + 'description': 'The date/time when the repository was last updated, since epoch in UTC', + }, + 'url': { + 'type': 'string', + 'description': 'The URL at which to view the repository in the browser', + }, + 'has_admin_permissions': { + 'type': 'boolean', + 'description': 'True if the current user has admin permissions on the repository', + }, + 'private': { + 'type': 'boolean', + 'description': 'True if the repository is private', + }, + }, + 'required': ['name', 'full_name', 'description', 'last_updated', 'url', + 'has_admin_permissions', 'private'], + }, +} + METADATA_SCHEMA = { 'type': 'object', 'properties': { @@ -242,3 +315,14 @@ class BuildTriggerHandler(object): prepared.tags = [commit_sha[:7]] return prepared + + @classmethod + def build_sources_response(cls, sources): + validate(sources, BUILD_SOURCES_SCHEMA) + return sources + + @classmethod + def build_namespaces_response(cls, namespaces_dict): + namespaces = list(namespaces_dict.values()) + validate(namespaces, NAMESPACES_SCHEMA) + return namespaces diff --git a/buildtrigger/bitbuckethandler.py b/buildtrigger/bitbuckethandler.py index 2f26626a9..02e6b228f 100644 --- a/buildtrigger/bitbuckethandler.py +++ b/buildtrigger/bitbuckethandler.py @@ -416,7 +416,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler): 'score': 1, } - return list(namespaces.values()) + return BuildTriggerHandler.build_namespaces_response(namespaces) def list_build_sources_for_namespace(self, namespace): def repo_view(repo): @@ -437,7 +437,8 @@ class BitbucketBuildTrigger(BuildTriggerHandler): if not result: raise RepositoryReadException('Could not read repository list: ' + err_msg) - return [repo_view(repo) for repo in data if repo['owner'] == namespace] + repos = [repo_view(repo) for repo in data if repo['owner'] == namespace] + return BuildTriggerHandler.build_sources_response(repos) def list_build_subdirs(self): config = self.config diff --git a/buildtrigger/githubhandler.py b/buildtrigger/githubhandler.py index 3233f5f5d..b7eca92cc 100644 --- a/buildtrigger/githubhandler.py +++ b/buildtrigger/githubhandler.py @@ -300,7 +300,7 @@ class GithubBuildTrigger(BuildTriggerHandler): 'score': org.plan.private_repos if org.plan else 0, } - return list(namespaces.values()) + return BuildTriggerHandler.build_namespaces_response(namespaces) @_catch_ssl_errors def list_build_sources_for_namespace(self, namespace): @@ -318,7 +318,8 @@ class GithubBuildTrigger(BuildTriggerHandler): gh_client = self._get_client() usr = gh_client.get_user() if namespace == usr.login: - return [repo_view(repo) for repo in usr.get_repos() if repo.owner.login == namespace] + repos = [repo_view(repo) for repo in usr.get_repos() if repo.owner.login == namespace] + return BuildTriggerHandler.build_sources_response(repos) try: org = gh_client.get_organization(namespace) @@ -327,7 +328,8 @@ class GithubBuildTrigger(BuildTriggerHandler): except GithubException: return [] - return [repo_view(repo) for repo in org.get_repos(type='member')] + repos = [repo_view(repo) for repo in org.get_repos(type='member')] + return BuildTriggerHandler.build_sources_response(repos) @_catch_ssl_errors diff --git a/buildtrigger/gitlabhandler.py b/buildtrigger/gitlabhandler.py index 90be9f698..5d221874b 100644 --- a/buildtrigger/gitlabhandler.py +++ b/buildtrigger/gitlabhandler.py @@ -289,7 +289,7 @@ class GitLabBuildTrigger(BuildTriggerHandler): 'url': gl_client.host + '/' + namespace['path'], } - return list(namespaces.values()) + return BuildTriggerHandler.build_namespaces_response(namespaces) @_catch_timeouts def list_build_sources_for_namespace(self, namespace): @@ -313,7 +313,8 @@ class GitLabBuildTrigger(BuildTriggerHandler): gl_client = self._get_authorized_client() repositories = _paginated_iterator(gl_client.getprojects, RepositoryReadException) - return [repo_view(repo) for repo in repositories if repo['namespace']['path'] == namespace] + repos = [repo_view(repo) for repo in repositories if repo['namespace']['path'] == namespace] + return BuildTriggerHandler.build_sources_response(repos) @_catch_timeouts def list_build_subdirs(self): diff --git a/buildtrigger/test/test_githosthandler.py b/buildtrigger/test/test_githosthandler.py index 2cf4f9175..06e73578d 100644 --- a/buildtrigger/test/test_githosthandler.py +++ b/buildtrigger/test/test_githosthandler.py @@ -108,12 +108,11 @@ def test_list_build_source_namespaces(githost_trigger): }]), ]) def test_list_build_sources_for_namespace(namespace, expected, githost_trigger): - # TODO: schema validation on the resulting namespaces. assert githost_trigger.list_build_sources_for_namespace(namespace) == expected def test_activate(githost_trigger): - config, private_key = githost_trigger.activate('http://some/url') + _, private_key = githost_trigger.activate('http://some/url') assert 'private_key' in private_key From c9bddd9c9a3285b6f4e78b8595f63c1b86bc3b5a Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 13 Feb 2017 18:14:45 -0500 Subject: [PATCH 26/29] Remove unnecessary check --- buildtrigger/test/bitbucketmock.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/buildtrigger/test/bitbucketmock.py b/buildtrigger/test/bitbucketmock.py index c442f20dd..93d66d143 100644 --- a/buildtrigger/test/bitbucketmock.py +++ b/buildtrigger/test/bitbucketmock.py @@ -16,9 +16,6 @@ def get_bitbucket_trigger(subdir=''): return trigger def get_repo_path_contents(path, revision): - if revision != 'master': - return (False, None, None) - data = { 'files': [{'path': 'Dockerfile'}], } From b403906bc8a074a91d8075d386dab15c10618723 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 13 Feb 2017 18:32:32 -0500 Subject: [PATCH 27/29] Fix flakiness in new tests due to change in hash seed --- buildtrigger/test/test_customhandler.py | 2 +- buildtrigger/test/test_githosthandler.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/buildtrigger/test/test_customhandler.py b/buildtrigger/test/test_customhandler.py index 6d05cb2b9..03fcad9be 100644 --- a/buildtrigger/test/test_customhandler.py +++ b/buildtrigger/test/test_customhandler.py @@ -10,7 +10,7 @@ from util.morecollections import AttrDict ('', InvalidPayloadException, 'Missing expected payload'), ('{}', InvalidPayloadException, "'commit' is a required property"), - ('{"commit": "foo", "ref": "bar", "default_branch": "baz"}', + ('{"commit": "foo", "ref": "refs/heads/something", "default_branch": "baz"}', InvalidPayloadException, "u'foo' does not match '^([A-Fa-f0-9]{7,})$'"), ('{"commit": "11d6fbc", "ref": "refs/heads/something", "default_branch": "baz"}', None, None), diff --git a/buildtrigger/test/test_githosthandler.py b/buildtrigger/test/test_githosthandler.py index 06e73578d..53005d4c0 100644 --- a/buildtrigger/test/test_githosthandler.py +++ b/buildtrigger/test/test_githosthandler.py @@ -78,7 +78,12 @@ def test_list_build_source_namespaces(githost_trigger): 'id': 'someorg' } ] - assert githost_trigger.list_build_source_namespaces() == namespaces_expected + + found = githost_trigger.list_build_source_namespaces() + found.sort() + + namespaces_expected.sort() + assert found == namespaces_expected @pytest.mark.parametrize('namespace, expected', [ From c3edc3855a26373d484cbb738b51090c8caf5ca3 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 28 Feb 2017 17:13:00 -0500 Subject: [PATCH 28/29] Fix build trigger tests --- buildtrigger/githubhandler.py | 2 +- buildtrigger/test/test_gitlabhandler.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/buildtrigger/githubhandler.py b/buildtrigger/githubhandler.py index b7eca92cc..7c47df4ff 100644 --- a/buildtrigger/githubhandler.py +++ b/buildtrigger/githubhandler.py @@ -11,7 +11,7 @@ from github import (Github, UnknownObjectException, GithubException, from jsonschema import validate -from app import github_trigger +from app import app, github_trigger from buildtrigger.triggerutil import (RepositoryReadException, TriggerActivationException, TriggerDeactivationException, TriggerStartException, EmptyRepositoryException, ValidationRequestException, diff --git a/buildtrigger/test/test_gitlabhandler.py b/buildtrigger/test/test_gitlabhandler.py index 25170cbca..c88c591d6 100644 --- a/buildtrigger/test/test_gitlabhandler.py +++ b/buildtrigger/test/test_gitlabhandler.py @@ -40,6 +40,7 @@ def test_lookup_user(email, expected_response, gitlab_trigger): # Valid payload: ('''{ + "object_kind": "push", "ref": "refs/heads/master", "checkout_sha": "aaaaaaa", "repository": { @@ -57,6 +58,7 @@ def test_lookup_user(email, expected_response, gitlab_trigger): # Skip message: ('''{ + "object_kind": "push", "ref": "refs/heads/master", "checkout_sha": "aaaaaaa", "repository": { From 81e96d6c1d89b0fe139b5f7222194a69bc0b58b6 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 2 Mar 2017 16:33:47 -0500 Subject: [PATCH 29/29] Fix merge breakage --- buildtrigger/githubhandler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/buildtrigger/githubhandler.py b/buildtrigger/githubhandler.py index 7c47df4ff..a6df8a979 100644 --- a/buildtrigger/githubhandler.py +++ b/buildtrigger/githubhandler.py @@ -362,8 +362,14 @@ class GithubBuildTrigger(BuildTriggerHandler): def load_dockerfile_contents(self): config = self.config gh_client = self._get_client() - source = config['build_source'] + + try: + repo = gh_client.get_repo(source) + except GithubException as ghe: + message = ghe.data.get('message', 'Unable to list contents of repository: %s' % source) + raise RepositoryReadException(message) + path = self.get_dockerfile_path() try: file_info = repo.get_file_contents(path)