Start on UI for Quay
This commit is contained in:
parent
9278871381
commit
27ce5c00b2
10 changed files with 258 additions and 17 deletions
|
@ -146,7 +146,7 @@ def get_tag_image(namespace_name, repository_name, tag_name):
|
||||||
joined = Image.select().join(RepositoryTag).join(Repository)
|
joined = Image.select().join(RepositoryTag).join(Repository)
|
||||||
return joined.where(Repository.name == repository_name,
|
return joined.where(Repository.name == repository_name,
|
||||||
Repository.namespace == namespace_name,
|
Repository.namespace == namespace_name,
|
||||||
RepositoryTag.name == tag_name)
|
RepositoryTag.name == tag_name).execute()[0]
|
||||||
|
|
||||||
|
|
||||||
def create_or_update_tag(namespace_name, repository_name, tag_name,
|
def create_or_update_tag(namespace_name, repository_name, tag_name,
|
||||||
|
|
|
@ -7,6 +7,8 @@ from functools import wraps
|
||||||
from data import model
|
from data import model
|
||||||
from app import app
|
from app import app
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
|
from auth.permissions import (ReadRepositoryPermission,
|
||||||
|
ModifyRepositoryPermission)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -23,6 +25,7 @@ def create_repo_api():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/', methods=['GET'])
|
@app.route('/api/repository/', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def list_repos_api():
|
def list_repos_api():
|
||||||
|
@ -46,11 +49,59 @@ def list_repos_api():
|
||||||
@login_required
|
@login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def update_repo_api(namespace, repository):
|
def update_repo_api(namespace, repository):
|
||||||
pass
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
if permission.can():
|
||||||
|
repo = model.get_repository(namespace, repository)
|
||||||
|
if repo:
|
||||||
|
values = request.get_json()
|
||||||
|
repo.description = values['description']
|
||||||
|
repo.save()
|
||||||
|
return jsonify({
|
||||||
|
'success': True
|
||||||
|
})
|
||||||
|
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/<path:repository>', methods=['GET'])
|
@app.route('/api/repository/<path:repository>', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def get_repo_api(namespace, repository):
|
def get_repo_api(namespace, repository):
|
||||||
pass
|
def image_view(image):
|
||||||
|
return {
|
||||||
|
'id': image.image_id,
|
||||||
|
'created': image.created,
|
||||||
|
'comment': image.comment
|
||||||
|
}
|
||||||
|
|
||||||
|
def tag_view(tag):
|
||||||
|
image = model.get_tag_image(namespace, repository, tag.name)
|
||||||
|
if not image:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'name': tag.name,
|
||||||
|
'image': image_view(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
def repo_view(repository, tags = []):
|
||||||
|
tag_list = []
|
||||||
|
for tag in tags:
|
||||||
|
tag_list.append(tag_view(tag))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'namespace': repository.namespace,
|
||||||
|
'name': repository.name,
|
||||||
|
'description': repository.description,
|
||||||
|
'tags': tag_list,
|
||||||
|
'can_write': ModifyRepositoryPermission(repository.namespace, repository.name).can()
|
||||||
|
}
|
||||||
|
|
||||||
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
if permission.can():
|
||||||
|
repo = model.get_repository(namespace, repository)
|
||||||
|
if repo:
|
||||||
|
tags = model.list_repository_tags(namespace, repository)
|
||||||
|
return jsonify(repo_view(repo, tags = tags))
|
||||||
|
|
||||||
|
abort(404)
|
||||||
|
|
40
static/css/quay.css
Normal file
40
static/css/quay.css
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
.editable .glyphicon {
|
||||||
|
opacity: 0.2;
|
||||||
|
font-size: 85%;
|
||||||
|
margin-left: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
transition: opacity 500ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noteditable .glyphicon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.editable .content:empty:after {
|
||||||
|
display: inline-block;
|
||||||
|
content: "(Click to add)";
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.editable:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.editable:hover .glyphicon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-dropdown {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
margin-right: 15px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
border: 0px;
|
||||||
|
}
|
|
@ -1,10 +1,23 @@
|
||||||
angular.module('quay', ['restangular']).
|
quayApp = angular.module('quay', ['restangular']).
|
||||||
config(['$routeProvider', function($routeProvider) {
|
config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
|
||||||
$routeProvider.
|
$routeProvider.
|
||||||
when('/repository/', {templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
|
||||||
when('/', {templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
|
when('/repository/:namespace/:name/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
|
||||||
|
|
||||||
|
when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
||||||
|
when('/', {title: 'Quay', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
|
||||||
otherwise({redirectTo: '/'});
|
otherwise({redirectTo: '/'});
|
||||||
|
|
||||||
|
//$locationProvider.html5Mode(true);
|
||||||
}]).
|
}]).
|
||||||
config(function(RestangularProvider) {
|
config(function(RestangularProvider) {
|
||||||
RestangularProvider.setBaseUrl('/api/');
|
RestangularProvider.setBaseUrl('/api/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
quayApp.run(['$location', '$rootScope', function($location, $rootScope) {
|
||||||
|
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
|
||||||
|
if (current.$$route.title) {
|
||||||
|
$rootScope.title = current.$$route.title;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}]);
|
|
@ -8,3 +8,33 @@ function RepoListCtrl($scope, Restangular) {
|
||||||
function LandingCtrl($scope) {
|
function LandingCtrl($scope) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RepoCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
|
$rootScope.title = 'Loading...';
|
||||||
|
|
||||||
|
$scope.editDescription = function() {
|
||||||
|
if (!$scope.repo.can_write) { return; }
|
||||||
|
$('#descriptionEdit')[0].value = $scope.repo.description || '';
|
||||||
|
$('#editModal').modal({});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.saveDescription = function() {
|
||||||
|
$('#editModal').modal('hide');
|
||||||
|
$scope.repo.description = $('#descriptionEdit')[0].value;
|
||||||
|
$scope.repo.put();
|
||||||
|
};
|
||||||
|
|
||||||
|
var namespace = $routeParams.namespace;
|
||||||
|
var name = $routeParams.name;
|
||||||
|
var tag = $routeParams.tag || 'latest';
|
||||||
|
|
||||||
|
var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name);
|
||||||
|
repositoryFetch.get().then(function(repo) {
|
||||||
|
$rootScope.title = namespace + '/' + name;
|
||||||
|
$scope.repo = repo;
|
||||||
|
$scope.currentTag = repo.tags[tag] || repo.tags['latest'];
|
||||||
|
}, function() {
|
||||||
|
$scope.repo = null;
|
||||||
|
$rootScope.title = 'Unknown Repository';
|
||||||
|
});
|
||||||
|
}
|
|
@ -1 +1,3 @@
|
||||||
<a ng-href="#/repository/">Repositories</a>
|
<div class="container">
|
||||||
|
<a ng-href="#/repository">Repositories</a>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<h2>Repositories</h2>
|
<div class="container">
|
||||||
|
<h3>Repositories</h3>
|
||||||
<div ng-repeat="repository in repositories">
|
<div ng-repeat="repository in repositories">
|
||||||
{{repository.namespace}}/{{repository.name}}
|
<a ng-href="#/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
58
static/partials/view-repo.html
Normal file
58
static/partials/view-repo.html
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<div class="container" ng-hide="repo">
|
||||||
|
No repository found
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" ng-show="repo">
|
||||||
|
<!-- Repo Header -->
|
||||||
|
<div class="header">
|
||||||
|
<h3>
|
||||||
|
<span class="glyphicon glyphicon-hdd"></span> <span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p ng-class="'lead ' + (repo.can_write ? 'editable' : 'noteditable')" ng-click="editDescription()"><span class="content">{{repo.description}}</span><span class="glyphicon glyphicon-pencil"></span></p>
|
||||||
|
|
||||||
|
<!-- Tab bar -->
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li>
|
||||||
|
<span class="tag-dropdown dropdown">
|
||||||
|
<span class="glyphicon glyphicon-bookmark"></span>
|
||||||
|
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag.name}} <b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li ng-repeat="tag in repo.tags">
|
||||||
|
<a href="{{ '#/repository/' + repo.namespace + '/' + repo.name + '/' + tag.name }}">{{tag.name}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
<li class="active"><a href="javascript:void(0)">Current Image</a></li>
|
||||||
|
<li><a href="javascript:void(0)">Image History</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
Loading...
|
||||||
|
|
||||||
|
<div id="current-image">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal edit for the description -->
|
||||||
|
<div class="modal fade" id="editModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Edit Repository Description</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<textarea id="descriptionEdit" placeholder="Enter description">{{ repo.description }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" ng-click="saveDescription()">Save changes</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
</div>
|
|
@ -1,13 +1,19 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html ng-app="quay">
|
<html ng-app="quay">
|
||||||
<head>
|
<head>
|
||||||
<title>Quay - Private Docker Repository</title>
|
<title ng-bind="title + ' · Quay'">Quay - Private Docker Repository</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css">
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-theme.min.css">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/css/quay.css">
|
||||||
|
|
||||||
|
|
||||||
|
<script src="//code.jquery.com/jquery.js"></script>
|
||||||
|
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
|
||||||
|
|
||||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
|
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
|
||||||
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
|
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
|
||||||
<script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script>
|
<script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script>
|
||||||
|
@ -17,7 +23,46 @@
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Hello World</h1>
|
<!-- Nav bar -->
|
||||||
|
<nav class="navbar navbar-default" role="navigation">
|
||||||
|
<!-- Quay -->
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand" href="#">Quay</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Collapsable stuff -->
|
||||||
|
<div class="collapse navbar-collapse navbar-ex1-collapse">
|
||||||
|
<ul class="nav navbar-nav">
|
||||||
|
<li><a ng-href="/repository">Repositories</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<ul class="nav navbar-nav navbar-right">
|
||||||
|
<form class="navbar-form navbar-left" role="search">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-control" placeholder="Find Repo">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<li class="dropdown">
|
||||||
|
<!--<button type="button" class="btn btn-default navbar-btn">Sign in</button>-->
|
||||||
|
|
||||||
|
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">devtable <b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a href="#">Settings</a></li>
|
||||||
|
<li><a href="#">Sign out</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div><!-- /.navbar-collapse -->
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div ng-view></div>
|
<div ng-view></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
BIN
test.db
BIN
test.db
Binary file not shown.
Reference in a new issue