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
This commit is contained in:
parent
21b09a7451
commit
8e863b8cf5
47 changed files with 1835 additions and 1068 deletions
|
@ -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):
|
||||
|
|
|
@ -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 []
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 []
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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/<apirepopath:repository>/trigger/<trigger_uuid>/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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -183,6 +183,13 @@ def confirm_invite():
|
|||
def repository(path):
|
||||
return index('')
|
||||
|
||||
|
||||
@web.route('/repository/<path:path>/trigger/<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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
64
static/css/directives/ui/linear-workflow.css
Normal file
64
static/css/directives/ui/linear-workflow.css
Normal file
|
@ -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;
|
||||
}
|
106
static/css/directives/ui/manage-trigger-control.css
Normal file
106
static/css/directives/ui/manage-trigger-control.css
Normal file
|
@ -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;
|
||||
}
|
36
static/css/directives/ui/regex-match-view.css
Normal file
36
static/css/directives/ui/regex-match-view.css
Normal file
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
35
static/css/pages/trigger-setup.css
Normal file
35
static/css/pages/trigger-setup.css
Normal file
|
@ -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;
|
||||
}
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
<!-- Credentials -->
|
||||
<div ng-repeat="credential in trigger.config.credentials">
|
||||
<p>
|
||||
{{ credential.name }}:
|
||||
<div class="copy-box" value="credential.value"></div>
|
||||
</p>
|
||||
{{ credential.name }}:
|
||||
<div class="copy-box" value="credential.value"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
32
static/directives/dockerfile-path-select.html
Normal file
32
static/directives/dockerfile-path-select.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<div class="dockerfile-path-select-element">
|
||||
<div class="dropdown-select" placeholder="'Enter path containing a Dockerfile'"
|
||||
selected-item="selectedPath"
|
||||
lookahead-items="paths"
|
||||
handle-input="setPath(input)"
|
||||
handle-item-selected="setSelectedPath(datum.value)"
|
||||
allow-custom-input="true"
|
||||
hide-dropdown="!supportsFullListing">
|
||||
<!-- Icons -->
|
||||
<i class="dropdown-select-icon none-icon fa fa-folder-o fa-lg" ng-show="isUnknownPath"></i>
|
||||
<i class="dropdown-select-icon none-icon fa fa-folder fa-lg" style="color: black;" ng-show="!isUnknownPath"></i>
|
||||
<i class="dropdown-select-icon fa fa-folder fa-lg"></i>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<ul class="dropdown-select-menu pull-right" role="menu">
|
||||
<li ng-repeat="path in paths">
|
||||
<a ng-click="setSelectedPath(path)" ng-if="path">
|
||||
<i class="fa fa-folder fa-lg"></i> {{ path }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown-header" role="presentation" ng-show="!paths.length">
|
||||
No Dockerfiles found in repository
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px">
|
||||
<div class="co-alert co-alert-danger" ng-show="!isValidPath && currentPath">
|
||||
Path entered for folder containing Dockerfile is invalid: Must start with a '/'.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
6
static/directives/linear-workflow-section.html
Normal file
6
static/directives/linear-workflow-section.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<div class="linear-workflow-section-element" ng-show="sectionVisible"
|
||||
ng-class="isCurrentSection ? 'current-section' : ''">
|
||||
<form ng-submit="submitSection()">
|
||||
<div ng-transclude />
|
||||
</form>
|
||||
</div>
|
31
static/directives/linear-workflow.html
Normal file
31
static/directives/linear-workflow.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<div class="linear-workflow-element">
|
||||
<!-- Contents -->
|
||||
<div ng-transclude/>
|
||||
|
||||
<div class="bottom-controls">
|
||||
<table class="upcoming-table">
|
||||
<tr>
|
||||
<td>
|
||||
<!-- Next button -->
|
||||
<button class="btn btn-primary" ng-disabled="!currentSection.valid"
|
||||
ng-click="nextSection()"
|
||||
ng-class="{'btn-success': currentSection.index == sections.length - 1, 'btn-lg': currentSection.index == sections.length - 1}">
|
||||
<span ng-if="currentSection.index != sections.length - 1">Continue</span>
|
||||
<span ng-if="currentSection.index == sections.length - 1"><i class="fa fa-check-circle"></i>{{ doneTitle }}</span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<!-- Next sections -->
|
||||
<div class="upcoming" ng-if="currentSection.index != sections.length - 1">
|
||||
<b>Next:</b>
|
||||
<ul>
|
||||
<li ng-repeat="section in sections" ng-if="section.index > currentSection.index">
|
||||
{{ section.title }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
43
static/directives/manage-trigger-custom-git.html
Normal file
43
static/directives/manage-trigger-custom-git.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
<div class="manage-trigger-custom-git-element manage-trigger-control">
|
||||
<div class="linear-workflow" workflow-state="currentState" done-title="Create Trigger"
|
||||
workflow-complete="activateTrigger({'config': config})">
|
||||
<!-- Section: Repository -->
|
||||
<div class="linear-workflow-section row"
|
||||
section-id="repo"
|
||||
section-title="Git Repository"
|
||||
section-valid="config.build_source">
|
||||
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col">
|
||||
<h3>Enter repository</h3>
|
||||
<strong>
|
||||
Please enter the HTTP or SSH style URL used to clone your git repository:
|
||||
</strong>
|
||||
<input class="form-control" type="text" placeholder="git@example.com:namespace/repository.git"
|
||||
ng-model="config.build_source" ng-pattern="/(((http|https):\/\/)(.+)|\w+@(.+):(.+))/">
|
||||
</div>
|
||||
<div class="col-lg-5 col-md-5 hidden-sm hidden-xs help-col">
|
||||
<p>Custom git triggers support any externally accessible git repository, via either the normal git protocol or HTTP.</p>
|
||||
|
||||
<p><b>It is the responsibility of the git repository to invoke a webhook to tell <span class="registry-name" short="true"></span> that a commit has been added.</b></p>
|
||||
</div>
|
||||
</div><!-- /Section: Repository -->
|
||||
|
||||
<!-- Section: Build context -->
|
||||
<div class="linear-workflow-section row"
|
||||
section-id="dockerfile"
|
||||
section-title="Build context"
|
||||
section-valid="config.subdir">
|
||||
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col">
|
||||
<h3>Select build context directory</h3>
|
||||
<strong>Please select the build context directory under the git repository:</strong>
|
||||
<input class="form-control" type="text" placeholder="/"
|
||||
ng-model="config.subdir" ng-pattern="/^($|\/|\/.+)/">
|
||||
</div>
|
||||
<div class="col-lg-5 col-md-5 hidden-sm hidden-xs help-col">
|
||||
<p>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.</p>
|
||||
<p>If the Dockerfile is located at the root of the git repository, enter <code>/</code> as the build context directory.</p>
|
||||
</div>
|
||||
|
||||
</div><!-- /Section: Build context -->
|
||||
</div>
|
330
static/directives/manage-trigger-githost.html
Normal file
330
static/directives/manage-trigger-githost.html
Normal file
|
@ -0,0 +1,330 @@
|
|||
<div class="manage-trigger-githost-element manage-trigger-control">
|
||||
<div class="linear-workflow" workflow-state="currentState" done-title="Create Trigger"
|
||||
workflow-complete="createTrigger()">
|
||||
|
||||
<!-- Section: Namespace -->
|
||||
<div class="linear-workflow-section row"
|
||||
section-id="namespace"
|
||||
section-title="{{ 'Select ' + namespaceTitle }}"
|
||||
section-valid="local.selectedNamespace">
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.namespaces">
|
||||
<h3>Select {{ namespaceTitle }}</h3>
|
||||
<strong>
|
||||
Please select the {{ namespaceTitle }} under which the repository lives
|
||||
</strong>
|
||||
|
||||
<div class="co-top-bar">
|
||||
<div class="co-filter-box">
|
||||
<span class="page-controls" total-count="local.orderedNamespaces.entries.length" current-page="local.namespaceOptions.page" page-size="namespacesPerPage"></span>
|
||||
<input class="form-control" type="text" ng-model="local.namespaceOptions.filter" placeholder="Filter {{ namespaceTitle }}s...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="co-table" style="margin-top: 20px;">
|
||||
<thead>
|
||||
<td class="checkbox-col"></td>
|
||||
<td ng-class="TableService.tablePredicateClass('id', local.namespaceOptions.predicate, local.namespaceOptions.reverse)">
|
||||
<a ng-click="TableService.orderBy('id', local.namespaceOptions)">{{ namespaceTitle }}</a>
|
||||
</td>
|
||||
<td ng-class="TableService.tablePredicateClass('score', local.namespaceOptions.predicate, local.namespaceOptions.reverse)"
|
||||
class="importance-col hidden-xs">
|
||||
<a ng-click="TableService.orderBy('score', local.namespaceOptions)">Importance</a>
|
||||
</td>
|
||||
</thead>
|
||||
|
||||
<tr class="co-checkable-row"
|
||||
ng-repeat="namespace in local.orderedNamespaces.visibleEntries | slice:(namespacesPerPage * local.namespaceOptions.page):(namespacesPerPage * (local.namespaceOptions.page + 1))"
|
||||
ng-class="local.selectedNamespace == namespace ? 'checked' : ''"
|
||||
bindonce>
|
||||
<td>
|
||||
<input type="radio" ng-model="local.selectedNamespace" ng-value="namespace">
|
||||
</td>
|
||||
<td>
|
||||
<img class="namespace-avatar" ng-src="{{ namespace.avatar_url }}">
|
||||
<span class="anchor" href="{{ namespace.url }}" is-text-only="!namespace.url">{{ namespace.id }}</span>
|
||||
</td>
|
||||
<td class="importance-col hidden-xs">
|
||||
<span class="strength-indicator" value="::namespace.score" maximum="::local.maxScore"
|
||||
log-base="10"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="empty" ng-if="local.namespaces.length && !local.orderedNamespaces.entries.length"
|
||||
style="margin-top: 20px;">
|
||||
<div class="empty-primary-msg">No matching {{ namespaceTitle }} found.</div>
|
||||
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8 col-md-8 col-sm-12 main-col" ng-show="!local.namespaces">
|
||||
<span class="cor-loader-inline"></span> Retrieving {{ namespaceTitle }}s
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col" ng-show="local.namespaces">
|
||||
<p>
|
||||
<span class="registry-name"></span> has been granted access to read and view these {{ namespaceTitle }}s.
|
||||
</p>
|
||||
<p>
|
||||
Don't see an expected {{ namespaceTitle }} here? Please make sure third-party access is enabled for <span class="registry-name"></span> under that {{ namespaceTitle }}.
|
||||
</p>
|
||||
</div>
|
||||
</div><!-- /Section: Namespace -->
|
||||
|
||||
<!-- Section: Repository -->
|
||||
<div class="linear-workflow-section row"
|
||||
section-id="repo"
|
||||
section-title="Select Repository"
|
||||
section-valid="local.selectedRepository">
|
||||
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.repositories">
|
||||
<h3>Select Repository</h3>
|
||||
<strong>
|
||||
Select a repository in
|
||||
<img class="namespace-avatar" ng-src="{{ local.selectedNamespace.avatar_url }}">
|
||||
{{ local.selectedNamespace.id }}
|
||||
</strong>
|
||||
|
||||
<div class="co-top-bar">
|
||||
<div class="co-filter-box">
|
||||
<span class="page-controls" total-count="local.orderedRepositories.entries.length" current-page="local.repositoryOptions.page" page-size="repositoriesPerPage"></span>
|
||||
<input class="form-control" type="text" ng-model="local.repositoryOptions.filter" placeholder="Filter repositories...">
|
||||
<div class="filter-options">
|
||||
<label><input type="checkbox" ng-model="local.repositoryOptions.hideStale">Hide stale repositories</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="co-table" style="margin-top: 20px;">
|
||||
<thead>
|
||||
<td class="checkbox-col"></td>
|
||||
<td ng-class="TableService.tablePredicateClass('name', local.repositoryOptions.predicate, local.repositoryOptions.reverse)" class="nowrap-col">
|
||||
<a ng-click="TableService.orderBy('name', local.repositoryOptions)">Repository Name</a>
|
||||
</td>
|
||||
<td ng-class="TableService.tablePredicateClass('last_updated_datetime', local.repositoryOptions.predicate, local.repositoryOptions.reverse)"
|
||||
class="last-updated-col nowrap-col">
|
||||
<a ng-click="TableService.orderBy('last_updated_datetime', local.namespaceOptions)">Last Updated</a>
|
||||
</td>
|
||||
<td class="hidden-xs">Description</td>
|
||||
</thead>
|
||||
|
||||
<tr class="co-checkable-row"
|
||||
ng-repeat="repository in local.orderedRepositories.visibleEntries | slice:(repositoriesPerPage * local.repositoryOptions.page):(repositoriesPerPage * (local.repositoryOptions.page + 1))"
|
||||
ng-class="local.selectedRepository == repository ? 'checked' : ''"
|
||||
bindonce>
|
||||
<td>
|
||||
<span ng-if="!repository.has_admin_permissions">
|
||||
<i class="fa fa-exclamation-triangle"
|
||||
data-title="Admin access is required to add the webhook trigger to this repository" bs-tooltip></i>
|
||||
</span>
|
||||
<input type="radio" ng-model="local.selectedRepository" ng-value="repository"
|
||||
ng-if="repository.has_admin_permissions">
|
||||
</td>
|
||||
<td class="nowrap-col">
|
||||
<i class="service-icon fa {{ getTriggerIcon() }}"></i>
|
||||
<span class="anchor" href="{{ repository.url }}" is-text-only="!repository.url">{{ repository.name }}</span>
|
||||
</td>
|
||||
<td class="last-updated-col nowrap-col">
|
||||
<span am-time-ago="repository.last_updated_datetime"></span>
|
||||
</td>
|
||||
<td class="hidden-xs">
|
||||
<span ng-if="repository.description">{{ repository.description }}</span>
|
||||
<span class="empty-description" ng-if="!repository.description">(None)</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="empty" ng-if="local.repositories.length && !local.orderedRepositories.entries.length"
|
||||
style="margin-top: 20px;">
|
||||
<div class="empty-primary-msg">No matching repositories found.</div>
|
||||
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8 col-md-8 col-sm-12 main-col" ng-show="!local.repositories">
|
||||
<span class="cor-loader-inline"></span> Retrieving repositories
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col" ng-show="local.repositories">
|
||||
<p>
|
||||
A webhook will be added to the selected repository in order to detect when new commits are made.
|
||||
</p>
|
||||
<p>
|
||||
Don't see an expected repository here? Please make sure you have admin access on that repository.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div><!-- /Section: Repository -->
|
||||
|
||||
<!-- Section: Trigger Options -->
|
||||
<div class="linear-workflow-section row"
|
||||
section-id="triggeroptions"
|
||||
section-title="Configure Trigger"
|
||||
section-valid="local.triggerOptions">
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.repositoryRefs">
|
||||
<h3>Configure Trigger</h3>
|
||||
<strong>
|
||||
Configure trigger options for
|
||||
<img class="namespace-avatar" ng-src="{{ local.selectedNamespace.avatar_url }}">
|
||||
{{ local.selectedNamespace.id }}/{{ local.selectedRepository.name }}
|
||||
</strong>
|
||||
|
||||
<div class="radio" style="margin-top: 20px;">
|
||||
<label>
|
||||
<input type="radio" name="optionRadio" ng-model="local.triggerOptions.hasBranchTagFilter" ng-value="false">
|
||||
<div class="title">Trigger for all branches and tags <span class="weak">(default)</span></div>
|
||||
<div class="description">Build a container image for each commit across all branches and tags</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="optionRadio" ng-model="local.triggerOptions.hasBranchTagFilter" ng-value="true">
|
||||
<div class="title">Trigger only on branches and tags matching a regular expression</div>
|
||||
<div class="description">Only build container images for a subset of branches and/or tags.</div>
|
||||
<div class="extended" ng-if="local.triggerOptions.hasBranchTagFilter">
|
||||
<table>
|
||||
<tr>
|
||||
<td style="white-space: nowrap;">Regular Expression:</td>
|
||||
<td>
|
||||
<input type="text" class="form-control" ng-model="local.triggerOptions.branchTagFilter" required>
|
||||
<div class="description">Examples: heads/master, tags/tagname, heads/.+</div>
|
||||
<div class="regex-match-view"
|
||||
items="local.repositoryFullRefs"
|
||||
regex="local.triggerOptions.branchTagFilter"
|
||||
ng-if="local.triggerOptions.branchTagFilter"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8 col-md-8 col-sm-12 main-col" ng-show="!local.repositoryRefs">
|
||||
<span class="cor-loader-inline"></span> Retrieving repository refs
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col">
|
||||
<p>Do you want to build a new container image for commits across all branches and tags, or limit to a subset?</p>
|
||||
<p>For example, if you use release branches instead of <code>master</code> for building versions of your software, you can configure the trigger to only build images for these branches.</p>
|
||||
<p>All images built will be tagged with the name of the branch or tag whose change invoked the trigger</p>
|
||||
</div>
|
||||
</div><!-- /Section: Trigger Options -->
|
||||
|
||||
<!-- Section: Dockerfile Location -->
|
||||
<div class="linear-workflow-section row"
|
||||
section-id="dockerfilelocation"
|
||||
section-title="Select Dockerfile"
|
||||
section-valid="local.hasValidDockerfilePath">
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.dockerfileLocations.status == 'error'">
|
||||
<div class="co-alert co-alert-warning">
|
||||
{{ local.dockerfileLocations.message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.dockerfileLocations.status == 'success'">
|
||||
<h3>Select Dockerfile</h3>
|
||||
<strong>
|
||||
Please select the location of the Dockerfile to build when this trigger is invoked
|
||||
</strong>
|
||||
|
||||
<div class="dockerfile-path-select" current-path="local.dockerfilePath" paths="local.dockerfileLocations.subdir"
|
||||
supports-full-listing="true" is-valid-path="local.hasValidDockerfilePath"></div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8 col-md-8 col-sm-12 main-col" ng-show="!local.dockerfileLocations">
|
||||
<span class="cor-loader-inline"></span> Retrieving Dockerfile locations
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col">
|
||||
<p>Please select the location containing the Dockerfile to be built.</p>
|
||||
<p>The build context will start at the location selected.</p>
|
||||
</div>
|
||||
</div><!-- /Section: Dockerfile Location -->
|
||||
|
||||
<!-- Section: Verification and Robot Account -->
|
||||
<div class="linear-workflow-section row"
|
||||
section-id="verification"
|
||||
section-title="Confirm"
|
||||
section-valid="local.triggerAnalysis.status != 'error' && (local.triggerAnalysis.status != 'requiresrobot' || local.robotAccount != null)">
|
||||
<!-- Error -->
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.triggerAnalysis.status == 'error'">
|
||||
<h3 class="error"><i class="fa fa-exclamation-circle"></i> Verification Error</h3>
|
||||
<strong>
|
||||
There was an error when verifying the state of <img class="namespace-avatar" ng-src="{{ local.selectedNamespace.avatar_url }}">
|
||||
{{ local.selectedNamespace.id }}/{{ local.selectedRepository.name }}
|
||||
</strong>
|
||||
|
||||
{{ local.triggerAnalysis.message }}
|
||||
</div>
|
||||
|
||||
<!-- Warning -->
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.triggerAnalysis.status == 'warning'">
|
||||
<h3 class="warning"><i class="fa fa-exclamation-triangle"></i> Verification Warning</h3>
|
||||
{{ local.triggerAnalysis.message }}
|
||||
</div>
|
||||
|
||||
<!-- Public base -->
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.triggerAnalysis.status == 'publicbase'">
|
||||
<h3 class="success"><i class="fa fa-check-circle"></i> Ready to go!</h3>
|
||||
<strong>Click "Create Trigger" to complete setup of this build trigger</strong>
|
||||
</div>
|
||||
|
||||
<!-- Requires robot and is not admin -->
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.triggerAnalysis.status == 'requiresrobot' && !local.triggerAnalysis.is_admin">
|
||||
<h3>Robot Account Required</h3>
|
||||
<p>The selected Dockerfile in the selected repository depends upon a private base image</p>
|
||||
<p>A robot account with access to the base image is required to setup this trigger, but you are not the administrator of this namespace.</p>
|
||||
<p>Administrative access is required to continue to ensure security of the robot credentials.</p>
|
||||
</div>
|
||||
|
||||
<!-- Requires robot and is admin -->
|
||||
<div class="col-lg-7 col-md-7 col-sm-12 main-col" ng-show="local.triggerAnalysis.status == 'requiresrobot' && local.triggerAnalysis.is_admin">
|
||||
<h3>Select Robot Account</h3>
|
||||
<strong>
|
||||
The selected Dockerfile in the selected repository depends upon a private base image. Select a robot account with access:
|
||||
</strong>
|
||||
|
||||
<div class="co-top-bar">
|
||||
<div class="co-filter-box">
|
||||
<span class="page-controls" total-count="local.orderedRobotAccounts.entries.length" current-page="local.robotOptions.page" page-size="robotsPerPage"></span>
|
||||
<input class="form-control" type="text" ng-model="local.robotOptions.filter" placeholder="Filter robot accounts...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="co-table" style="margin-top: 20px;">
|
||||
<thead>
|
||||
<td class="checkbox-col"></td>
|
||||
<td ng-class="TableService.tablePredicateClass('name', local.robotOptions.predicate, local.robotOptions.reverse)">
|
||||
<a ng-click="TableService.orderBy('name', local.robotOptions)">Robot Account</a>
|
||||
</td>
|
||||
<td ng-class="TableService.tablePredicateClass('can_read', local.robotOptions.predicate, local.robotOptions.reverse)">
|
||||
<a ng-click="TableService.orderBy('can_read', local.robotOptions)">Has Read Access</a>
|
||||
</td>
|
||||
</thead>
|
||||
|
||||
<tr class="co-checkable-row"
|
||||
ng-repeat="robot in local.orderedRobotAccounts.visibleEntries | slice:(robotsPerPage * local.namespaceOptions.page):(robotsPerPage * (local.robotOptions.page + 1))"
|
||||
ng-class="local.robotAccount == robot ? 'checked' : ''"
|
||||
bindonce>
|
||||
<td>
|
||||
<input type="radio" ng-model="local.robotAccount" ng-value="robot">
|
||||
</td>
|
||||
<td>
|
||||
<span class="entity-reference" entity="robot"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="robot.can_read" class="success">Can Read</span>
|
||||
<span ng-if="!robot.can_read">Read access will be added if selected</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="empty" ng-if="local.triggerAnalysis.robots.length && !local.orderedRobotAccounts.entries.length"
|
||||
style="margin-top: 20px;">
|
||||
<div class="empty-primary-msg">No matching robot accounts found.</div>
|
||||
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 col-md-4 hidden-sm hidden-xs help-col" ng-if="local.triggerAnalysis.status == 'requiresrobot' && local.triggerAnalysis.is_admin">
|
||||
<p>The Dockerfile you selected utilizes a private base image.</p>
|
||||
<p>In order for the <span class="registry-name"></span> to pull the base image during the build process, a robot account with access must be selected.</p>
|
||||
<p>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.</p>
|
||||
</div>
|
||||
</div><!-- /Section: Robot Account -->
|
||||
|
||||
</div>
|
29
static/directives/regex-match-view.html
Normal file
29
static/directives/regex-match-view.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<div class="regex-match-view-element">
|
||||
<div ng-if="filterMatches(regex, items, false) == null">
|
||||
<i class="fa fa-exclamation-triangle"></i>Invalid Regular Expression!
|
||||
</div>
|
||||
<div ng-if="filterMatches(regex, items, false) != null">
|
||||
<table class="match-table">
|
||||
<tr>
|
||||
<td>Matching:</td>
|
||||
<td>
|
||||
<ul class="matching match-list">
|
||||
<li ng-repeat="item in filterMatches(regex, items, true)">
|
||||
<i class="fa {{ item.icon }}"></i>{{ item.title }}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Not Matching:</td>
|
||||
<td>
|
||||
<ul class="not-matching match-list">
|
||||
<li ng-repeat="item in filterMatches(regex, items, false)">
|
||||
<i class="fa {{ item.icon }}"></i>{{ item.title }}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
|
@ -126,10 +126,8 @@
|
|||
|
||||
<tr ng-repeat="trigger in triggers | filter:{'is_active':false}">
|
||||
<td colspan="5" style="text-align: center">
|
||||
<span class="cor-loader-inline"></span>
|
||||
Trigger Setup in progress:
|
||||
<a ng-click="setupTrigger(trigger)">Resume</a> |
|
||||
<a ng-click="deleteTrigger(trigger)">Cancel</a>
|
||||
This build trigger has not had its setup completed:
|
||||
<a ng-click="deleteTrigger(trigger)">Delete Trigger</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
@ -185,14 +183,6 @@
|
|||
build-started="handleBuildStarted(build)">
|
||||
</div>
|
||||
|
||||
<!-- Setup trigger dialog-->
|
||||
<div class="setup-trigger-dialog"
|
||||
repository="repository"
|
||||
trigger="currentSetupTrigger"
|
||||
canceled="cancelSetupTrigger(trigger)"
|
||||
counter="showTriggerSetupCounter"
|
||||
trigger-runner="askRunTrigger(trigger)"></div>
|
||||
|
||||
<!-- Manual trigger dialog -->
|
||||
<div class="manual-trigger-build-dialog"
|
||||
repository="repository"
|
||||
|
@ -201,5 +191,4 @@
|
|||
build-started="handleBuildStarted(build)"></div>
|
||||
|
||||
<!-- /Dialogs -->
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
<div class="setup-trigger-directive-element">
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="setupTriggerModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Setup new build trigger</h4>
|
||||
</div>
|
||||
<div class="modal-body loading" ng-show="currentView == 'activating'">
|
||||
<span class="cor-loader-inline"></span> Setting up trigger...
|
||||
</div>
|
||||
<div class="modal-body" ng-show="currentView != 'activating'">
|
||||
<!-- Trigger-specific setup -->
|
||||
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
|
||||
<div ng-switch-when="custom-git">
|
||||
<div class="trigger-setup-custom" repository="repository" trigger="trigger"
|
||||
next-step-counter="nextStepCounter" current-step-valid="state.stepValid"
|
||||
analyze="checkAnalyze(isValid)"></div>
|
||||
</div>
|
||||
<div ng-switch-default>
|
||||
<div class="trigger-setup-githost" repository="repository" trigger="trigger"
|
||||
kind="{{ trigger.service }}"
|
||||
next-step-counter="nextStepCounter" current-step-valid="state.stepValid"
|
||||
analyze="checkAnalyze(isValid)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading pull information -->
|
||||
<div ng-show="currentView == 'analyzing'" class="loading">
|
||||
<span class="cor-loader-inline"></span> Checking pull credential requirements...
|
||||
</div>
|
||||
|
||||
<!-- Pull information -->
|
||||
<div class="trigger-option-section" ng-show="currentView == 'analyzed'">
|
||||
|
||||
<!-- Messaging -->
|
||||
<div ng-switch on="pullInfo.analysis.status">
|
||||
<div ng-switch-when="error" class="alert alert-danger">{{ pullInfo.analysis.message }}</div>
|
||||
<div ng-switch-when="warning" class="alert alert-warning">{{ pullInfo.analysis.message }}</div>
|
||||
<div ng-switch-when="notimplemented" class="alert alert-warning">
|
||||
<p>For {{ TriggerService.getTitle(trigger.service) }} triggers, we are unable to determine dependencies automatically.</p>
|
||||
<p>If the git repository being built depends on a private base image, you must manually select a robot account with the proper permissions.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dockerfile-found" ng-if="pullInfo.analysis.is_public === false">
|
||||
<div class="dockerfile-found-content">
|
||||
A robot account is <strong>required</strong> for this build trigger because the
|
||||
Dockerfile found
|
||||
pulls from the private <span class="registry-name"></span> repository
|
||||
|
||||
<a href="/repository/{{ pullInfo.analysis.namespace }}/{{ pullInfo.analysis.name }}" ng-safenewtab>
|
||||
{{ pullInfo.analysis.namespace }}/{{ pullInfo.analysis.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px">
|
||||
Please select the credentials to use when pulling the base image:
|
||||
</div>
|
||||
<div ng-if="!isNamespaceAdmin(repository.namespace)" style="color: #aaa;">
|
||||
<strong>Note:</strong> In order to set pull credentials for a build trigger, you must be an
|
||||
Administrator of the namespace <strong>{{ repository.namespace }}</strong>
|
||||
</div>
|
||||
|
||||
<!-- Namespace admin -->
|
||||
<div ng-show="isNamespaceAdmin(repository.namespace)">
|
||||
<!-- Select credentials -->
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="pullInfo.is_public ? 'active btn-info' : ''"
|
||||
ng-click="pullInfo.is_public = true">
|
||||
None
|
||||
</button>
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="pullInfo.is_public ? '' : 'active btn-info'"
|
||||
ng-click="pullInfo.is_public = false">
|
||||
<i class="fa ci-robot"></i>
|
||||
Robot account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Robot Select -->
|
||||
<div ng-show="!pullInfo.is_public" style="margin-top: 10px">
|
||||
<div class="entity-search" namespace="repository.namespace"
|
||||
placeholder="'Select robot account for pulling...'"
|
||||
current-entity="pullInfo.pull_entity"
|
||||
allowed-entities="['robot']"></div>
|
||||
|
||||
<div ng-if="pullInfo.analysis.robots.length" style="margin-top: 20px; margin-bottom: 0px;">
|
||||
<strong>Note</strong>: We've automatically selected robot account
|
||||
<span class="entity-reference" entity="pullInfo.analysis.robots[0]"></span>,
|
||||
since it has access to the private repository.
|
||||
</div>
|
||||
<div ng-if="!pullInfo.analysis.robots.length && pullInfo.analysis.name"
|
||||
style="margin-top: 20px; margin-bottom: 0px;">
|
||||
<strong>Note</strong>: No robot account currently has access to the private repository. Please create one and/or assign access in the
|
||||
<a href="/repository/{{ pullInfo.analysis.namespace }}/{{ pullInfo.analysis.name }}/admin" ng-safenewtab>
|
||||
repository's admin panel.
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="trigger-option-section" ng-show="currentView == 'postActivation'">
|
||||
<div ng-if="trigger.config.credentials" class="credentials" trigger="trigger"></div>
|
||||
<div ng-if="!trigger.config.credentials">
|
||||
<div class="alert alert-success">The trigger has been successfully created.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer" ng-show="currentView != 'activating'">
|
||||
<button type="button" class="btn btn-primary" ng-disabled="!state.stepValid"
|
||||
ng-click="nextStepCounter = nextStepCounter + 1"
|
||||
ng-show="currentView == 'config'">Next</button>
|
||||
|
||||
<button type="button" class="btn btn-primary"
|
||||
ng-disabled="!trigger.$ready || (!pullInfo['is_public'] && !pullInfo['pull_entity'])"
|
||||
ng-click="activate()"
|
||||
ng-show="currentView == 'analyzed'">Create Trigger</button>
|
||||
|
||||
<button type="button" class="btn btn-success" ng-click="runTriggerNow()"
|
||||
ng-if="currentView == 'postActivation'">Run Trigger Now</button>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ currentView == 'postActivation' ? 'Done' : 'Cancel' }}</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||
<div class="step-view-step-content">
|
||||
<div ng-show="!loading">
|
||||
<div ng-transclude></div>
|
||||
</div>
|
||||
<div ng-show="loading" class="loading-message">
|
||||
<span class="cor-loader-inline"></span>
|
||||
{{ loadMessage }}
|
||||
</div>
|
||||
</div>
|
|
@ -1,3 +0,0 @@
|
|||
<div class="step-view-element">
|
||||
<div class="transcluded" ng-transclude>
|
||||
</div>
|
|
@ -1,40 +0,0 @@
|
|||
<div class="trigger-setup-custom-element">
|
||||
<div class="selected-info" ng-show="nextStepCounter > 0">
|
||||
<table style="width: 100%;">
|
||||
<tr ng-show="nextStepCounter > 0">
|
||||
<td width="200px">Repository</td>
|
||||
<td>{{ state.build_source }}</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="nextStepCounter > 1">
|
||||
<td>Dockerfile Location:</td>
|
||||
<td>
|
||||
<div class="dockerfile-location">
|
||||
<i class="fa fa-folder fa-lg"></i> {{ state.subdir || '/' }}
|
||||
</div>
|
||||
</td>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Step view -->
|
||||
<div class="step-view" next-step-counter="nextStepCounter" current-step-valid="currentStepValid"
|
||||
steps-completed="stepsCompleted()">
|
||||
|
||||
<!-- Git URL Input -->
|
||||
<!-- TODO(jschorr): make nopLoad(callback) no longer required -->
|
||||
<div class="step-view-step" complete-condition="trigger['config']['build_source']" load-callback="nopLoad(callback)"
|
||||
load-message="Loading Git URL Input">
|
||||
<div style="margin-bottom: 12px;">Please enter an HTTP or SSH style URL used to clone your git repository:</div>
|
||||
<input class="form-control" type="text" placeholder="git@example.com:namespace/repository.git" style="width: 100%;"
|
||||
ng-model="state.build_source" ng-pattern="/(((http|https):\/\/)(.+)|\w+@(.+):(.+))/">
|
||||
</div>
|
||||
|
||||
<!-- Dockerfile folder select -->
|
||||
<div class="step-view-step" complete-condition="trigger.$ready" load-callback="nopLoad(callback)"
|
||||
load-message="Loading Folder Input">
|
||||
<div style="margin-bottom: 12px">Dockerfile Location:</div>
|
||||
<input class="form-control" type="text" placeholder="/" style="width: 100%;"
|
||||
ng-model="state.subdir" ng-pattern="/^($|\/|\/.+)/">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,201 +0,0 @@
|
|||
<div class="trigger-setup-githost-element">
|
||||
<!-- Current selected info -->
|
||||
<div class="selected-info" ng-show="nextStepCounter > 0">
|
||||
<table style="width: 100%;">
|
||||
<tr ng-show="state.currentRepo && nextStepCounter > 0">
|
||||
<td width="200px">
|
||||
Repository:
|
||||
</td>
|
||||
<td>
|
||||
<div class="current-repo">
|
||||
<i class="dropdown-select-icon org-icon fa" ng-class="scmIcon(kind)"
|
||||
ng-show="!state.currentRepo.avatar_url"></i>
|
||||
<img class="dropdown-select-icon org-icon"
|
||||
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/img/empty.png' }}"
|
||||
ng-show="state.currentRepo.avatar_url">
|
||||
{{ state.currentRepo.repo }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="nextStepCounter > 1">
|
||||
<td>
|
||||
Branches and Tags:
|
||||
</td>
|
||||
<td>
|
||||
<div class="ref-filter">
|
||||
<span ng-if="!state.hasBranchTagFilter">(Build All)</span>
|
||||
<span ng-if="state.hasBranchTagFilter">Regular Expression: <code>{{ state.branchTagFilter }}</code></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="nextStepCounter > 2">
|
||||
<td>
|
||||
Dockerfile Location:
|
||||
</td>
|
||||
<td>
|
||||
<div class="dockerfile-location">
|
||||
<i class="fa fa-folder fa-lg"></i> {{ state.currentLocation || '(Repository Root)' }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Step view -->
|
||||
<div class="step-view" next-step-counter="nextStepCounter" current-step-valid="currentStepValid"
|
||||
steps-completed="stepsCompleted()">
|
||||
<!-- Repository select -->
|
||||
<div class="step-view-step" complete-condition="state.currentRepo" load-callback="loadRepositories(callback)"
|
||||
load-message="Loading Repositories">
|
||||
<div style="margin-bottom: 12px">Please choose the repository that will trigger the build:</div>
|
||||
<div class="dropdown-select" placeholder="'Enter or select a repository'" selected-item="state.currentRepo"
|
||||
lookahead-items="repoLookahead" allow-custom-input="true">
|
||||
<!-- Icons -->
|
||||
<i class="dropdown-select-icon none-icon fa fa-lg" ng-class="scmIcon(kind)"></i>
|
||||
<img class="dropdown-select-icon org-icon"
|
||||
ng-show="state.currentRepo.avatar_url"
|
||||
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/img/empty.png' }}">
|
||||
<i class="dropdown-select-icon org-icon fa fa-lg" ng-class="scmIcon(kind)"
|
||||
ng-show="!state.currentRepo.avatar_url"></i>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<ul class="dropdown-select-menu scrollable-menu" role="menu">
|
||||
<li ng-repeat-start="org in orgs" role="presentation" class="dropdown-header org-header">
|
||||
<img ng-src="{{ org.info.avatar_url }}" class="org-icon">{{ org.info.name }}
|
||||
</li>
|
||||
<li ng-repeat="repo in org.repos" class="trigger-repo-listing">
|
||||
<a ng-click="selectRepo(repo, org)">
|
||||
<i class="fa fa-lg" ng-class="scmIcon(kind)"></i> {{ repo }}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="divider" ng-repeat-end ng-show="$index < orgs.length - 1"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Branch/Tag filter/select -->
|
||||
<div class="step-view-step" complete-condition="!state.hasBranchTagFilter || state.branchTagFilter"
|
||||
load-callback="loadBranchesAndTags(callback)"
|
||||
load-message="Loading Branches and Tags">
|
||||
|
||||
<div style="margin-bottom: 12px">Please choose the branches and tags to which this trigger will apply:</div>
|
||||
<div style="margin-left: 20px;">
|
||||
<div class="btn-group btn-group-sm" style="margin-bottom: 12px">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="state.hasBranchTagFilter ? '' : 'active btn-info'" ng-click="state.hasBranchTagFilter = false">
|
||||
All Branches and Tags
|
||||
</button>
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-class="state.hasBranchTagFilter ? 'active btn-info' : ''" ng-click="state.hasBranchTagFilter = true">
|
||||
Matching Regular Expression
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ng-show="state.hasBranchTagFilter" style="margin-top: 10px;">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="text" ng-model="state.branchTagFilter"
|
||||
placeholder="(Regular expression. Examples: heads/branchname, tags/tagname)" required>
|
||||
<div class="dropdown input-group-addon">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li><a ng-click="state.branchTagFilter = 'heads/.+'">
|
||||
<i class="fa fa-code-fork"></i>All Branches</a>
|
||||
</li>
|
||||
<li><a ng-click="state.branchTagFilter = 'tags/.+'">
|
||||
<i class="fa fa-tag"></i>All Tags</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<div style="margin-top: 10px">
|
||||
<div class="ref-matches" ng-if="branchNames.length">
|
||||
<span class="kind">Branches:</span>
|
||||
<ul class="matching-refs branches">
|
||||
<li ng-repeat="branchName in branchNames | limitTo:20"
|
||||
class="ref-reference"
|
||||
ng-class="isMatching('heads', branchName, state.branchTagFilter) ? 'match' : 'not-match'">
|
||||
<span ng-click="addRef('heads', branchName)" ng-safenewtab>
|
||||
{{ branchName }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<span ng-if="branchNames.length > 20">...</span>
|
||||
</div>
|
||||
<div class="ref-matches" ng-if="tagNames.length">
|
||||
<span class="kind">Tags:</span>
|
||||
<ul class="matching-refs tags">
|
||||
<li ng-repeat="tagName in tagNames | limitTo:20"
|
||||
class="ref-reference"
|
||||
ng-class="isMatching('tags', tagName, state.branchTagFilter) ? 'match' : 'not-match'">
|
||||
<span ng-click="addRef('tags', tagName)" ng-safenewtab>
|
||||
{{ tagName }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<span ng-if="tagNames.length > 20">...</span>
|
||||
</div>
|
||||
<div ng-if="state.branchTagFilter && !branchNames.length"
|
||||
style="margin-top: 10px">
|
||||
<strong>Warning:</strong> No branches found
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dockerfile folder select -->
|
||||
<div class="step-view-step" complete-condition="trigger.$ready" load-callback="loadLocations(callback)"
|
||||
load-message="Loading Folders">
|
||||
|
||||
<div style="margin-bottom: 12px">Dockerfile Location:</div>
|
||||
<div class="dropdown-select" placeholder="'(Repository Root)'" selected-item="state.currentLocation"
|
||||
lookahead-items="locations" handle-input="handleLocationInput(input)"
|
||||
handle-item-selected="handleLocationSelected(datum)"
|
||||
allow-custom-input="true"
|
||||
hide-dropdown="!supportsFullListing">
|
||||
<!-- Icons -->
|
||||
<i class="dropdown-select-icon none-icon fa fa-folder-o fa-lg" ng-show="state.isInvalidLocation"></i>
|
||||
<i class="dropdown-select-icon none-icon fa fa-folder fa-lg" style="color: black;" ng-show="!state.isInvalidLocation"></i>
|
||||
<i class="dropdown-select-icon fa fa-folder fa-lg"></i>
|
||||
|
||||
<!-- Dropdown menu -->
|
||||
<ul class="dropdown-select-menu" role="menu">
|
||||
<li ng-repeat="location in locations">
|
||||
<a ng-click="setLocation(location)" ng-if="!location">
|
||||
<i class="fa fa-github fa-lg"></i> Repository Root
|
||||
</a>
|
||||
<a ng-click="setLocation(location)" ng-if="location">
|
||||
<i class="fa fa-folder fa-lg"></i> {{ location }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown-header" role="presentation" ng-show="!locations.length">
|
||||
No Dockerfiles found in repository
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="cor-loader" ng-show="!locations && !locationError"></div>
|
||||
<div class="alert alert-warning" ng-show="locationError">
|
||||
{{ locationError }}
|
||||
</div>
|
||||
<div class="alert alert-warning" ng-show="locations && !locations.length && supportsFullListing">
|
||||
Warning: No Dockerfiles were found in {{ state.currentRepo.repo }}
|
||||
</div>
|
||||
<div class="alert alert-info" ng-show="locations.length && state.isInvalidLocation && supportsFullListing">
|
||||
Note: The folder does not currently exist or contain a Dockerfile
|
||||
</div>
|
||||
</div>
|
||||
<!-- /step-view -->
|
||||
</div>
|
||||
</div>
|
|
@ -1,4 +1,4 @@
|
|||
<span>
|
||||
<i class="fa fa-git-square fa-lg" style="margin-right: 6px;" data-title="git" bs-tooltip="tooltip.title"></i>
|
||||
Push to {{ trigger.config.build_source }}
|
||||
Push to repository {{ trigger.config.build_source }}
|
||||
</span>
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
54
static/js/directives/ui/dockerfile-path-select.js
Normal file
54
static/js/directives/ui/dockerfile-path-select.js
Normal file
|
@ -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;
|
||||
});
|
|
@ -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) {
|
||||
|
|
141
static/js/directives/ui/linear-workflow.js
Normal file
141
static/js/directives/ui/linear-workflow.js
Normal file
|
@ -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
|
||||
// <form> 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;
|
||||
});
|
27
static/js/directives/ui/manage-trigger-custom-git.js
Normal file
27
static/js/directives/ui/manage-trigger-custom-git.js
Normal file
|
@ -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;
|
||||
});
|
306
static/js/directives/ui/manage-trigger-githost.js
Normal file
306
static/js/directives/ui/manage-trigger-githost.js
Normal file
|
@ -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;
|
||||
});
|
36
static/js/directives/ui/regex-match-view.js
Normal file
36
static/js/directives/ui/regex-match-view.js
Normal file
|
@ -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;
|
||||
});
|
|
@ -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;
|
||||
});
|
|
@ -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;
|
||||
});
|
|
@ -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;
|
||||
});
|
89
static/js/pages/trigger-setup.js
Normal file
89
static/js/pages/trigger-setup.js
Normal file
|
@ -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];
|
||||
};
|
||||
}
|
||||
}());
|
|
@ -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')
|
||||
|
||||
|
|
65
static/partials/trigger-setup.html
Normal file
65
static/partials/trigger-setup.html
Normal file
|
@ -0,0 +1,65 @@
|
|||
<div class="resource-view trigger-setup-element"
|
||||
resources="[repositoryResource, triggerResource]"
|
||||
error-message="'Build trigger not found'">
|
||||
<div class="page-content">
|
||||
<div class="cor-title">
|
||||
<span class="cor-title-link">
|
||||
<a class="back-link" href="/repository/{{ repository.namespace }}/{{ repository.name }}?tab=builds">
|
||||
<i class="fa fa-hdd-o" style="margin-right: 4px"></i>
|
||||
{{ repository.namespace }}/{{ repository.name }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="cor-title-content">
|
||||
<i class="fa" ng-class="getTriggerIcon()"></i>
|
||||
Setup Build Trigger: {{ getTriggerId() }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="co-main-content-panel" ng-show="state != 'activated' && trigger.is_active">
|
||||
<div class="co-alert co-alert-info">
|
||||
Trigger has already been activated.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="co-main-content-panel" ng-show="state == 'activated' || !trigger.is_active">
|
||||
<!-- state = activated -->
|
||||
<div class="activated" ng-if="state == 'activated'">
|
||||
<div class="row">
|
||||
<div class="col-md-offset-3 col-md-6 col-sm-12 col-lg-6 content">
|
||||
<h3>Trigger has been successfully activated</h3>
|
||||
<div class="credentials" trigger="trigger"></div>
|
||||
<div class="button-bar">
|
||||
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}?tab=builds">
|
||||
Return to {{ repository.namespace }}/{{ repository.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- /state = activated -->
|
||||
|
||||
<!-- state = managing or activating -->
|
||||
<div ng-if="state == 'managing' || state == 'activating'"
|
||||
ng-class="{'activating': state == 'activating'}">
|
||||
<!-- Select the correct flow -->
|
||||
<div ng-switch on="trigger.service">
|
||||
<!-- Custom Git -->
|
||||
<div ng-switch-when="custom-git">
|
||||
<div class="manage-trigger-custom-git" trigger="trigger"
|
||||
activate-trigger="activateTrigger(config, pull_robot)"></div>
|
||||
</div> <!-- /custom-git -->
|
||||
|
||||
<!-- Hosted Git (GitHub, Gitlab, BitBucket) -->
|
||||
<div ng-switch-default>
|
||||
<div class="manage-trigger-githost" trigger="trigger" repository="repository"
|
||||
activate-trigger="activateTrigger(config, pull_robot)"></div>
|
||||
</div> <!-- /hosted -->
|
||||
</div> <!-- /ngSwitch -->
|
||||
|
||||
<div class="activating-message" ng-show="state == 'activating'">
|
||||
<div class="cor-loader-inline"></div><b>Completing setup of the build trigger</b>
|
||||
</div>
|
||||
</div> <!-- /state = managing -->
|
||||
|
||||
</div> <!-- /co-main-content-panel -->
|
||||
</div>
|
||||
</div>
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
|
Reference in a new issue