Add basic user interface for application repos
Adds support for creating app repos, viewing app repos and seeing the list of app repos in the Quay UI.
This commit is contained in:
parent
3dd6e6919d
commit
f9e6110f73
47 changed files with 1009 additions and 106 deletions
|
@ -20,7 +20,7 @@ CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
|
|||
'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT',
|
||||
'CONTACT_INFO', 'AVATAR_KIND', 'LOCAL_OAUTH_HANDLER', 'DOCUMENTATION_LOCATION',
|
||||
'DOCUMENTATION_METADATA', 'SETUP_COMPLETE', 'DEBUG', 'MARKETO_MUNCHKIN_ID',
|
||||
'STATIC_SITE_BUCKET', 'RECAPTCHA_SITE_KEY']
|
||||
'STATIC_SITE_BUCKET', 'RECAPTCHA_SITE_KEY', 'CHANNEL_COLORS']
|
||||
|
||||
|
||||
def frontend_visible_config(config_dict):
|
||||
|
@ -437,3 +437,10 @@ class DefaultConfig(object):
|
|||
FEATURE_TEAM_SYNCING = False
|
||||
TEAM_RESYNC_STALE_TIME = '30m'
|
||||
TEAM_SYNC_WORKER_FREQUENCY = 60 # seconds
|
||||
|
||||
# Colors for channels.
|
||||
CHANNEL_COLORS = ['#969696', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728',
|
||||
'#ff9896', '#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2', '#f7b6d2',
|
||||
'#7f7f7f', '#c7c7c7', '#bcbd22', '#1f77b4', '#17becf', '#9edae5', '#393b79',
|
||||
'#5254a3', '#6b6ecf', '#9c9ede', '#9ecae1', '#31a354', '#b5cf6b', '#a1d99b',
|
||||
'#8c6d31', '#ad494a', '#e7ba52', '#a55194']
|
||||
|
|
|
@ -387,7 +387,7 @@ class PreOCIModel(DockerRegistryV2DataInterface):
|
|||
return [_tag_view(tag) for tag in tags_query]
|
||||
|
||||
def get_visible_repositories(self, username, limit, offset):
|
||||
query = model.repository.get_visible_repositories(username, repo_kind='image',
|
||||
query = model.repository.get_visible_repositories(username, kind_filter='image',
|
||||
include_public=(username is None))
|
||||
query = query.limit(limit).offset(offset)
|
||||
return [_repository_for_repo(repo) for repo in query]
|
||||
|
|
|
@ -42,7 +42,10 @@ def filter_to_repos_for_user(query, username=None, namespace=None, repo_kind='im
|
|||
return Repository.select().where(Repository.id == '-1')
|
||||
|
||||
# Filter on the type of repository.
|
||||
query = query.where(Repository.kind == Repository.kind.get_id(repo_kind))
|
||||
try:
|
||||
query = query.where(Repository.kind == Repository.kind.get_id(repo_kind))
|
||||
except RepositoryKind.DoesNotExist:
|
||||
raise DataModelException('Unknown repository kind')
|
||||
|
||||
# Add the start ID if necessary.
|
||||
if start_id is not None:
|
||||
|
|
|
@ -276,8 +276,13 @@ def unstar_repository(user, repository):
|
|||
raise DataModelException('Star not found.')
|
||||
|
||||
|
||||
def get_user_starred_repositories(user, repo_kind='image'):
|
||||
def get_user_starred_repositories(user, kind_filter='image'):
|
||||
""" Retrieves all of the repositories a user has starred. """
|
||||
try:
|
||||
repo_kind = Repository.kind.get_id(kind_filter)
|
||||
except RepositoryKind.DoesNotExist:
|
||||
raise DataModelException('Unknown kind of repository')
|
||||
|
||||
query = (Repository
|
||||
.select(Repository, User, Visibility, Repository.id.alias('rid'))
|
||||
.join(Star)
|
||||
|
@ -285,8 +290,7 @@ def get_user_starred_repositories(user, repo_kind='image'):
|
|||
.join(User)
|
||||
.switch(Repository)
|
||||
.join(Visibility)
|
||||
.where(Star.user == user,
|
||||
Repository.kind == Repository.kind.get_id(repo_kind)))
|
||||
.where(Star.user == user, Repository.kind == repo_kind))
|
||||
|
||||
return query
|
||||
|
||||
|
@ -320,7 +324,7 @@ def get_when_last_modified(repository_ids):
|
|||
return last_modified_map
|
||||
|
||||
|
||||
def get_visible_repositories(username, namespace=None, repo_kind='image', include_public=False,
|
||||
def get_visible_repositories(username, namespace=None, kind_filter='image', include_public=False,
|
||||
start_id=None, limit=None):
|
||||
""" Returns the repositories visible to the given user (if any).
|
||||
"""
|
||||
|
@ -340,8 +344,8 @@ def get_visible_repositories(username, namespace=None, repo_kind='image', includ
|
|||
# Note: We only need the permissions table if we will filter based on a user's permissions.
|
||||
query = query.switch(Repository).distinct().join(RepositoryPermission, JOIN_LEFT_OUTER)
|
||||
|
||||
query = _basequery.filter_to_repos_for_user(query, username, namespace, repo_kind, include_public,
|
||||
start_id=start_id)
|
||||
query = _basequery.filter_to_repos_for_user(query, username, namespace, kind_filter,
|
||||
include_public, start_id=start_id)
|
||||
|
||||
if limit is not None:
|
||||
query = query.limit(limit).order_by(SQL('rid'))
|
||||
|
|
|
@ -121,13 +121,17 @@ def create_app_release(repo, tag_name, manifest_data, digest, force=False):
|
|||
ManifestBlob.create(manifest=manifest, blob=blob)
|
||||
return tag
|
||||
|
||||
|
||||
def get_releases(repo, media_type=None):
|
||||
""" Returns an array of Tag.name for a repo, can filter by media_type. """
|
||||
def get_release_objs(repo, media_type=None):
|
||||
""" Returns an array of Tag for a repo, with optional filtering by media_type. """
|
||||
release_query = (Tag
|
||||
.select()
|
||||
.where(Tag.repository == repo,
|
||||
Tag.tag_kind == Tag.tag_kind.get_id("release")))
|
||||
if media_type:
|
||||
release_query = tag_model.filter_tags_by_media_type(release_query, media_type)
|
||||
return [t.name for t in tag_model.tag_alive_oci(release_query)]
|
||||
|
||||
return tag_model.tag_alive_oci(release_query)
|
||||
|
||||
def get_releases(repo, media_type=None):
|
||||
""" Returns an array of Tag.name for a repo, can filter by media_type. """
|
||||
return [t.name for t in get_release_objs(repo, media_type)]
|
||||
|
|
|
@ -4,12 +4,13 @@ import logging
|
|||
import datetime
|
||||
import features
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from flask import request, abort
|
||||
|
||||
from app import dockerfile_build_queue
|
||||
from data import model
|
||||
from data import model, oci_model
|
||||
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
|
||||
require_repo_read, require_repo_write, require_repo_admin,
|
||||
RepositoryParamResource, resource, query_param, parse_args, ApiResource,
|
||||
|
@ -77,10 +78,10 @@ class RepositoryList(ApiResource):
|
|||
'type': 'string',
|
||||
'description': 'Markdown encoded description for the repository',
|
||||
},
|
||||
'kind': {
|
||||
'type': 'string',
|
||||
'repo_kind': {
|
||||
'type': ['string', 'null'],
|
||||
'description': 'The kind of repository',
|
||||
'enum': ['image', 'application'],
|
||||
'enum': ['image', 'application', None],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -116,7 +117,7 @@ class RepositoryList(ApiResource):
|
|||
if not REPOSITORY_NAME_REGEX.match(repository_name):
|
||||
raise InvalidRequest('Invalid repository name')
|
||||
|
||||
kind = req.get('kind', 'image')
|
||||
kind = req.get('repo_kind', 'image') or 'image'
|
||||
repo = model.repository.create_repository(namespace_name, repository_name, owner, visibility,
|
||||
repo_kind=kind)
|
||||
repo.description = req['description']
|
||||
|
@ -126,7 +127,8 @@ class RepositoryList(ApiResource):
|
|||
'namespace': namespace_name}, repo=repo)
|
||||
return {
|
||||
'namespace': namespace_name,
|
||||
'name': repository_name
|
||||
'name': repository_name,
|
||||
'kind': kind,
|
||||
}, 201
|
||||
|
||||
raise Unauthorized()
|
||||
|
@ -144,6 +146,7 @@ class RepositoryList(ApiResource):
|
|||
type=truthy_bool, default=False)
|
||||
@query_param('popularity', 'Whether to include the repository\'s popularity metric.',
|
||||
type=truthy_bool, default=False)
|
||||
@query_param('repo_kind', 'The kind of repositories to return', type=str, default='image')
|
||||
@page_support()
|
||||
def get(self, page_token, parsed_args):
|
||||
""" Fetch the list of repositories visible to the current user under a variety of situations.
|
||||
|
@ -158,6 +161,7 @@ class RepositoryList(ApiResource):
|
|||
username = user.username if user else None
|
||||
next_page_token = None
|
||||
repos = None
|
||||
repo_kind = parsed_args['repo_kind']
|
||||
|
||||
# Lookup the requested repositories (either starred or non-starred.)
|
||||
if parsed_args['starred']:
|
||||
|
@ -169,14 +173,15 @@ class RepositoryList(ApiResource):
|
|||
def can_view_repo(repo):
|
||||
return ReadRepositoryPermission(repo.namespace_user.username, repo.name).can()
|
||||
|
||||
unfiltered_repos = model.repository.get_user_starred_repositories(user)
|
||||
unfiltered_repos = model.repository.get_user_starred_repositories(user, kind_filter=repo_kind)
|
||||
repos = [repo for repo in unfiltered_repos if can_view_repo(repo)]
|
||||
elif parsed_args['namespace']:
|
||||
# Repositories filtered by namespace do not need pagination (their results are fairly small),
|
||||
# so we just do the lookup directly.
|
||||
repos = list(model.repository.get_visible_repositories(username=username,
|
||||
include_public=parsed_args['public'],
|
||||
namespace=parsed_args['namespace']))
|
||||
namespace=parsed_args['namespace'],
|
||||
kind_filter=repo_kind))
|
||||
else:
|
||||
# Determine the starting offset for pagination. Note that we don't use the normal
|
||||
# model.modelutil.paginate method here, as that does not operate over UNION queries, which
|
||||
|
@ -188,7 +193,8 @@ class RepositoryList(ApiResource):
|
|||
repo_query = model.repository.get_visible_repositories(username=username,
|
||||
include_public=parsed_args['public'],
|
||||
start_id=start_id,
|
||||
limit=REPOS_PER_PAGE+1)
|
||||
limit=REPOS_PER_PAGE+1,
|
||||
kind_filter=repo_kind)
|
||||
|
||||
repos, next_page_token = model.modelutil.paginate_query(repo_query, limit=REPOS_PER_PAGE,
|
||||
id_alias='rid')
|
||||
|
@ -217,6 +223,7 @@ class RepositoryList(ApiResource):
|
|||
'name': repo_obj.name,
|
||||
'description': repo_obj.description,
|
||||
'is_public': repo_obj.visibility_id == model.repository.get_public_repo_visibility().id,
|
||||
'kind': repo_kind,
|
||||
}
|
||||
|
||||
repo_id = repo_obj.rid
|
||||
|
@ -266,6 +273,55 @@ class Repository(RepositoryParamResource):
|
|||
"""Fetch the specified repository."""
|
||||
logger.debug('Get repo: %s/%s' % (namespace, repository))
|
||||
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
if repo is None:
|
||||
raise NotFound()
|
||||
|
||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||
can_admin = AdministerRepositoryPermission(namespace, repository).can()
|
||||
|
||||
is_starred = (model.repository.repository_is_starred(get_authenticated_user(), repo)
|
||||
if get_authenticated_user() else False)
|
||||
is_public = model.repository.is_repository_public(repo)
|
||||
|
||||
# Note: This is *temporary* code for the new OCI model stuff.
|
||||
if repo.kind.name == 'application':
|
||||
def channel_view(channel):
|
||||
return {
|
||||
'name': channel.name,
|
||||
'release': channel.linked_tag.name,
|
||||
'last_modified': format_date(datetime.fromtimestamp(channel.linked_tag.lifetime_start / 1000)),
|
||||
}
|
||||
|
||||
def release_view(release):
|
||||
return {
|
||||
'name': release.name,
|
||||
'last_modified': format_date(datetime.fromtimestamp(release.lifetime_start / 1000)),
|
||||
'channels': releases_channels_map[release.name],
|
||||
}
|
||||
|
||||
channels = oci_model.channel.get_repo_channels(repo)
|
||||
releases_channels_map = defaultdict(list)
|
||||
for channel in channels:
|
||||
releases_channels_map[channel.linked_tag.name].append(channel.name)
|
||||
|
||||
repo_data = {
|
||||
'namespace': namespace,
|
||||
'name': repository,
|
||||
'kind': repo.kind.name,
|
||||
'description': repo.description,
|
||||
'can_write': can_write,
|
||||
'can_admin': can_admin,
|
||||
'is_public': is_public,
|
||||
'is_organization': repo.namespace_user.organization,
|
||||
'is_starred': is_starred,
|
||||
'channels': [channel_view(chan) for chan in channels],
|
||||
'releases': [release_view(release) for release in oci_model.release.get_release_objs(repo)],
|
||||
}
|
||||
|
||||
return repo_data
|
||||
|
||||
# Older image-only repo code.
|
||||
def tag_view(tag, manifest):
|
||||
tag_info = {
|
||||
'name': tag.name,
|
||||
|
@ -282,63 +338,54 @@ class Repository(RepositoryParamResource):
|
|||
|
||||
return tag_info
|
||||
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
stats = None
|
||||
if repo:
|
||||
tags = model.tag.list_repository_tags(namespace, repository, include_storage=True)
|
||||
manifests = model.tag.get_tag_manifests(tags)
|
||||
tags = model.tag.list_repository_tags(namespace, repository, include_storage=True)
|
||||
manifests = model.tag.get_tag_manifests(tags)
|
||||
|
||||
tag_dict = {tag.name: tag_view(tag, manifests.get(tag.id)) for tag in tags}
|
||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||
can_admin = AdministerRepositoryPermission(namespace, repository).can()
|
||||
tag_dict = {tag.name: tag_view(tag, manifests.get(tag.id)) for tag in tags}
|
||||
if parsed_args['includeStats']:
|
||||
stats = []
|
||||
found_dates = {}
|
||||
|
||||
is_starred = (model.repository.repository_is_starred(get_authenticated_user(), repo)
|
||||
if get_authenticated_user() else False)
|
||||
is_public = model.repository.is_repository_public(repo)
|
||||
start_date = datetime.now() - timedelta(days=MAX_DAYS_IN_3_MONTHS)
|
||||
counts = model.log.get_repository_action_counts(repo, start_date)
|
||||
for count in counts:
|
||||
stats.append({
|
||||
'date': count.date.isoformat(),
|
||||
'count': count.count,
|
||||
})
|
||||
|
||||
if parsed_args['includeStats']:
|
||||
stats = []
|
||||
found_dates = {}
|
||||
found_dates['%s/%s' % (count.date.month, count.date.day)] = True
|
||||
|
||||
start_date = datetime.now() - timedelta(days=MAX_DAYS_IN_3_MONTHS)
|
||||
counts = model.log.get_repository_action_counts(repo, start_date)
|
||||
for count in counts:
|
||||
# Fill in any missing stats with zeros.
|
||||
for day in range(1, MAX_DAYS_IN_3_MONTHS):
|
||||
day_date = datetime.now() - timedelta(days=day)
|
||||
key = '%s/%s' % (day_date.month, day_date.day)
|
||||
if not key in found_dates:
|
||||
stats.append({
|
||||
'date': count.date.isoformat(),
|
||||
'count': count.count,
|
||||
'date': day_date.date().isoformat(),
|
||||
'count': 0,
|
||||
})
|
||||
|
||||
found_dates['%s/%s' % (count.date.month, count.date.day)] = True
|
||||
repo_data = {
|
||||
'namespace': namespace,
|
||||
'name': repository,
|
||||
'kind': repo.kind.name,
|
||||
'description': repo.description,
|
||||
'tags': tag_dict,
|
||||
'can_write': can_write,
|
||||
'can_admin': can_admin,
|
||||
'is_public': is_public,
|
||||
'is_organization': repo.namespace_user.organization,
|
||||
'is_starred': is_starred,
|
||||
'status_token': repo.badge_token if not is_public else '',
|
||||
}
|
||||
|
||||
# Fill in any missing stats with zeros.
|
||||
for day in range(1, MAX_DAYS_IN_3_MONTHS):
|
||||
day_date = datetime.now() - timedelta(days=day)
|
||||
key = '%s/%s' % (day_date.month, day_date.day)
|
||||
if not key in found_dates:
|
||||
stats.append({
|
||||
'date': day_date.date().isoformat(),
|
||||
'count': 0,
|
||||
})
|
||||
if stats is not None:
|
||||
repo_data['stats'] = stats
|
||||
|
||||
repo_data = {
|
||||
'namespace': namespace,
|
||||
'name': repository,
|
||||
'description': repo.description,
|
||||
'tags': tag_dict,
|
||||
'can_write': can_write,
|
||||
'can_admin': can_admin,
|
||||
'is_public': is_public,
|
||||
'is_organization': repo.namespace_user.organization,
|
||||
'is_starred': is_starred,
|
||||
'status_token': repo.badge_token if not is_public else '',
|
||||
}
|
||||
return repo_data
|
||||
|
||||
if stats is not None:
|
||||
repo_data['stats'] = stats
|
||||
|
||||
return repo_data
|
||||
|
||||
raise NotFound()
|
||||
|
||||
@require_repo_write
|
||||
@nickname('updateRepo')
|
||||
|
@ -361,7 +408,6 @@ class Repository(RepositoryParamResource):
|
|||
|
||||
@require_repo_admin
|
||||
@nickname('deleteRepository')
|
||||
@disallow_for_app_repositories
|
||||
def delete(self, namespace, repository):
|
||||
""" Delete a repository. """
|
||||
model.repository.purge_repository(namespace, repository)
|
||||
|
|
|
@ -28,7 +28,6 @@ TRIGGER_ARGS = {'trigger_uuid': '1234'}
|
|||
FIELD_ARGS = {'trigger_uuid': '1234', 'field_name': 'foobar'}
|
||||
|
||||
@pytest.mark.parametrize('resource, method, params', [
|
||||
(Repository, 'delete', None),
|
||||
(RepositoryBuildList, 'get', None),
|
||||
(RepositoryBuildList, 'post', None),
|
||||
(RepositoryBuildResource, 'get', BUILD_ARGS),
|
||||
|
|
|
@ -191,6 +191,14 @@ def buildtrigger(path, trigger):
|
|||
return index('')
|
||||
|
||||
|
||||
@route_show_if(features.APP_REGISTRY)
|
||||
@web.route('/application/', defaults={'path': ''})
|
||||
@web.route('/application/<path:path>', methods=['GET'])
|
||||
@no_cache
|
||||
def application(path):
|
||||
return index('')
|
||||
|
||||
|
||||
@web.route('/starred/')
|
||||
@no_cache
|
||||
def starred():
|
||||
|
|
75
static/css/directives/ui/app-public-view.css
Normal file
75
static/css/directives/ui/app-public-view.css
Normal file
|
@ -0,0 +1,75 @@
|
|||
.app-public-view-element {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.app-public-view-element .app-header {
|
||||
padding: 10px;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.app-public-view-element .app-header i.fa {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.app-public-view-element .main-content {
|
||||
min-height: 300px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.app-public-view-element .side-bar {
|
||||
padding: 20px;
|
||||
border-left: 1px solid #dddddd;
|
||||
background-color: #fdfdfd;
|
||||
}
|
||||
|
||||
.app-public-view-element .side-bar .sidebar-table {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.app-public-view-element .side-bar .sidebar-table h4 {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.app-public-view-element .app-row {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-public-view-element .main-content,
|
||||
.app-public-view-element .side-bar {
|
||||
float: none;
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.app-public-view-element .side-bar .visibility-indicator-component-element {
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.app-public-view-element .tab-content {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.app-public-view-element .tab-content .tab-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-public-view-element .co-main-content-panel {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.app-public-view-element .co-top-tab-bar li {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.app-public-view-element .co-panel .co-panel-heading {
|
||||
font-size: 16px;
|
||||
margin-left: -30px;
|
||||
margin-right: -30px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.app-public-view-element .co-panel .co-panel-heading i.fa {
|
||||
display: none;
|
||||
}
|
54
static/css/directives/ui/channel-icon.css
Normal file
54
static/css/directives/ui/channel-icon.css
Normal file
|
@ -0,0 +1,54 @@
|
|||
.channel-icon-element {
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
margin-right: 4px;
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* From: http://csshexagon.com/ */
|
||||
.channel-icon-element .hexagon {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 30px;
|
||||
height: 17.32px;
|
||||
background-color: red;
|
||||
margin: 8.66px 0;
|
||||
}
|
||||
|
||||
.channel-icon-element .hexagon .before,
|
||||
.channel-icon-element .hexagon .after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 0;
|
||||
border-left: 15px solid transparent;
|
||||
border-right: 15px solid transparent;
|
||||
}
|
||||
|
||||
.channel-icon-element .hexagon .before {
|
||||
bottom: 100%;
|
||||
border-bottom: 8.66px solid red;
|
||||
}
|
||||
|
||||
.channel-icon-element .hexagon .after {
|
||||
top: 100%;
|
||||
width: 0;
|
||||
border-top: 8.66px solid red;
|
||||
}
|
||||
|
||||
.channel-icon-element b {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
z-index: 1;
|
||||
color: white;
|
||||
font-weight: normal;
|
||||
font-size: 18px;
|
||||
}
|
17
static/css/directives/ui/visibility-indicator-component.css
Normal file
17
static/css/directives/ui/visibility-indicator-component.css
Normal file
|
@ -0,0 +1,17 @@
|
|||
.visibility-indicator-component-element {
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
color: white;
|
||||
height: 25px;
|
||||
font-size: 16px;
|
||||
padding: 0 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.visibility-indicator-component-element.private {
|
||||
background-color: #d64456;
|
||||
}
|
||||
|
||||
.visibility-indicator-component-element.public {
|
||||
background-color: #2fc98e;
|
||||
}
|
|
@ -7,6 +7,19 @@
|
|||
padding: 20px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.new-repo .repo-kind-select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.new-repo .repo-kind-select {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.new-repo .namespace-selector-header .slash {
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
<!-- Signed in -->
|
||||
<ul class="nav navbar-nav navbar-links" ng-if="!user.anonymous">
|
||||
<li quay-require="['APP_REGISTRY']"><a ng-href="/application/" quay-section="application">Applications</a></li>
|
||||
<li><a ng-href="/repository/" quay-section="repository">Repositories</a></li>
|
||||
<li><a ng-href="/tutorial/" quay-section="tutorial">Tutorial</a></li>
|
||||
<li><a href="https://docs.quay.io/" ng-safenewtab>Docs</a></li>
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: inherit' }}" data-title="Private Repository"></i>
|
||||
<i class="fa fa-hdd-o"></i>
|
||||
<i class="fa fa-hdd-o" ng-if="repo.kind == 'image'"></i>
|
||||
<i class="fa ci-appcube" ng-if="repo.kind == 'application'"></i>
|
||||
|
|
|
@ -28,7 +28,11 @@
|
|||
<div class="col-lg-10 col-md-10 col-sm-10 col-xs-10 repo-panel-title-row">
|
||||
<span class="repo-icon repo-circle no-background" repo="repository"></span>
|
||||
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}" class="repo-panel-repo-link"
|
||||
data-repo="{{repository.namespace}}/{{ repository.name }}">
|
||||
data-repo="{{repository.namespace}}/{{ repository.name }}" ng-if="repoKind != 'application'">
|
||||
<span ng-show="!hideNamespaces">{{ repository.namespace }}/</span>{{ repository.name }}
|
||||
</a>
|
||||
<a ng-href="/application/{{repository.namespace}}/{{ repository.name }}" class="repo-panel-repo-link"
|
||||
data-repo="{{repository.namespace}}/{{ repository.name }}" ng-if="repoKind == 'application'">
|
||||
<span ng-show="!hideNamespaces">{{ repository.namespace }}/</span>{{ repository.name }}
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<td class="hidden-xs"
|
||||
ng-class="tablePredicateClass('is_starred', options.predicate, options.reverse)"
|
||||
style="width: 70px"
|
||||
ng-if="loggedIn">
|
||||
ng-if="loggedIn && repoKind != 'application'">
|
||||
<a ng-click="orderBy('is_starred')">Star</a>
|
||||
</td>
|
||||
</thead>
|
||||
|
@ -47,7 +47,11 @@
|
|||
<tr ng-repeat="repository in orderedRepositories.entries | slice:(reposPerPage * options.page):(reposPerPage * (options.page + 1))">
|
||||
<td class="repo-name-icon">
|
||||
<span class="avatar" size="24" data="::getAvatarData(repository.namespace)"></span>
|
||||
<a href="/repository/{{ ::repository.namespace }}/{{ ::repository.name }}">
|
||||
<a href="/repository/{{ ::repository.namespace }}/{{ ::repository.name }}" ng-if="repoKind != 'application'">
|
||||
<span class="namespace">{{ ::repository.namespace }}</span>
|
||||
<span class="name">{{ ::repository.name }}</span>
|
||||
</a>
|
||||
<a href="/application/{{ ::repository.namespace }}/{{ ::repository.name }}" ng-if="repoKind == 'application'">
|
||||
<span class="namespace">{{ ::repository.namespace }}</span>
|
||||
<span class="name">{{ ::repository.name }}</span>
|
||||
</a>
|
||||
|
@ -62,7 +66,7 @@
|
|||
<span class="strength-indicator" value="::repository.popularity" maximum="::maxPopularity"
|
||||
log-base="10"></span>
|
||||
</td>
|
||||
<td ng-show="loggedIn">
|
||||
<td ng-show="loggedIn && repoKind != 'application'">
|
||||
<span class="repo-star" repository="::repository"
|
||||
star-toggled="starToggled({'repository': repository})"></span>
|
||||
</td>
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
<!-- Table View -->
|
||||
<div ng-if="showAsList">
|
||||
<div class="repo-list-table" repositories-resources="resources" namespaces="namespaces"
|
||||
star-toggled="starToggled({'repository': repository})">
|
||||
star-toggled="starToggled({'repository': repository})"
|
||||
repo-kind="{{ repoKind }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -22,7 +23,8 @@
|
|||
<div class="repo-list-grid" repositories-resource="starredRepositories"
|
||||
starred="true"
|
||||
star-toggled="starToggled({'repository': repository})"
|
||||
ng-if="starredRepositories">
|
||||
ng-if="starredRepositories"
|
||||
repo-kind="{{ repoKind }}">
|
||||
</div>
|
||||
|
||||
<!-- User and Org Repository Listings -->
|
||||
|
@ -31,7 +33,8 @@
|
|||
starred="false" namespace="namespace"
|
||||
star-toggled="starToggled({'repository': repository})"
|
||||
hide-title="namespaces.length == 1"
|
||||
hide-namespaces="true">
|
||||
hide-namespaces="true"
|
||||
repo-kind="{{ repoKind }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="repo-panel-settings-element">
|
||||
<h3 class="tab-header">Repository Settings</h3>
|
||||
<h3 class="tab-header"><span class="repository-title" repository="repository"></span> Settings</h3>
|
||||
|
||||
<!-- User/Team Permissions -->
|
||||
<div class="co-panel" id="repoPermissions">
|
||||
|
@ -11,7 +11,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Access Tokens (DEPRECATED) -->
|
||||
<div class="co-panel" ng-show="hasTokens">
|
||||
<div class="co-panel" ng-show="hasTokens && repository.kind == 'image'">
|
||||
<div class="co-panel-heading"><i class="fa fa-key"></i> Access Token Permissions</div>
|
||||
<div class="panel-body" style="padding-top: 5px;">
|
||||
<div class="repository-tokens-table" repository="repository" has-tokens="hasTokens" is-enabled="isEnabled"></div>
|
||||
|
@ -19,18 +19,22 @@
|
|||
</div>
|
||||
|
||||
<!-- Events and Notifications -->
|
||||
<div class="repository-events-table" repository="repository"
|
||||
is-enabled="isEnabled"></div>
|
||||
<div ng-if="repository.kind == 'image'">
|
||||
<div class="repository-events-table" repository="repository"
|
||||
is-enabled="isEnabled"></div>
|
||||
</div>
|
||||
|
||||
<!-- Visibility settings -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading"><i class="fa fa-unlock-alt"></i> Repository Visibility</div>
|
||||
<div class="co-panel-heading"><i class="fa fa-unlock-alt"></i>
|
||||
<span class="repository-title" repository="repository"></span> Visibility
|
||||
</div>
|
||||
<div class="cor-loader" ng-show="!repository"></div>
|
||||
<div ng-show="repository">
|
||||
<!-- Public/Private -->
|
||||
<div class="panel-body panel-section lock-section" ng-if="!repository.is_public">
|
||||
<i class="fa fa-lock lock-icon"></i>
|
||||
<div>This repository is currently <b>private</b>. Only users on the permissions list may view and interact with it.</div>
|
||||
<div>This <span class="repository-title" repository="repository"></span> is currently <b>private</b>. Only users on the permissions list may view and interact with it.</div>
|
||||
|
||||
<button class="btn btn-default" ng-click="askChangeAccess('public')">
|
||||
<i class="fa fa-unlock"></i>Make Public
|
||||
|
@ -40,7 +44,7 @@
|
|||
<div class="panel-body panel-section lock-section" ng-if="repository.is_public">
|
||||
<i class="fa fa-unlock lock-icon"></i>
|
||||
|
||||
<div>This repository is currently <b>public</b> and is visible to all users, and may be pulled by all users.</div>
|
||||
<div>This <span class="repository-title" repository="repository"></span> is currently <b>public</b> and is visible to all users, and may be pulled by all users.</div>
|
||||
|
||||
<button class="btn btn-default" ng-click="askChangeAccess('private')" ng-show="!planRequired">
|
||||
<i class="fa fa-lock"></i>Make Private
|
||||
|
@ -56,17 +60,19 @@
|
|||
|
||||
<!-- Delete repository -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading"><i class="fa fa-trash"></i> Delete Repository</div>
|
||||
<div class="co-panel-heading">
|
||||
<i class="fa fa-trash"></i> Delete <span class="repository-title" repository="repository"></span>
|
||||
</div>
|
||||
<div class="cor-loader" ng-show="!repository"></div>
|
||||
<div ng-show="repository">
|
||||
<div class="panel-body panel-section">
|
||||
<div class="co-alert co-alert-danger">
|
||||
<button class="btn btn-danger delete-btn" ng-click="askDelete()">
|
||||
<i class="fa fa-trash"></i>
|
||||
Delete Repository
|
||||
Delete <span class="repository-title" repository="repository"></span>
|
||||
</button>
|
||||
|
||||
Deleting a repository <b>cannot be undone</b>. Here be dragons!
|
||||
Deleting a <span class="repository-title" repository="repository"></span> <b>cannot be undone</b>. Here be dragons!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -74,7 +80,7 @@
|
|||
|
||||
|
||||
<!-- Build Status Badge -->
|
||||
<div class="co-panel hidden-xs">
|
||||
<div class="co-panel hidden-xs" ng-if="repository.kind == 'image'">
|
||||
<div class="co-panel-heading"><i class="fa fa-tasks"></i> Build Status Badge</div>
|
||||
<div class="cor-loader" ng-show="!repository"></div>
|
||||
<div ng-show="repository">
|
||||
|
@ -123,12 +129,12 @@
|
|||
<div class="cor-confirm-dialog"
|
||||
dialog-context="deleteRepoInfo"
|
||||
dialog-action="deleteRepo(info, callback)"
|
||||
dialog-title="Delete Repository"
|
||||
dialog-action-title="Delete Repository">
|
||||
dialog-title="Delete"
|
||||
dialog-action-title="Delete">
|
||||
<div class="co-alert co-alert-danger" style="margin-bottom: 10px;">
|
||||
This action cannot be undone!
|
||||
</div>
|
||||
|
||||
Continue with deletion of this repository?
|
||||
Continue with deletion of this <span class="repository-title" repository="repository"></span>?
|
||||
</div>
|
||||
</div>
|
||||
|
|
4
static/directives/repository-title.html
Normal file
4
static/directives/repository-title.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
<span class="repository-title-element">
|
||||
<span ng-if="repository.kind == 'image'">Repository</span>
|
||||
<span ng-if="repository.kind == 'application'">Application</span>
|
||||
</span>
|
|
@ -15,6 +15,10 @@ angular.module('quay').directive('repoPanelSettings', function () {
|
|||
controller: function($scope, $element, ApiService, Config) {
|
||||
$scope.deleteDialogCounter = 0;
|
||||
|
||||
var getTitle = function(repo) {
|
||||
return repo.kind == 'application' ? 'application' : 'image';
|
||||
};
|
||||
|
||||
$scope.getBadgeFormat = function(format, repository) {
|
||||
if (!repository) { return ''; }
|
||||
|
||||
|
@ -52,7 +56,8 @@ angular.module('quay').directive('repoPanelSettings', function () {
|
|||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||
};
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Could not delete repository', callback);
|
||||
var errorHandler = ApiService.errorDisplay(
|
||||
'Could not delete ' + getTitle($scope.repository), callback);
|
||||
|
||||
ApiService.deleteRepository(null, params).then(function() {
|
||||
callback(true);
|
||||
|
@ -62,9 +67,11 @@ angular.module('quay').directive('repoPanelSettings', function () {
|
|||
}, errorHandler);
|
||||
};
|
||||
|
||||
|
||||
$scope.askChangeAccess = function(newAccess) {
|
||||
bootbox.confirm('Are you sure you want to make this repository ' + newAccess + '?', function(r) {
|
||||
var msg = 'Are you sure you want to make this ' + getTitle($scope.repository) + ' ' +
|
||||
newAccess + '?';
|
||||
|
||||
bootbox.confirm(msg, function(r) {
|
||||
if (!r) { return; }
|
||||
$scope.changeAccess(newAccess);
|
||||
});
|
||||
|
@ -81,7 +88,7 @@ angular.module('quay').directive('repoPanelSettings', function () {
|
|||
|
||||
ApiService.changeRepoVisibility(visibility, params).then(function() {
|
||||
$scope.repository.is_public = newAccess == 'public';
|
||||
}, ApiService.errorDisplay('Could not change repository visibility'));
|
||||
}, ApiService.errorDisplay('Could not change visibility'));
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
<div class="app-public-view-element">
|
||||
<div class="co-main-content-panel">
|
||||
<div class="app-row">
|
||||
<!-- Main panel -->
|
||||
<div class="col-md-9 main-content">
|
||||
<!-- App Header -->
|
||||
<div class="app-header">
|
||||
<a href="https://coreos.com/blog/quay-application-registry-for-kubernetes.html" class="hidden-xs hidden-sm" style="float: right; padding: 6px;" ng-safenewtab><i class="fa fa-info-circle" style="margin-right: 6px;"></i>Learn more about applications</a>
|
||||
<h3><i class="fa ci-appcube"></i>{{ $ctrl.repository.namespace }}/{{ $ctrl.repository.name }}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="co-top-tab-bar">
|
||||
<li class="co-top-tab" ng-class="$ctrl.currentTab == 'description' ? 'active': ''" ng-click="$ctrl.showTab('description')">
|
||||
Description
|
||||
</li>
|
||||
<li class="co-top-tab" ng-class="$ctrl.currentTab == 'channels' ? 'active': ''" ng-click="$ctrl.showTab('channels')">
|
||||
Channels
|
||||
</li>
|
||||
<li class="co-top-tab" ng-class="$ctrl.currentTab == 'releases' ? 'active': ''" ng-click="$ctrl.showTab('releases')">
|
||||
Releases
|
||||
</li>
|
||||
<li class="co-top-tab" ng-class="$ctrl.currentTab == 'settings' ? 'active': ''" ng-click="$ctrl.showTab('settings')"
|
||||
ng-if="$ctrl.repository.can_admin">
|
||||
Settings
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div ng-show="$ctrl.currentTab == 'description'">
|
||||
<div class="description markdown-input"
|
||||
content="$ctrl.repository.description"
|
||||
can-write="$ctrl.repository.can_write"
|
||||
content-changed="$ctrl.updateDescription"
|
||||
field-title="'application description'">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="$ctrl.currentTab == 'channels'">
|
||||
<div ng-show="!$ctrl.repository.channels.length && $ctrl.repository.can_write">
|
||||
<h3>No channels found for this application</h3>
|
||||
<br>
|
||||
<p>
|
||||
To push a new channel (from within the Helm package directory and with the <a href="https://coreos.com/apps" ng-safenewtab>Helm registry plugin</a> installed):
|
||||
<pre class="command">
|
||||
helm registry push --namespace {{ $ctrl.repository.namespace }} --channel {channelName} {{ $ctrl.Config.SERVER_HOSTNAME }}
|
||||
</pre>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div ng-show="$ctrl.repository.channels.length || !$ctrl.repository.can_write">
|
||||
<cor-table table-data="$ctrl.repository.channels" table-item-title="channels" filter-fields="['name']">
|
||||
<cor-table-col datafield="name" sortfield="name" title="Name"
|
||||
templateurl="/static/js/directives/ui/app-public-view/channel-name.html"></cor-table-col>
|
||||
<cor-table-col datafield="release" sortfield="release" title="Current Release"></cor-table-col>
|
||||
<cor-table-col datafield="last_modified" sortfield="last_modified" title="Last Modified"
|
||||
selected="true" dataKind="datetime"
|
||||
templateurl="/static/js/directives/ui/app-public-view/last-modified.html"></cor-table-col>
|
||||
</cor-table>
|
||||
</div>
|
||||
</div> <!-- /channels -->
|
||||
|
||||
<div ng-show="$ctrl.currentTab == 'releases'">
|
||||
<div ng-show="!$ctrl.repository.releases.length && $ctrl.repository.can_write">
|
||||
<h3>No releases found for this application</h3>
|
||||
<br>
|
||||
<p>
|
||||
To push a new release (from within the Helm package directory and with the <a href="https://coreos.com/apps" ng-safenewtab>Helm registry plugin</a> installed):
|
||||
<pre class="command">
|
||||
helm registry push --namespace {{ $ctrl.repository.namespace }} {{ $ctrl.Config.SERVER_HOSTNAME }}
|
||||
</pre>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div ng-show="$ctrl.repository.releases.length || !$ctrl.repository.can_write">
|
||||
<cor-table table-data="$ctrl.repository.releases" table-item-title="releases" filter-fields="['name']">
|
||||
<cor-table-col datafield="name" sortfield="name" title="Name"></cor-table-col>
|
||||
<cor-table-col datafield="last_modified" sortfield="last_modified"
|
||||
title="Created"
|
||||
selected="true" dataKind="datetime"
|
||||
templateurl="/static/js/directives/ui/app-public-view/last-modified.html"></cor-table-col>
|
||||
<cor-table-col datafield="channels" title="Channels"
|
||||
templateurl="/static/js/directives/ui/app-public-view/channels-list.html"></cor-table-col>
|
||||
</cor-table>
|
||||
</div>
|
||||
</div> <!-- /releases -->
|
||||
|
||||
<div ng-show="$ctrl.currentTab == 'settings'" ng-if="$ctrl.repository.can_admin">
|
||||
<div class="repo-panel-settings" repository="$ctrl.repository" is-enabled="$ctrl.settingsShown"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Side bar -->
|
||||
<div class="col-md-3 side-bar">
|
||||
<div>
|
||||
<visibility-indicator repository="$ctrl.repository"></visibility-indicator>
|
||||
</div>
|
||||
<div ng-if="$ctrl.repository.is_public">{{ $ctrl.repository.namespace }} is sharing this application publicly</div>
|
||||
<div ng-if="!$ctrl.repository.is_public">This application is private and only visible to those with permission</div>
|
||||
|
||||
<div class="sidebar-table" ng-if="$ctrl.repository.channels.length">
|
||||
<h4>Latest Channels</h4>
|
||||
<cor-table table-data="$ctrl.repository.channels" table-item-title="channels" filter-fields="['name']" compact="true" max-display-count="3">
|
||||
<cor-table-col datafield="name" sortfield="name" title="Name"
|
||||
templateurl="/static/js/directives/ui/app-public-view/channel-name.html"></cor-table-col>
|
||||
<cor-table-col datafield="release" sortfield="release" title="Current Release"></cor-table-col>
|
||||
</cor-table>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-table" ng-if="$ctrl.repository.releases.length">
|
||||
<h4>Latest Releases</h4>
|
||||
<cor-table table-data="$ctrl.repository.releases" table-item-title="releases" filter-fields="['name']" compact="true" max-display-count="3">
|
||||
<cor-table-col datafield="name" sortfield="name" title="Name"></cor-table-col>
|
||||
<cor-table-col datafield="last_modified" sortfield="last_modified"
|
||||
title="Created"
|
||||
selected="true" dataKind="datetime"
|
||||
templateurl="/static/js/directives/ui/app-public-view/last-modified.html"></cor-table-col>
|
||||
</cor-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
import { Input, Component } from 'angular-ts-decorators';
|
||||
|
||||
/**
|
||||
* A component that displays the public information associated with an application repository.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'appPublicView',
|
||||
templateUrl: '/static/js/directives/ui/app-public-view/app-public-view.component.html'
|
||||
})
|
||||
export class AppPublicViewComponent implements ng.IComponentController {
|
||||
@Input('<') public repository: any;
|
||||
private currentTab: string = 'description';
|
||||
private settingsShown: number = 0;
|
||||
|
||||
constructor(private Config: any) {
|
||||
this.updateDescription = this.updateDescription.bind(this);
|
||||
}
|
||||
|
||||
private updateDescription(content: string) {
|
||||
this.repository.description = content;
|
||||
this.repository.put();
|
||||
}
|
||||
|
||||
public showTab(tab: string): void {
|
||||
this.currentTab = tab;
|
||||
if (tab == 'settings') {
|
||||
this.settingsShown++;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<channel-icon name="item.name"></channel-icon><span style="vertical-align: middle; margin-left: 6px;">{{ item.name }}</span>
|
|
@ -0,0 +1,3 @@
|
|||
<span ng-repeat="channel_name in item.channels">
|
||||
<channel-icon name="channel_name"></channel-icon>
|
||||
</span>
|
|
@ -0,0 +1,3 @@
|
|||
<span data-title="{{ item.last_modified | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}" bs-tooltip>
|
||||
<span am-time-ago="item.last_modified"></span>
|
||||
</span>
|
|
@ -0,0 +1,7 @@
|
|||
<span class="channel-icon-element" data-title="{{ $ctrl.name }}" bs-tooltip>
|
||||
<span class="hexagon" ng-style="{'background-color': $ctrl.color($ctrl.name)}">
|
||||
<span class="before" ng-style="{'border-bottom-color': $ctrl.color($ctrl.name)}"></span>
|
||||
<span class="after" ng-style="{'border-top-color': $ctrl.color($ctrl.name)}"></span>
|
||||
</span>
|
||||
<b>{{ $ctrl.initial($ctrl.name) }}</b>
|
||||
</span>
|
|
@ -0,0 +1,47 @@
|
|||
import { Input, Component } from 'angular-ts-decorators';
|
||||
|
||||
/**
|
||||
* A component that displays the icon of a channel.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'channelIcon',
|
||||
templateUrl: '/static/js/directives/ui/channel-icon/channel-icon.component.html',
|
||||
})
|
||||
export class ChannelIconComponent implements ng.IComponentController {
|
||||
@Input('<') public name: string;
|
||||
|
||||
private colors: any;
|
||||
|
||||
constructor(Config: any, private md5: any) {
|
||||
this.colors = Config['CHANNEL_COLORS'];
|
||||
}
|
||||
|
||||
private initial(name: string): string {
|
||||
if (name == 'alpha') {
|
||||
return 'α';
|
||||
}
|
||||
if (name == 'beta') {
|
||||
return 'β';
|
||||
}
|
||||
if (name == 'stable') {
|
||||
return 'S';
|
||||
}
|
||||
return name[0].toUpperCase();
|
||||
}
|
||||
|
||||
private color(name: string): string {
|
||||
if (name == 'alpha') {
|
||||
return this.colors[0];
|
||||
}
|
||||
if (name == 'beta') {
|
||||
return this.colors[1];
|
||||
}
|
||||
if (name == 'stable') {
|
||||
return this.colors[2];
|
||||
}
|
||||
|
||||
var hash: string = this.md5.createHash(name);
|
||||
var num: number = parseInt(hash.substr(0, 4));
|
||||
return this.colors[num % this.colors.length];
|
||||
}
|
||||
}
|
40
static/js/directives/ui/cor-table/cor-table-col.component.ts
Normal file
40
static/js/directives/ui/cor-table/cor-table-col.component.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Input, Component } from 'angular-ts-decorators';
|
||||
import { CorTableComponent } from './cor-table.component';
|
||||
|
||||
/**
|
||||
* Defines a column (optionally sortable) in the table.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'corTableCol',
|
||||
template: '',
|
||||
require: {
|
||||
parent: '^^corTable'
|
||||
},
|
||||
})
|
||||
export class CorTableColumn implements ng.IComponentController {
|
||||
@Input('@') public title: string;
|
||||
@Input('@') public templateurl: string;
|
||||
|
||||
@Input('@') public datafield: string;
|
||||
@Input('@') public sortfield: string;
|
||||
@Input('@') public selected: string;
|
||||
@Input('@') public dataKind: string;
|
||||
|
||||
private parent: CorTableComponent;
|
||||
|
||||
public $onInit(): void {
|
||||
this.parent.addColumn(this);
|
||||
}
|
||||
|
||||
public isNumeric(): boolean {
|
||||
return this.dataKind == 'datetime';
|
||||
}
|
||||
|
||||
public processColumnForOrdered(tableService: any, value: any): any {
|
||||
if (this.dataKind == 'datetime') {
|
||||
return tableService.getReversedTimestamp(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
42
static/js/directives/ui/cor-table/cor-table.component.html
Normal file
42
static/js/directives/ui/cor-table/cor-table.component.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<div class="cor-table-element">
|
||||
<span ng-transclude/>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="co-top-bar" ng-if="$ctrl.compact != 'true'">
|
||||
<span class="co-filter-box with-options" ng-if="$ctrl.tableData.length && $ctrl.filterFields.length">
|
||||
<span class="filter-message" ng-if="$ctrl.options.filter">
|
||||
Showing {{ $ctrl.orderedData.entries.length }} of {{ $ctrl.tableData.length }} {{ $ctrl.tableItemTitle }}
|
||||
</span>
|
||||
<input class="form-control" type="text" ng-model="$ctrl.options.filter"
|
||||
placeholder="Filter {{ $ctrl.tableItemTitle }}..." ng-change="$ctrl.refreshOrder()">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div class="empty" ng-if="!$ctrl.tableData.length && $ctrl.compact != 'true'">
|
||||
<div class="empty-primary-msg">No {{ $ctrl.tableItemTitle }} found.</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<table class="co-table" ng-show="$ctrl.tableData.length">
|
||||
<thead>
|
||||
<td ng-repeat="col in $ctrl.columns" ng-class="$ctrl.tablePredicateClass(col, $ctrl.options)">
|
||||
<a ng-click="$ctrl.setOrder(col)">{{ col.title }}</a>
|
||||
</td>
|
||||
</thead>
|
||||
<tbody ng-repeat="item in $ctrl.orderedData.visibleEntries | limitTo:$ctrl.maxDisplayCount">
|
||||
<tr>
|
||||
<td ng-repeat="col in $ctrl.columns">
|
||||
<div ng-include="col.templateurl" ng-if="col.templateurl"></div>
|
||||
<div ng-if="!col.templateurl">{{ item[col.datafield] }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="empty" ng-if="!$ctrl.orderedData.entries.length && $ctrl.tableData.length"
|
||||
style="margin-top: 20px;">
|
||||
<div class="empty-primary-msg">No matching {{ $ctrl.tableItemTitle }} found.</div>
|
||||
<div class="empty-secondary-msg">Try adjusting your filter above.</div>
|
||||
</div>
|
||||
</div>
|
82
static/js/directives/ui/cor-table/cor-table.component.ts
Normal file
82
static/js/directives/ui/cor-table/cor-table.component.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { Input, Component } from 'angular-ts-decorators';
|
||||
import { CorTableColumn } from './cor-table-col.component';
|
||||
|
||||
/**
|
||||
* A component that displays a table of information, with optional filtering and automatic sorting.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'corTable',
|
||||
templateUrl: '/static/js/directives/ui/cor-table/cor-table.component.html',
|
||||
transclude: true,
|
||||
})
|
||||
export class CorTableComponent implements ng.IComponentController {
|
||||
@Input('=') public tableData: any[];
|
||||
@Input('@') public tableItemTitle: string;
|
||||
@Input('<') public filterFields: string[];
|
||||
@Input('@') public compact: string;
|
||||
@Input('<') public maxDisplayCount: number;
|
||||
|
||||
private columns: CorTableColumn[];
|
||||
private orderedData: any;
|
||||
private options: any;
|
||||
|
||||
constructor(private TableService: any) {
|
||||
this.columns = [];
|
||||
this.options = {
|
||||
'filter': '',
|
||||
'reverse': false,
|
||||
'predicate': '',
|
||||
'page': 0,
|
||||
};
|
||||
}
|
||||
|
||||
public addColumn(col: CorTableColumn): void {
|
||||
this.columns.push(col);
|
||||
|
||||
if (col.selected == 'true') {
|
||||
this.options['predicate'] = col.datafield;
|
||||
}
|
||||
|
||||
this.refreshOrder();
|
||||
}
|
||||
|
||||
private setOrder(col: CorTableColumn): void {
|
||||
this.TableService.orderBy(col.datafield, this.options);
|
||||
this.refreshOrder();
|
||||
}
|
||||
|
||||
private tablePredicateClass(col: CorTableColumn, options: any) {
|
||||
return this.TableService.tablePredicateClass(col.datafield, this.options.predicate,
|
||||
this.options.reverse);
|
||||
}
|
||||
|
||||
private refreshOrder(): void {
|
||||
var columnMap = {};
|
||||
this.columns.forEach(function(col) {
|
||||
columnMap[col.datafield] = col;
|
||||
});
|
||||
|
||||
var filterCols = this.columns.filter(function(col) {
|
||||
return !!col.sortfield;
|
||||
}).map((col) => (col.datafield));
|
||||
|
||||
var numericCols = this.columns.filter(function(col) {
|
||||
return col.isNumeric();
|
||||
}).map((col) => (col.datafield));
|
||||
|
||||
var processed = this.tableData.map((item) => {
|
||||
var keys = Object.keys(item);
|
||||
var newObj = {};
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
var key = keys[i];
|
||||
if (columnMap[key]) {
|
||||
newObj[key] = columnMap[key].processColumnForOrdered(this.TableService, item[key]);
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
});
|
||||
|
||||
this.orderedData = this.TableService.buildOrderedItems(processed, this.options,
|
||||
filterCols, numericCols);
|
||||
}
|
||||
}
|
|
@ -14,7 +14,8 @@ angular.module('quay').directive('repoListGrid', function () {
|
|||
namespace: '=namespace',
|
||||
starToggled: '&starToggled',
|
||||
hideTitle: '=hideTitle',
|
||||
hideNamespaces: '=hideNamespaces'
|
||||
hideNamespaces: '=hideNamespaces',
|
||||
repoKind: '@repoKind'
|
||||
},
|
||||
controller: function($scope, $element, UserService) {
|
||||
$scope.isOrganization = function(namespace) {
|
||||
|
|
|
@ -11,7 +11,8 @@ angular.module('quay').directive('repoListTable', function () {
|
|||
scope: {
|
||||
'repositoriesResources': '=repositoriesResources',
|
||||
'namespaces': '=namespaces',
|
||||
'starToggled': '&starToggled'
|
||||
'starToggled': '&starToggled',
|
||||
'repoKind': '@repoKind'
|
||||
},
|
||||
controller: function($scope, $element, $filter, TableService, UserService) {
|
||||
$scope.repositories = null;
|
||||
|
|
|
@ -12,6 +12,7 @@ angular.module('quay').directive('repoListView', function () {
|
|||
namespaces: '=namespaces',
|
||||
starredRepositories: '=starredRepositories',
|
||||
starToggled: '&starToggled',
|
||||
repoKind: '@repoKind'
|
||||
},
|
||||
controller: function($scope, $element, CookieService) {
|
||||
$scope.resources = [];
|
||||
|
|
18
static/js/directives/ui/repository-title.js
Normal file
18
static/js/directives/ui/repository-title.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* An element which displays the title of a repository (either 'repository' or 'application').
|
||||
*/
|
||||
angular.module('quay').directive('repositoryTitle', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/repository-title.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '<repository'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
<span class="visibility-indicator-component-element" ng-class="{'public': $ctrl.repository.is_public, 'private': !$ctrl.repository.is_public}">
|
||||
<span class="public" ng-if="$ctrl.repository.is_public">Public</span>
|
||||
<span class="private" ng-if="!$ctrl.repository.is_public">Private</span>
|
||||
</span>
|
|
@ -0,0 +1,17 @@
|
|||
import { Input, Component } from 'angular-ts-decorators';
|
||||
|
||||
/**
|
||||
* A component that displays a box with "Public" or "Private", depending on the visibility
|
||||
* of the repository.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'visibilityIndicator',
|
||||
templateUrl: '/static/js/directives/ui/visibility-indicator/visibility-indicator.component.html'
|
||||
})
|
||||
export class VisibilityIndicatorComponent implements ng.IComponentController {
|
||||
@Input('<') public repository: any;
|
||||
|
||||
constructor() {
|
||||
|
||||
}
|
||||
}
|
82
static/js/pages/app-list.js
Normal file
82
static/js/pages/app-list.js
Normal file
|
@ -0,0 +1,82 @@
|
|||
(function() {
|
||||
/**
|
||||
* Application listing page. Shows all applications for all visibile namespaces.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('app-list', 'app-list.html', AppListCtrl, {
|
||||
'newLayout': true,
|
||||
'title': 'Applications',
|
||||
'description': 'View and manage applications'
|
||||
})
|
||||
}]);
|
||||
|
||||
function AppListCtrl($scope, $sanitize, $q, Restangular, UserService, ApiService, Features) {
|
||||
$scope.namespace = null;
|
||||
$scope.page = 1;
|
||||
$scope.publicPageCount = null;
|
||||
$scope.allRepositories = {};
|
||||
$scope.loading = true;
|
||||
$scope.resources = [];
|
||||
$scope.Features = Features;
|
||||
|
||||
// When loading the UserService, if the user is logged in, create a list of
|
||||
// relevant namespaces and collect the relevant repositories.
|
||||
UserService.updateUserIn($scope, function(user) {
|
||||
$scope.loading = false;
|
||||
if (!user.anonymous) {
|
||||
// Add our user to our list of namespaces.
|
||||
$scope.namespaces = [{
|
||||
'name': user.username,
|
||||
'avatar': user.avatar
|
||||
}];
|
||||
|
||||
// Add each org to our list of namespaces.
|
||||
user.organizations.map(function(org) {
|
||||
$scope.namespaces.push({
|
||||
'name': org.name,
|
||||
'avatar': org.avatar
|
||||
});
|
||||
});
|
||||
|
||||
// Load the repos.
|
||||
loadRepos();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.isOrganization = function(namespace) {
|
||||
return !!UserService.getOrganization(namespace);
|
||||
};
|
||||
|
||||
// Finds a duplicate repo if it exists. If it doesn't, inserts the repo.
|
||||
var findDuplicateRepo = function(repo) {
|
||||
var found = $scope.allRepositories[repo.namespace + '/' + repo.name];
|
||||
if (found) {
|
||||
return found;
|
||||
} else {
|
||||
$scope.allRepositories[repo.namespace + '/' + repo.name] = repo;
|
||||
return repo;
|
||||
}
|
||||
};
|
||||
|
||||
var loadRepos = function() {
|
||||
if (!$scope.user || $scope.user.anonymous || $scope.namespaces.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.namespaces.map(function(namespace) {
|
||||
var options = {
|
||||
'namespace': namespace.name,
|
||||
'last_modified': true,
|
||||
'popularity': true,
|
||||
'repo_kind': 'application'
|
||||
};
|
||||
|
||||
namespace.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
|
||||
return resp.repositories.map(findDuplicateRepo);
|
||||
});
|
||||
|
||||
$scope.resources.push(namespace.repositories);
|
||||
});
|
||||
};
|
||||
}
|
||||
})();
|
39
static/js/pages/app-view.js
Normal file
39
static/js/pages/app-view.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
(function() {
|
||||
/**
|
||||
* Application view page.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('app-view', 'app-view.html', AppViewCtrl, {
|
||||
'newLayout': true,
|
||||
'title': '{{ namespace }}/{{ name }}',
|
||||
'description': 'Application {{ namespace }}/{{ name }}'
|
||||
});
|
||||
}]);
|
||||
|
||||
function AppViewCtrl($scope, $routeParams, $location, $timeout, ApiService, UserService, AngularPollChannel, ImageLoaderService, CookieService) {
|
||||
$scope.namespace = $routeParams.namespace;
|
||||
$scope.name = $routeParams.name;
|
||||
|
||||
$scope.viewScope = {};
|
||||
$scope.settingsShown = 0;
|
||||
|
||||
$scope.showSettings = function() {
|
||||
$scope.settingsShown++;
|
||||
};
|
||||
|
||||
var loadRepository = function() {
|
||||
var params = {
|
||||
'repository': $scope.namespace + '/' + $scope.name,
|
||||
'repo_kind': 'application',
|
||||
'includeStats': true
|
||||
};
|
||||
|
||||
$scope.repositoryResource = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
$scope.repository = repo;
|
||||
$scope.viewScope.repository = repo;
|
||||
});
|
||||
};
|
||||
|
||||
loadRepository();
|
||||
}
|
||||
})();
|
|
@ -20,7 +20,8 @@
|
|||
'is_public': 0,
|
||||
'description': '',
|
||||
'initialize': '',
|
||||
'name': $routeParams['name']
|
||||
'name': $routeParams['name'],
|
||||
'repo_kind': 'image'
|
||||
};
|
||||
|
||||
$scope.changeNamespace = function(namespace) {
|
||||
|
@ -54,13 +55,19 @@
|
|||
'namespace': repo.namespace,
|
||||
'repository': repo.name,
|
||||
'visibility': repo.is_public == '1' ? 'public' : 'private',
|
||||
'description': repo.description
|
||||
'description': repo.description,
|
||||
'repo_kind': repo.repo_kind
|
||||
};
|
||||
|
||||
ApiService.createRepo(data).then(function(created) {
|
||||
$scope.creating = false;
|
||||
$scope.created = created;
|
||||
|
||||
if (repo.repo_kind == 'application') {
|
||||
$location.path('/application/' + created.namespace + '/' + created.name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the build if applicable.
|
||||
if ($scope.repo.initialize == 'dockerfile' || $scope.repo.initialize == 'zipfile') {
|
||||
$scope.createdForBuild = created;
|
||||
|
|
|
@ -40,6 +40,12 @@ export class QuayRoutes {
|
|||
}
|
||||
|
||||
routeBuilder
|
||||
// Application View
|
||||
.route('/application/:namespace/:name', 'app-view')
|
||||
|
||||
// Repo List
|
||||
.route('/application/', 'app-list')
|
||||
|
||||
// Repository View
|
||||
.route('/repository/:namespace/:name', 'repo-view')
|
||||
.route('/repository/:namespace/:name/tag/:tag', 'repo-view')
|
||||
|
|
|
@ -12,6 +12,11 @@ import { ManageTriggerCustomGitComponent } from './directives/ui/manage-trigger-
|
|||
import { ManageTriggerGithostComponent } from './directives/ui/manage-trigger-githost/manage-trigger-githost.component';
|
||||
import { LinearWorkflowComponent } from './directives/ui/linear-workflow/linear-workflow.component';
|
||||
import { LinearWorkflowSectionComponent } from './directives/ui/linear-workflow/linear-workflow-section.component';
|
||||
import { AppPublicViewComponent } from './directives/ui/app-public-view/app-public-view.component';
|
||||
import { VisibilityIndicatorComponent } from './directives/ui/visibility-indicator/visibility-indicator.component';
|
||||
import { CorTableComponent } from './directives/ui/cor-table/cor-table.component';
|
||||
import { CorTableColumn } from './directives/ui/cor-table/cor-table-col.component';
|
||||
import { ChannelIconComponent } from './directives/ui/channel-icon/channel-icon.component';
|
||||
import { QuayConfig } from './quay-config.module';
|
||||
import { QuayRun } from './quay-run.module';
|
||||
import { BuildServiceImpl } from './services/build/build.service.impl';
|
||||
|
@ -37,6 +42,11 @@ import { DataFileServiceImpl } from './services/datafile/datafile.service.impl';
|
|||
ManageTriggerGithostComponent,
|
||||
LinearWorkflowComponent,
|
||||
LinearWorkflowSectionComponent,
|
||||
AppPublicViewComponent,
|
||||
VisibilityIndicatorComponent,
|
||||
CorTableComponent,
|
||||
CorTableColumn,
|
||||
ChannelIconComponent,
|
||||
],
|
||||
providers: [
|
||||
ViewArrayImpl,
|
||||
|
|
30
static/partials/app-list.html
Normal file
30
static/partials/app-list.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<div class="repo-list page-content">
|
||||
<div class="cor-title">
|
||||
<span class="cor-title-link"></span>
|
||||
<span class="cor-title-content">Applications</span>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div class="cor-loader" ng-if="loading || user.beforeload"></div>
|
||||
|
||||
<!-- Not signed in -->
|
||||
<div class="co-main-content-panel" ng-if="!loading && user.anonymous && !user.beforeload">
|
||||
<!-- The user is not logged in -->
|
||||
<div class="cor-container signin-container row">
|
||||
<!-- Sign In -->
|
||||
<div class="user-setup" redirect-url="redirectUrl"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signed in -->
|
||||
<div class="row" ng-if="!loading && !user.anonymous">
|
||||
<div class="col-md-12">
|
||||
<div class="repo-list-panel co-main-content-panel">
|
||||
<div class="repo-list-view" namespaces="namespaces" repo-kind="application">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
5
static/partials/app-view.html
Normal file
5
static/partials/app-view.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
<div class="resource-view repository-view"
|
||||
resource="repositoryResource"
|
||||
error-message="'Application not found'">
|
||||
<app-public-view repository="viewScope.repository"></app-public-view>
|
||||
</div>
|
|
@ -31,6 +31,14 @@
|
|||
<div class="section">
|
||||
<div class="new-header">
|
||||
<div class="namespace-selector-header">
|
||||
<span quay-show="Features.APP_REGISTRY">
|
||||
<span class="visible-xs xs-label">Repository Kind:</span>
|
||||
<select class="form-control repo-kind-select" ng-model="repo.repo_kind">
|
||||
<option value="image">Container Image Repository</option>
|
||||
<option value="application">Application Repository</option>
|
||||
</select>
|
||||
</span>
|
||||
|
||||
<span class="visible-xs xs-label">Namespace:</span>
|
||||
<span class="namespace-selector" user="user" namespace="repo.namespace" require-create="true"></span>
|
||||
|
||||
|
@ -49,6 +57,7 @@
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
|
@ -96,9 +105,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="repo.is_public == '1' || (!planRequired && !checkingPlan)">
|
||||
<div class="row" ng-show="Features.BUILD_SUPPORT">
|
||||
<div class="row" ng-show="Features.BUILD_SUPPORT && repo.repo_kind == 'image'">
|
||||
<div class="col-md-12">
|
||||
<div class="section">
|
||||
<div class="section-title">Initialize repository</div>
|
||||
|
@ -132,7 +140,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="repo.initialize == 'dockerfile' || repo.initialize == 'zipfile'">
|
||||
<div class="row" ng-show="(repo.initialize == 'dockerfile' || repo.initialize == 'zipfile') && repo.repo_kind == 'image'">
|
||||
<div class="col-md-12">
|
||||
<div class="section">
|
||||
<div class="section-title">Initialize from Dockerfile</div>
|
||||
|
@ -149,7 +157,7 @@
|
|||
|
||||
<div class="row"
|
||||
ng-repeat="type in TriggerService.getTypes()"
|
||||
ng-if="TriggerService.isEnabled(type) && repo.initialize == type">
|
||||
ng-if="TriggerService.isEnabled(type) && repo.initialize == type && repo.repo_kind == 'image'">
|
||||
<div class="col-md-12">
|
||||
<div class="co-alert co-alert-info">
|
||||
You will be redirected to authorize for {{ TriggerService.getTitle(type) }} once the repository has been created
|
||||
|
|
|
@ -57,7 +57,8 @@
|
|||
<div class="repo-list-panel co-main-content-panel">
|
||||
<div class="repo-list-view" namespaces="namespaces"
|
||||
star-toggled="starToggled(repository)"
|
||||
starred-repositories="starred_repositories">
|
||||
starred-repositories="starred_repositories"
|
||||
repo-kind="image">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -776,7 +776,7 @@ class RegistryTestsMixin(object):
|
|||
data = {
|
||||
'repository': 'someapprepo',
|
||||
'visibility': 'private',
|
||||
'kind': 'application',
|
||||
'repo_kind': 'application',
|
||||
'description': 'test app repo',
|
||||
}
|
||||
self.conduct('POST', '/api/v1/repository', json_data=data, expected_code=201)
|
||||
|
|
|
@ -1919,6 +1919,20 @@ class TestCreateRepo(ApiTestCase):
|
|||
self.assertEquals(ADMIN_ACCESS_USER, json['namespace'])
|
||||
self.assertEquals('newrepo', json['name'])
|
||||
|
||||
def test_create_app_repo(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
json = self.postJsonResponse(RepositoryList,
|
||||
data=dict(repository='newrepo',
|
||||
visibility='public',
|
||||
description='',
|
||||
repo_kind='application'),
|
||||
expected_code=201)
|
||||
|
||||
|
||||
self.assertEquals(ADMIN_ACCESS_USER, json['namespace'])
|
||||
self.assertEquals('newrepo', json['name'])
|
||||
self.assertEquals('application', json['kind'])
|
||||
|
||||
def test_createrepo_underorg(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
@ -1935,6 +1949,26 @@ class TestCreateRepo(ApiTestCase):
|
|||
|
||||
|
||||
class TestListRepos(ApiTestCase):
|
||||
def test_list_app_repos(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
||||
# Create an app repo.
|
||||
self.postJsonResponse(RepositoryList,
|
||||
data=dict(repository='newrepo',
|
||||
visibility='public',
|
||||
description='',
|
||||
repo_kind='application'),
|
||||
expected_code=201)
|
||||
|
||||
json = self.getJsonResponse(RepositoryList,
|
||||
params=dict(namespace=ADMIN_ACCESS_USER,
|
||||
public=False,
|
||||
repo_kind='application'))
|
||||
|
||||
self.assertEquals(1, len(json['repositories']))
|
||||
self.assertEquals('application', json['repositories'][0]['kind'])
|
||||
|
||||
|
||||
def test_listrepos_asguest(self):
|
||||
# Queries: Base + the list query
|
||||
with assert_query_count(BASE_QUERY_COUNT + 1):
|
||||
|
|
Reference in a new issue