Major rewrite with just breaking changes

This commit is contained in:
Roman Vynar
2024-04-16 13:54:18 +03:00
parent f91c3b9aca
commit e334d4c6c7
44 changed files with 1201 additions and 1156 deletions

View File

@@ -4,19 +4,20 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Docker Registry UI</title>
<link rel="stylesheet" type="text/css" href="{{ basePath }}/static/datatables.min.css"/>
<script type="text/javascript" src="{{ basePath }}/static/datatables.min.js"></script>
<title>Registry UI</title>
<link rel="stylesheet" type="text/css" href="{{ basePath }}/static/css/bootstrap-icons.min.css">
<link rel="stylesheet" type="text/css" href="{{ basePath }}/static/css/datatables.min.css"/>
<script type="text/javascript" src="{{ basePath }}/static/js/datatables.min.js"></script>
{{yield head()}}
</head>
<body>
<div class="container">
<div style="float: left">
<h2><a href="{{ basePath }}/" style="text-decoration: none">Docker Registry UI</a></h2>
<h2><a href="{{ basePath }}/" style="text-decoration: none"><i class="bi-journals"></i> Registry UI</a></h2>
</div>
{{if eventsAllowed}}
<div style="float: right">
<h4><a href="{{ basePath }}/events">Event Log</a></h4>
<h4><a href="{{ basePath }}/event-log" style="text-decoration: none"><i class="bi-calendar-week"></i> Event Log</a></h4>
</div>
{{end}}
<div style="clear: both"></div>
@@ -25,7 +26,7 @@
<div style="padding: 10px 0; margin-bottom: 20px">
<div style="text-align: center; color:darkgrey">
Docker Registry UI v{{version}} | <a href="https://quiq.com">Quiq Inc.</a>
Registry UI v{{version}} | <a href="https://quiq.com" target="_blank">Quiq Inc.</a>
</div>
</div>
</div>

10
templates/breadcrumb.html Normal file
View File

@@ -0,0 +1,10 @@
{{ block breadcrumb() }}
<li><a href="{{ basePath }}/">{{ registryHost }}</a></li>
{{if . != nil}}
{{x := ""}}
{{range _, p := split(., "/")}}
{{x = x + "/" + p}}
<li><a href="{{ basePath }}{{ x }}">{{ p }}</a></li>
{{end}}
{{end}}
{{ end }}

121
templates/catalog.html Normal file
View File

@@ -0,0 +1,121 @@
{{extends "base.html"}}
{{import "breadcrumb.html"}}
{{block head()}}
<script type="text/javascript" src="{{ basePath }}/static/js/bootstrap-confirmation.min.js"></script>
<script type="text/javascript" src="{{ basePath }}/static/js/sorting_natural.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$('#datatable_repos').DataTable({
"pageLength": 10,
"stateSave": true,
"language": {
"emptyTable": "Catalog is being initializing..."
}
});
$('#datatable_tags').DataTable({
"pageLength": 10,
"order": [[ 0, 'desc' ]],
"stateSave": true,
columnDefs: [
{ type: 'natural', targets: 0 }
],
"language": {
"emptyTable": "No tags."
}
})
function populateConfirmation() {
$('[data-toggle=confirmation]').confirmation({
rootSelector: '[data-toggle=confirmation]',
container: 'body'
});
}
populateConfirmation()
$('#datatable_tags').on('draw.dt', populateConfirmation)
});
</script>
{{end}}
{{block body()}}
<ol class="breadcrumb">
{{ yield breadcrumb() repoPath }}
</ol>
{{if len(repos)>0 || !isCatalogReady}}
<h4>List of Repositories</h4>
<table id="datatable_repos" class="table table-striped table-bordered dataTables_wrapper">
<thead bgcolor="#ddd">
<tr>
<th>Repository</th>
<th width="20%">Tags</th>
</tr>
</thead>
<tbody>
{{range _, repo := repos}}
{{ full_repo_path := repoPath != "" ? repoPath+"/"+repo : repo }}
{{if !isset(tagCounts[full_repo_path]) || (isset(tagCounts[full_repo_path]) && tagCounts[full_repo_path] > 0)}}
<tr>
<td><i class="bi bi-folder2" style="margin-right: 10px"></i> <a href="{{ basePath }}/{{ full_repo_path }}">{{ repo }}</a></td>
<td>{{ tagCounts[full_repo_path] }}</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
{{end}} {* end repos *}
{{if len(tags)>0}}
<h4>List of Tags</h4>
<table id="datatable_tags" class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th>Tag Name</th>
</tr>
</thead>
<tbody>
{{range _, tag := tags}}
<tr>
<td>
<i class="bi bi-file-text" style="margin-right: 10px"></i> <a href="{{ basePath }}/{{ repoPath }}:{{ tag }}">{{ tag }}</a>
{{if deleteAllowed}}
<a href="{{ basePath }}/delete-tag?repoPath={{ repoPath }}&tag={{ tag }}" data-toggle="confirmation" class="btn btn-danger btn-xs pull-right" role="button">Delete</a>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
{{end}} {* end tags *}
{{if eventsAllowed and isset(events) }}
<h4>Latest activity</h4>
<table id="datatable_events" class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th>Action</th>
<th>Image</th>
<th>IP Address</th>
<th>User</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{{range _, e := events}}
<tr>
<td>{{ e.Action }}</td>
{{if hasPrefix(e.Tag,"sha256") }}
<td title="{{ e.Tag }}">{{ e.Repository }}@{{ e.Tag[:19] }}...</td>
{{else}}
<td>{{ e.Repository }}:{{ e.Tag }}</td>
{{end}}
<td>{{ e.IP }}</td>
<td>{{ e.User }}</td>
<td>{{ e.Created|pretty_time }}</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
{{end}}

View File

@@ -1,26 +1,63 @@
{{extends "base.html"}}
{{import "breadcrumb.html"}}
{{block head()}}
<script type="text/javascript">
$(document).ready(function() {
$('#datatable').DataTable({
var table = $('#datatable').DataTable({
"pageLength": 10,
"order": [[ 4, 'desc' ]],
"stateSave": true,
"stateSave": false,
"searchCols": [
null,
{search: $('input:checkbox[name="sha256_chk"]').val()},
],
"language": {
"emptyTable": "No events."
}
});
$.fn.dataTable.ext.search.push(function( settings, searchData, index, rowData, counter ) {
var action = $('input:checkbox[name="action_chk"]:checked').map(function() {
return this.value;
}).get();
if (action.length === 0) {
return true;
}
if (action.indexOf(searchData[0]) !== -1) {
return true;
}
return false;
});
$('input:checkbox[name="action_chk"]').on('change', function () {
table.draw();
});
$('input:checkbox[name="sha256_chk"]').on('change', function () {
if ($(this).prop('checked')) {
table.column(1).search($(this).val()).draw() ;
} else {
table.column(1).search('').draw() ;
}
});
});
</script>
{{end}}
{{block body()}}
<ol class="breadcrumb">
{{ yield breadcrumb() }}
<li class="active">Event Log</li>
</ol>
{{if eventsAllowed}}
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="action_chk" value="push">
<label class="form-check-label">Hide Pull</label>
<label class="form-check-label" style="margin-right:10px"></label>
<input class="form-check-input" type="checkbox" name="sha256_chk" value="!@sha256" checked>
<label class="form-check-label">Hide sha256 entries</label>
</div>
<table id="datatable" class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
@@ -32,7 +69,7 @@
</tr>
</thead>
<tbody>
{{range e := events}}
{{range _, e := events}}
<tr>
<td>{{ e.Action }}</td>
{{if hasPrefix(e.Tag,"sha256") }}

90
templates/image_info.html Normal file
View File

@@ -0,0 +1,90 @@
{{extends "base.html"}}
{{import "breadcrumb.html"}}
{{import "json_to_table.html"}}
{{block head()}}
<style>
/* col 0 style */
td:nth-child(1) {
color: #838383;
text-align: right;
}
/* td: long line wrap */
td {
word-break: break-word;
}
</style>
{{end}}
{{block body()}}
<ol class="breadcrumb">
{{ yield breadcrumb() ii.ImageRefRepo }}
<li><a href="{{ basePath }}/{{ repoPath }}">{{ ii.ImageRefTag }}</a></li>
</ol>
<h4>
{{if ii.IsImage}}<i class="bi-file-earmark" style="font-size: 2rem;"></i> Image{{end}}
{{if ii.IsImageIndex}}<i class="bi-files" style="font-size: 2rem;"></i> Image Index{{end}}
</h4>
<table class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th colspan="2">Summary</th>
</tr>
</thead>
<tr>
<td width="20%"><b>Image Reference</b></td><td>{{ registryHost }}/{{ repoPath }}</td>
</tr>
<tr>
<td><b>Digest</b></td><td><a href="{{ basePath }}/{{ ii.ImageRefRepo }}@{{ ii.ImageRefDigest }}">{{ ii.ImageRefDigest }}</a></td>
</tr>
<tr>
<td><b>Media Type</b></td><td>{{ ii.MediaType }}</td>
</tr>
{{if ii.IsImageIndex}}
<tr>
<td><b>Sub-Images</b></td><td>{{ len(ii.Manifest["manifests"]) }} </td>
</tr>
<tr>
<td><b>Platforms</b></td><td>{{ ii.Platforms }}</td>
</tr>
{{end}}
{{if ii.IsImage}}
<tr>
<td><b>Image ID</b></td><td>{{ ii.ConfigImageID }}</td>
</tr>
<tr>
<td><b>Image Size</b></td><td>{{ ii.ImageSize|pretty_size }}</td>
</tr>
<tr>
<td><b>Platform</b></td><td>{{ ii.Platforms }}</td>
</tr>
<tr>
<td><b>Created On</b></td><td>{{ ii.Created|pretty_time }}</td>
</tr>
{{end}}
</table>
<table class="table" style="margin-bottom: 0">
<thead bgcolor="#ddd">
<tr>
<th>{{if ii.IsImage}}Manifest{{else}}Index Manifest{{end}}</th>
</tr>
</thead>
</table>
{{ yield json_to_table() ii.Manifest }}
{{if ii.IsImage}}
<br>
<table class="table" style="margin-bottom: 0">
<thead bgcolor="#ddd">
<tr>
<th>Config File</th>
</tr>
</thead>
</table>
{{ yield json_to_table() ii.ConfigFile }}
{{end}}
{{end}}

View File

@@ -0,0 +1,32 @@
{{ block json_to_table() }}
{{ try }}
<table class="table table-striped table-bordered" style="margin-bottom: 0">
{{range i, k := sort_map_keys(.) }}
{{ v := .[k] }}
<tr>
<td width="15%" style="padding: 2px 8px;">{{k}}</td>
<td style="padding: 2px 8px;">
{{if ii.IsImage && k == "size"}}{{ pretty_size(v) }}
{{else if ii.IsImageIndex && k == "digest"}}<a href="{{ basePath }}/{{ ii.ImageRefRepo }}@{{ v }}">{{ v }}</a>
{{else}}{{ yield json_to_table() v }}{{end}}
</td>
</tr>
{{end}}
</table>
{{ catch err }}
{{if err.Error() == "reflect: call of reflect.Value.MapKeys on slice Value"}}
<table class="table table-striped table-bordered" style="margin-bottom: 0">
{{range _, e := . }}
<tr>
<td style="text-align: left; color: #000; padding: 0px 0px;">{{ yield json_to_table() e }}</td>
</tr>
{{end}}
</table>
{{else}}
{{ . }}
{{end}}
{{end}} {* end try *}
{{ end }}

View File

@@ -1,68 +0,0 @@
{{extends "base.html"}}
{{block head()}}
<script type="text/javascript">
$(document).ready(function() {
$('#namespace').on('change', function (e) {
window.location = '{{ basePath }}/' + this.value;
});
namespace = window.location.pathname;
namespace = namespace.replace("{{ basePath }}", "");
if (namespace == '/') {
namespace = 'library';
} else {
namespace = namespace.split('/')[1]
}
$('#namespace').val(namespace);
$('#datatable').DataTable({
"pageLength": 25,
"stateSave": true,
"language": {
"emptyTable": "No repositories in \"" + namespace + "\" namespace."
}
});
});
</script>
{{end}}
{{block body()}}
<div style="float: right">
<select id="namespace" class="form-control input-sm" style="height: 36px">
{{range namespace := namespaces}}
<option value="{{ namespace }}">{{ namespace }}</option>
{{end}}
</select>
</div>
<div style="float: right">
<ol class="breadcrumb">
<li class="active">Namespace</li>
</ol>
</div>
<ol class="breadcrumb">
<li><a href="{{ basePath }}/">{{ registryHost }}</a></li>
{{if namespace != "library"}}
<li><a href="{{ basePath }}/{{ namespace }}">{{ namespace }}</a></li>
{{end}}
</ol>
<table id="datatable" class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th>Repository</th>
<th width="20%">Tags</th>
</tr>
</thead>
<tbody>
{{range repo := repos}}
{{if !isset(tagCounts[namespace+"/"+repo]) || (isset(tagCounts[namespace+"/"+repo]) && tagCounts[namespace+"/"+repo] > 0)}}
<tr>
<td><a href="{{ basePath }}/{{ namespace }}/{{ repo|url }}">{{ repo }}</a></td>
<td>{{ tagCounts[namespace+"/"+repo] }}</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
{{end}}

View File

@@ -1,140 +0,0 @@
{{extends "base.html"}}
{{block head()}}
<style>
/* col 0 style */
td:nth-child(1) {
color: #838383;
text-align: right;
}
/* td: long line wrap */
td {
word-break: break-word;
}
</style>
{{end}}
{{block body()}}
<ol class="breadcrumb">
<li><a href="{{ basePath }}/">{{ registryHost }}</a></li>
{{if namespace != "library"}}
<li><a href="{{ basePath }}/{{ namespace }}">{{ namespace }}</a></li>
{{end}}
<li><a href="{{ basePath }}/{{ namespace }}/{{ repo }}">{{ repo|url_decode }}</a></li>
<li class="active">{{ tag }}</li>
</ol>
<h4>Image Details</h4>
<table class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th colspan="2">Summary</th>
</tr>
</thead>
<tr>
<td width="20%"><b>Image URL</b></td><td>{{ registryHost }}/{{ repoPath }}{{ isDigest ? "@" : ":" }}{{ tag }}</td>
</tr>
<tr>
<td><b>Digest</b></td><td>sha256:{{ sha256 }}</td>
</tr>
{{if created}}
<tr>
<td><b>Created On</b></td><td>{{ created|pretty_time }}</td>
</tr>
{{end}}
{{if not digestList}}
<tr>
<td><b>Image Size</b></td><td>{{ imageSize|pretty_size }}</td>
</tr>
<tr>
<td><b>Layer Count</b></td><td>{{ layersCount }}</td>
</tr>
{{end}}
<tr>
<td><b>Manifest Formats</b></td>
<td>{{if not isDigest}}Manifest v2 schema 1{{else}}<font color="#c2c2c2">Manifest v2 schema 1</font>{{end}} |
{{if not digestList && layersV2}}Manifest v2 schema 2{{else}}<font color="#c2c2c2">Manifest v2 schema 2</font>{{end}} |
{{if digestList}}Manifest List v2 schema 2{{else}}<font color="#c2c2c2">Manifest List v2 schema 2</font>{{end}}
</td>
</tr>
</table>
{{if digestList}}
<h4>Sub-images <!-- Manifest List v2 schema 2: multi-arch or cache image --></h4>
{{range index, manifest := digestList}}
<table class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th colspan="2">Manifest #{{ index+1 }}</th>
</tr>
</thead>
{{range key := manifest["ordered_keys"]}}
<tr>
<td width="20%">{{ key }}</td>
{{if key == "platform" || key == "annotations"}}
<td style="padding: 0">
<table class="table table-bordered" style="padding: 0; width: 100%; margin-bottom: 0; min-height: 37px">
<!-- Nested range does not work. Iterating via filter over the map. -->
{{ manifest[key]|parse_map|raw }}
</table>
</td>
{{else if key == "size"}}
<td>{{ manifest[key]|pretty_size }}</td>
{{else}}
<td>{{ manifest[key]|raw }}</td>
{{end}}
</tr>
{{end}}
</table>
{{end}}
{{else if layersV2}}
<h4>Blobs <!-- Manifest v2 schema 2--></h4>
<table class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th>Layer #</th>
<th>Digest</th>
<th>Size</th>
</tr>
</thead>
{{range index, layer := layersV2}}
<tr>
<td>{{ len(layersV2)-index }}</td>
<td>{{ layer["digest"] }}</td>
<td>{{ layer["size"]|pretty_size }}</td>
</tr>
{{end}}
</table>
{{end}}
{{if not isDigest && layersV1}}
<h4>Image History <!-- Manifest v2 schema 1--></h4>
{{range index, layer := layersV1}}
<table class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th colspan="2">Layer #{{ len(layersV1)-index }}</th>
</tr>
</thead>
{{range key := layer["ordered_keys"]}}
<tr>
<td width="20%">{{ key }}</td>
{{if key == "config" || key == "container_config"}}
<td style="padding: 0">
<table class="table table-bordered" style="padding: 0; width: 100%; margin-bottom: 0; min-height: 37px">
<!-- Nested range does not work. Iterating via filter over the map. -->
{{ layer[key]|parse_map|raw }}
</table>
</td>
{{else if key == "created"}}
<td>{{ layer[key]|pretty_time }}</td>
{{else}}
<td>{{ layer[key] }}</td>
{{end}}
</tr>
{{end}}
</table>
{{end}}
{{end}}
{{end}}

View File

@@ -1,92 +0,0 @@
{{extends "base.html"}}
{{block head()}}
<script type="text/javascript" src="{{ basePath }}/static/bootstrap-confirmation.min.js"></script>
<script type="text/javascript" src="{{ basePath }}/static/sorting_natural.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$('#datatable').DataTable({
"pageLength": 10,
"order": [[ 0, 'desc' ]],
"stateSave": true,
columnDefs: [
{ type: 'natural', targets: 0 }
],
"language": {
"emptyTable": "No tags in this repository."
}
})
function populateConfirmation() {
$('[data-toggle=confirmation]').confirmation({
rootSelector: '[data-toggle=confirmation]',
container: 'body'
});
}
populateConfirmation()
$('#datatable').on('draw.dt', populateConfirmation)
});
</script>
{{end}}
{{block body()}}
<ol class="breadcrumb">
<li><a href="{{ basePath }}/">{{ registryHost }}</a></li>
{{if namespace != "library"}}
<li><a href="{{ basePath }}/{{ namespace }}">{{ namespace }}</a></li>
{{end}}
<li class="active">{{ repo|url_decode }}</li>
</ol>
<table id="datatable" class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th>Tag Name</th>
</tr>
</thead>
<tbody>
{{range tag := tags}}
<tr>
<td>
<a href="{{ basePath }}/{{ namespace }}/{{ repo }}/{{ tag }}">{{ tag }}</a>
{{if deleteAllowed}}
<a href="{{ basePath }}/{{ namespace }}/{{ repo }}/{{ tag }}/delete" data-toggle="confirmation" class="btn btn-danger btn-xs pull-right" role="button">Delete</a>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
{{if eventsAllowed}}
<h4>Latest events on this repo</h4>
<table id="datatable_log" class="table table-striped table-bordered">
<thead bgcolor="#ddd">
<tr>
<th>Action</th>
<th>Image</th>
<th>IP Address</th>
<th>User</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{{range e := events}}
<tr>
<td>{{ e.Action }}</td>
{{if hasPrefix(e.Tag,"sha256") }}
<td title="{{ e.Tag }}">{{ e.Repository }}@{{ e.Tag[:19] }}...</td>
{{else}}
<td>{{ e.Repository }}:{{ e.Tag }}</td>
{{end}}
<td>{{ e.IP }}</td>
<td>{{ e.User }}</td>
<td>{{ e.Created|pretty_time }}</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
{{end}}