Merge pull request #2489 from coreos-inc/apps-ui
Add basic user interface for application repos
This commit is contained in:
commit
a6954f246c
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