Start on UI for Quay

This commit is contained in:
Joseph Schorr 2013-09-26 17:59:20 -04:00
parent 9278871381
commit 27ce5c00b2
10 changed files with 258 additions and 17 deletions

View file

@ -146,7 +146,7 @@ def get_tag_image(namespace_name, repository_name, tag_name):
joined = Image.select().join(RepositoryTag).join(Repository)
return joined.where(Repository.name == repository_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,

View file

@ -7,6 +7,8 @@ from functools import wraps
from data import model
from app import app
from util.names import parse_repository_name
from auth.permissions import (ReadRepositoryPermission,
ModifyRepositoryPermission)
logger = logging.getLogger(__name__)
@ -23,6 +25,7 @@ def create_repo_api():
pass
@app.route('/api/repository/', methods=['GET'])
@login_required
def list_repos_api():
@ -46,11 +49,59 @@ def list_repos_api():
@login_required
@parse_repository_name
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'])
@login_required
@parse_repository_name
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
View 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;
}

View file

@ -1,10 +1,23 @@
angular.module('quay', ['restangular']).
config(['$routeProvider', function($routeProvider) {
quayApp = angular.module('quay', ['restangular']).
config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
$routeProvider.
when('/repository/', {templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
when('/', {templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
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: '/'});
//$locationProvider.html5Mode(true);
}]).
config(function(RestangularProvider) {
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;
}
});
}]);

View file

@ -8,3 +8,33 @@ function RepoListCtrl($scope, Restangular) {
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';
});
}

View file

@ -1 +1,3 @@
<a ng-href="#/repository/">Repositories</a>
<div class="container">
<a ng-href="#/repository">Repositories</a>
</div>

View file

@ -1,4 +1,6 @@
<h2>Repositories</h2>
<div ng-repeat="repository in repositories">
{{repository.namespace}}/{{repository.name}}
</div>
<div class="container">
<h3>Repositories</h3>
<div ng-repeat="repository in repositories">
<a ng-href="#/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
</div>
</div>

View 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">&times;</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>

View file

@ -1,13 +1,19 @@
<!DOCTYPE html>
<html ng-app="quay">
<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">
<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="/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="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
<script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script>
@ -17,7 +23,46 @@
</head>
<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>
</body>
</html>
</html>

BIN
test.db

Binary file not shown.