Graphing/Edit Image

- Updated graph on view image page
  - Initial form format for edit image page.
This commit is contained in:
Josh Stark 2020-01-04 18:29:03 +00:00
parent cf5b277ff2
commit 4ccca5e55a
25 changed files with 614 additions and 21 deletions

View File

@ -161,4 +161,10 @@ public class FleetAppController extends AbstractAppController implements Service
public final AuthenticationResult authenticateUser(final String username, final String password) {
return authenticationDelegate.authenticate(username, password);
}
public final void trackBranch(final ImageKey imageKey, final String branchName) {
getRepositoryService().trackBranchOnImage(imageKey, branchName);
synchroniseImage(imageKey);
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.v2;
public final class Utils {
public static <T> T ensureNotNull(final T obj) {
if (null == obj) {
throw new IllegalArgumentException("Parameter null");
}
return obj;
}
}

View File

@ -33,6 +33,7 @@ import io.linuxserver.fleet.v2.types.meta.history.ImagePullHistory;
import io.linuxserver.fleet.v2.types.meta.history.ImagePullStatistic;
import java.sql.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
@ -58,6 +59,7 @@ public class DefaultImageDAO extends AbstractDAO implements ImageDAO {
private static final String GetImage = "{CALL Image_Get(?)}";
private static final String DeleteImage = "{CALL Image_Delete(?)}";
private static final String GetImageStats = "{CALL Image_GetStats(?)}";
private static final String DeleteStats = "{CALL Image_ClearStatsBefore(?)}";
public DefaultImageDAO(final DatabaseProvider databaseConnection) {
super(databaseConnection);

View File

@ -27,6 +27,7 @@ import io.linuxserver.fleet.v2.types.internal.ImageOutlineRequest;
import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest;
import io.linuxserver.fleet.v2.types.internal.TagBranchOutlineRequest;
import java.time.LocalDate;
import java.util.List;
public interface ImageDAO {

View File

@ -17,12 +17,14 @@
package io.linuxserver.fleet.v2.key;
import io.linuxserver.fleet.v2.Utils;
public abstract class AbstractHasKey<KEY extends Key> implements HasKey<KEY> {
private final KEY key;
public AbstractHasKey(final KEY key) {
this.key = key;
this.key = Utils.ensureNotNull(key);
}
@Override

View File

@ -29,6 +29,7 @@ import io.linuxserver.fleet.v2.types.docker.DockerImage;
import io.linuxserver.fleet.v2.types.docker.DockerTag;
import io.linuxserver.fleet.v2.types.internal.ImageOutlineRequest;
import io.linuxserver.fleet.v2.types.internal.RepositoryOutlineRequest;
import io.linuxserver.fleet.v2.types.internal.TagBranchOutlineRequest;
import io.linuxserver.fleet.v2.types.meta.ItemSyncSpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -232,6 +233,27 @@ public class RepositoryService {
}
}
public void trackBranchOnImage(final ImageKey imageKey, final String branchName) {
final Image image = repositoryCache.findImage(imageKey);
if (null == image) {
throw new IllegalArgumentException("Could not find image with key " + imageKey);
}
if (image.findTagBranchByName(branchName) != null) {
throw new IllegalArgumentException("Image is already tracking branch " + branchName);
}
final InsertUpdateResult<TagBranch> outlineResult = imageDAO.createTagBranchOutline(new TagBranchOutlineRequest(imageKey, branchName));
if (outlineResult.isError()) {
throw new RuntimeException(outlineResult.getStatusMessage());
}
final Image updatableClone = image.cloneForUpdate();
updatableClone.addTagBranch(outlineResult.getResult());
storeImage(updatableClone);
}
private void updateCache(final Image storedImage) {
final Repository imageParentRepository = repositoryCache.findItem(storedImage.getRepositoryKey());

View File

@ -28,6 +28,6 @@ public class CheckAppVersionSchedule extends AbstractAppSchedule {
@Override
public void executeSchedule() {
getLogger().info("Currently not implemented. This is a placeholder schedule");
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.v2.thread.schedule;
import io.linuxserver.fleet.core.FleetAppController;
public class TidyHistoricDataSchedule extends AbstractAppSchedule {
public TidyHistoricDataSchedule(final ScheduleSpec spec,
final FleetAppController controller) {
super(spec, controller);
}
@Override
public void executeSchedule() {
getLogger().info("Currently not implemented. This is a placeholder schedule");
}
}

View File

@ -67,6 +67,10 @@ public class Image extends AbstractSyncItem<ImageKey, Image> {
return cloned;
}
public final Image cloneForUpdate() {
return cloneWithSyncSpec(getSpec());
}
@Override
public final Image cloneWithSyncSpec(final ItemSyncSpec syncSpec) {

View File

@ -18,6 +18,8 @@
package io.linuxserver.fleet.v2.types;
import io.linuxserver.fleet.v2.key.AbstractHasKey;
import io.linuxserver.fleet.v2.key.HasKey;
import io.linuxserver.fleet.v2.key.ImageKey;
import io.linuxserver.fleet.v2.key.TagBranchKey;
import java.util.concurrent.atomic.AtomicReference;

View File

@ -24,8 +24,16 @@ import java.util.stream.Collectors;
public class ApiImagePullHistoryWrapper extends AbstractApiWrapper<List<ImagePullStatistic>> {
public ApiImagePullHistoryWrapper(final List<ImagePullStatistic> originalObject) {
private final ImagePullStatistic.StatGroupMode groupMode;
public ApiImagePullHistoryWrapper(final List<ImagePullStatistic> originalObject,
final ImagePullStatistic.StatGroupMode groupMode) {
super(originalObject);
this.groupMode = groupMode;
}
public final String getGroupModeDataPoint() {
return groupMode.getDataPoint();
}
public final List<String> getLabels() {
@ -36,6 +44,10 @@ public class ApiImagePullHistoryWrapper extends AbstractApiWrapper<List<ImagePul
return getOriginalObject().stream().map(ImagePullStatistic::getPullCount).collect(Collectors.toList());
}
public final long getMean() {
return (long) getPullDifferential().getPulls().stream().mapToLong(Long::longValue).average().orElse(0.0);
}
public final PullDifferentialsWithLabels getPullDifferential() {
final PullDifferentialsWithLabels differentialsWithLabels = new PullDifferentialsWithLabels();

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2020 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.v2.types.docker;
public enum DockerCapability {
AUDIT_CONTROL,
AUDIT_WRITE,
BLOCK_SUSPEND,
CHOWN,
DAC_OVERRIDE,
DAC_READ_SEARCH,
FOWNER,
FSETID,
IPC_LOCK,
IPC_OWNER,
KILL,
LEASE,
LINUX_IMMUTABLE,
MAC_ADMIN,
MAC_OVERRIDE,
MKNOD,
NET_ADMIN,
NET_BIND_SERVICE,
NET_BROADCAST,
NET_RAW,
SETFCAP,
SETGID,
SETPCAP,
SETUID,
SYSLOG,
SYS_ADMIN,
SYS_BOOT,
SYS_CHROOT,
SYS_MODULE,
SYS_NICE,
SYS_PACCT,
SYS_PTRACE,
SYS_RAWIO,
SYS_RESOURCE,
SYS_TIME,
SYS_TTY_CONFIG,
WAKE_ALARM;
}

View File

@ -76,6 +76,19 @@ public class ImagePullStatistic implements Comparable<ImagePullStatistic> {
}
public enum StatGroupMode {
Day, Week, Month;
Day("hour"),
Week("day"),
Month("day");
private final String dataPoints;
StatGroupMode(final String dataPoints) {
this.dataPoints = dataPoints;
}
public final String getDataPoint() {
return dataPoints;
}
}
}

View File

@ -45,12 +45,14 @@ public interface Locations {
String Schedule = "schedule";
String Sync = "sync";
String Stats = "stats";
String Track = "track";
}
interface Admin {
String Repositories = "/admin/repositories";
String Images = "/admin/images";
String ImageEdit = "/admin/image";
String Schedules = "/admin/schedules";
String Users = "/admin/users";
}

View File

@ -64,6 +64,7 @@ public class WebRouteController {
get(Locations.Admin.Repositories, new AdminRepositoryController(app), roles(AppRole.Anyone));
get(Locations.Admin.Images, new AdminImageController( app), roles(AppRole.Anyone));
get(Locations.Admin.ImageEdit, new AdminImageEditController( app), roles(AppRole.Anyone));
get(Locations.Admin.Schedules, new AdminScheduleController( app), roles(AppRole.Anyone));
path(Locations.Internal.Api, () -> {
@ -90,6 +91,10 @@ public class WebRouteController {
path(Locations.Internal.Stats, () -> {
get(apiController::getImagePullHistory, roles(AppRole.Anyone));
});
path(Locations.Internal.Track, () -> {
put(apiController::trackNewBranch, roles(AppRole.Anyone));
});
});
path(Locations.Internal.Schedule, () -> {

View File

@ -0,0 +1,56 @@
/*
* Copyright (c) 2019 LinuxServer.io
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.linuxserver.fleet.v2.web.routes;
import io.javalin.http.Context;
import io.linuxserver.fleet.core.FleetAppController;
import io.linuxserver.fleet.v2.key.ImageKey;
import io.linuxserver.fleet.v2.service.RepositoryService;
import io.linuxserver.fleet.v2.types.docker.DockerCapability;
import io.linuxserver.fleet.v2.web.PageModelSpec;
public class AdminImageEditController extends AbstractPageHandler {
private RepositoryService repositoryService;
public AdminImageEditController(final FleetAppController controller) {
super(controller);
repositoryService = controller.getRepositoryService();
}
@Override
protected PageModelSpec handlePageLoad(final Context ctx) {
final String imageKeyParam = ctx.queryParam("imageKey");
if (null != imageKeyParam) {
final PageModelSpec modelSpec = new PageModelSpec("views/pages/admin/image-edit.ftl");
modelSpec.addModelAttribute("image", repositoryService.getImage(ImageKey.parse(imageKeyParam)));
modelSpec.addModelAttribute("containerCapabilities", DockerCapability.values());
return modelSpec;
} else {
return new PageModelSpec("views/pages/not-found.ftl");
}
}
@Override
protected PageModelSpec handleFormSubmission(final Context ctx) {
return null;
}
}

View File

@ -172,7 +172,21 @@ public class InternalApiController extends AbstractAppService {
final ImagePullStatistic.StatGroupMode groupMode = ctx.queryParam("groupMode", ImagePullStatistic.StatGroupMode.class).get();
final Image cachedImage = getController().getRepositoryService().getImage(ImageKey.parse(imageKeyParam));
ctx.json(new ApiImagePullHistoryWrapper(cachedImage.getMetaData().getHistoryFor(groupMode)));
ctx.json(new ApiImagePullHistoryWrapper(cachedImage.getMetaData().getHistoryFor(groupMode), groupMode));
} catch (IllegalArgumentException e) {
throw new ApiException(e.getMessage(), e);
}
}
public void trackNewBranch(final Context ctx) {
try {
final String imageKeyParam = ctx.formParam("imageKey", String.class).get();
final String branchName = ctx.formParam("branchName", String.class).get();
getController().trackBranch(ImageKey.parse(imageKeyParam), branchName);
} catch (IllegalArgumentException e) {
throw new ApiException(e.getMessage(), e);

View File

@ -24,4 +24,5 @@ VALUES
('SyncAllCachedImages', '1:hours', '0:minutes', 'io.linuxserver.fleet.v2.thread.schedule.sync.AllImagesSyncSchedule'),
('GetMissingImages', '30:minutes', '0:minutes', 'io.linuxserver.fleet.v2.thread.schedule.sync.GetMissingImagesSchedule'),
('RefreshCache', '1:days', '15:minutes', 'io.linuxserver.fleet.v2.thread.schedule.cache.RefreshCacheSchedule'),
('TidyHistoricData', '1:days', '0:minutes', 'io.linuxserver.fleet.v2.thread.schedule.TidyHistoricDataSchedule'),
('CheckAppVersion', '1:days', '0:minutes', 'io.linuxserver.fleet.v2.thread.schedule.CheckAppVersionSchedule');

View File

@ -187,6 +187,23 @@ var adminManager = (function($) {
ajaxManager.call(request, function() {});
};
var trackNewBranch = function(branchName, imageKey) {
var request = {
url: '/internalapi/image/track',
method: 'put',
data: {
'imageKey': imageKey,
'branchName': branchName
}
};
ajaxManager.call(request, function() {
window.location.reload();
});
};
var cleanEmpty = function(val) {
return (typeof val === 'undefined' || $.trim(val).length === 0) ? null : val;
};
@ -233,6 +250,14 @@ var adminManager = (function($) {
$('.sync-image').on('click', function() {
syncImage($(this));
});
$('#TrackNewBranch').on('click', function() {
var branchName = $.trim($('#NewTrackedBranch').val());
if (branchName.length > 0) {
trackNewBranch(branchName, $('#ImageKey').val());
}
});
};
return {

View File

@ -268,6 +268,19 @@ var imageSearchManager = (function($) {
var chartManager = (function($) {
var formatNumber = function(num) {
var array = num.toString().split('');
var index = -3;
while (array.length + index > 0) {
array.splice(index, 0, ',');
index -= 4;
}
return array.join('');
};
var populateChart = function(imageKey, groupMode) {
var request = {
@ -278,16 +291,29 @@ var chartManager = (function($) {
ajaxManager.call(request, function(history) {
var ctx = document.getElementById('ImagePullHistory').getContext('2d');
$('#PullActivityDataPoint').text(history.groupModeDataPoint);
$('#PullActivityRate').text(formatNumber(history.mean));
var ctx = document.getElementById('ImagePullHistory').getContext('2d');
var gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'rgba(0, 209, 178, 0.5)');
gradient.addColorStop(0.3, 'rgba(0, 209, 178, 0)');
new Chart(ctx, {
type: 'line',
data: {
labels: history.pullDifferential.labels,
datasets: [{
data: history.pullDifferential.pulls,
borderColor: 'rgba(0, 209, 178, 1)',
backgroundColor: 'rgba(0, 209, 178, 0.3)'
}]
datasets: [
{
lineTension: 0,
data: history.pullDifferential.pulls,
pointRadius: 0,
pointHitRadius: 2,
borderWidth: 2,
borderColor: 'rgba(0, 209, 178, 1)',
backgroundColor : gradient
}
]
},
options: {
responsive: true,

View File

@ -1,5 +1,5 @@
#Fri Jan 03 17:22:49 GMT 2020
app.build.date=2020-01-03T17\:22\:49
#Sat Jan 04 18:27:43 GMT 2020
app.build.date=2020-01-04T18\:27\:43
app.build.os=Linux
app.build.user=josh
app.version=2.0.0

View File

@ -0,0 +1,270 @@
<#--
Copyright (c) 2019 LinuxServer.io
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<#import "../../prebuilt/base.ftl" as base />
<#import "../../prebuilt/fleet-title.ftl" as title />
<#import "../../ui/components/dropdown.ftl" as dropdown />
<#import "../../ui/layout/section.ftl" as section />
<#import "../../ui/layout/container.ftl" as container />
<#import "../../ui/form/input.ftl" as input />
<#import "../../ui/elements/button.ftl" as button />
<#import "../../ui/elements/table.ftl" as table />
<@base.base title='Edit ${image.name} | Admin' context="admin_image_edit">
<#if image?has_content>
<input type="hidden" id="ImageKey" value="${image.key}" />
<@section.section id="ManageImage">
<@container.container>
<div class="columns is-multiline">
<div class="column is-12">
<@title.title icon="cube" thinValue=image.repositoryName boldValue=image.name separator="/" subtitle="Update metadata and tracked branches" />
</div>
<#--
Tag branches
-->
<div class="column is-half-desktop is-full-mobile has-margin-top">
<h2 class="title is-4">Tracked Tag Branches</h2>
<@table.table id="ImageTrackedBranches" isScrollable=true isFullWidth=true>
<thead>
<tr>
<th>Branch Name</th>
<th></th>
</tr>
</thead>
<tbody>
<#list image.tagBranches as tagBranch>
<tr>
<td class="is-vcentered">${tagBranch.branchName}</td>
<td>
<#if !tagBranch.branchProtected>
<@button.buttons isRightAligned=true>
<@button.button extraClasses="remove-tag-branch" colour="danger" size="small">
<i class="fas fa-trash"></i> Remove
</@button.button>
</@button.buttons>
<#else>
<@button.buttons isRightAligned=true>
<@button.button colour="light" size="small" isDisabled=true>
Protected
</@button.button>
</@button.buttons>
</#if>
</td>
</tr>
</#list>
<tr>
<td>
<@input.text id="NewTrackedBranch" icon="sitemap" size="small" />
</td>
<td>
<@button.buttons isRightAligned=true>
<@button.button id="TrackNewBranch" size="small" colour="success">
<i class="fas fa-plus"></i> Track
</@button.button>
</@button.buttons>
</td>
</tr>
</tbody>
</@table.table>
</div>
<#--
General base information which is to be added manually (data which can't necessarily be inferred from upstream)
-->
<div class="column is-12 has-margin-top">
<h2 class="title is-4">General</h2>
</div>
<div class="column is-half-desktop is-full-tablet is-full-mobile">
<@input.text id="ImageBase" label="Base Image" />
</div>
<div class="column is-half-desktop is-full-tablet is-full-mobile">
<@input.text id="ImageCategory" label="Category" />
</div>
<div class="column is-half-desktop is-full-tablet is-full-mobile">
<@input.text id="ImageSupportUrl" label="Support Url" />
</div>
<div class="column is-half-desktop is-full-tablet is-full-mobile">
<@input.text id="ImageAppUrl" label="Application Url" />
</div>
<#--
A display logo for the grid listing and main image display page
-->
<div class="column is-half-desktop is-full-tablet is-full-mobile has-margin-top">
<h2 class="title is-4">App Logo</h2>
<div class="file has-name is-boxed">
<label class="file-label">
<input class="file-input" type="file" name="ImageLogo" id="ImageLogo" />
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label">
Choose a file...
</span>
</span>
<span class="file-name">
Screen Shot 2017-07-29 at 15.54.25.png
</span>
</label>
</div>
</div>
<#--
Port/Volume mappings for containers created from this image
-->
<div class="column is-12 has-margin-top">
<h2 class="title is-4">Container Template</h2>
<h3 class="title is-5">Recommended Runtime</h3>
</div>
<div class="column is-12">
<@input.text id="ImageTemplateUpstreamUrl" label="Registry Url" />
</div>
<div class="column is-3-desktop is-12-tablet is-12-mobile">
<@input.dropdown label="Restart Policy" id="ImageTemplateRestartPolicy">
<option value="no">no</option>
<option value="always">always</option>
<option value="unless-stopped">unless-stopped</option>
<option value="on-failure">on-failure</option>
</@input.dropdown>
</div>
<div class="column is-3-desktop is-12-tablet is-12-mobile">
<@input.toggle id="ImageTemplateNetworkHost" label="Host Network" size="large" />
</div>
<div class="column is-3-desktop is-12-tablet is-12-mobile">
<@input.toggle id="ImageTemplatePrivileged" label="Privileged" size="large" />
</div>
<div class="column is-3-desktop is-12-tablet is-12-mobile">
<@input.dropdown id="ImageTemplateCapabilities" label="Capabilities" isMultiple=true>
<#list containerCapabilities as capability>
<option value="${capability}">${capability}</option>
</#list>
</@input.dropdown>
</div>
<div class="column is-12 has-margin-top">
<h3 class="title is-5">Ports</h3>
<@table.table id="ImageTemplatePorts" isScrollable=true isFullWidth=true>
<thead>
<tr>
<th>Port</th>
<th>Protocol</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</@table.table>
<@button.buttons isRightAligned=true>
<@button.button id="AddNewPort" colour="normal-colour" size="small">
<i class="fas fa-plus has-text-success"></i> Add
</@button.button>
</@button.buttons>
</div>
<div class="column is-12 has-margin-top">
<h3 class="title is-5">Volumes</h3>
<@table.table id="ImageTemplateVolumes" isScrollable=true isFullWidth=true>
<thead>
<tr>
<th>Volume</th>
<th>Read Only?</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</@table.table>
<@button.buttons isRightAligned=true>
<@button.button id="AddNewVolume" colour="normal-colour" size="small">
<i class="fas fa-plus has-text-success"></i> Add
</@button.button>
</@button.buttons>
</div>
<div class="column is-12 has-margin-top">
<h3 class="title is-5">Environment</h3>
<@table.table id="ImageTemplateEnv" isScrollable=true isFullWidth=true>
<thead>
<tr>
<th>Environment Variable</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</@table.table>
<@button.buttons isRightAligned=true>
<@button.button id="AddNewEnv" colour="normal-colour" size="small">
<i class="fas fa-plus has-text-success"></i> Add
</@button.button>
</@button.buttons>
</div>
<div class="column is-12 has-margin-top">
<h3 class="title is-5">Devices</h3>
<@table.table id="ImageTemplateDevices" isScrollable=true isFullWidth=true>
<thead>
<tr>
<th>Device</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</@table.table>
<@button.buttons isRightAligned=true>
<@button.button id="AddNewDevice" colour="normal-colour" size="small">
<i class="fas fa-plus has-text-success"></i> Add
</@button.button>
</@button.buttons>
</div>
</div>
</@container.container>
</@section.section>
<#else>
<@section.section id="ManageImages">
<@container.container>
Unable to find repository.
</@container.container>
</@section.section>
</#if>
</@base.base>

View File

@ -75,7 +75,7 @@
<@button.button id="ForceResync_${image.key.id}" size="small" title="Force resync" colour="normal-colour" extraAttributes='data-image-key="${image.key}"' extraClasses="sync-image">
<i class="fas fa-sync-alt is-marginless"></i>
</@button.button>
<@button.link size="small" title="Edit image metadata" colour="normal-colour" link="/admin/image?key=${image.fullName}">
<@button.link size="small" title="Edit image metadata" colour="normal-colour" link="/admin/image?imageKey=${image.key}">
<i class="fas fa-pencil-alt is-marginless"></i>
</@button.link>
</@button.buttons>

View File

@ -83,26 +83,34 @@
</div>
<div class="column is-6-desktop is-12-tablet">
<@box.box extraClasses="is-paddingless is-clipped is-relative">
<@box.box>
<h2 class="title is-5 has-text-centered has-margin-top">Pull Activity</h2>
<h2 class="title is-5 has-text-centered">Pull Activity</h2>
<div class="tabs is-toggle is-centered is-small is-marginless">
<ul class="is-marginless">
<li data-group-mode="Day">
<a><span>1d</span></a>
</li>
<li class="is-active" data-group-mode="Week">
<li data-group-mode="Week">
<a><span>1w</span></a>
</li>
<li data-group-mode="Month">
<li class="is-active" data-group-mode="Month">
<a><span>1m</span></a>
</li>
</ul>
</div>
<div class="chart-container" style="position: relative; width: 100%; height: 200px">
<canvas id="ImagePullHistory"></canvas>
<div class="columns has-margin-top">
<div class="column is-half-desktop is-full-mobile has-text-centered is-vcentered">
<h4 class="title is-6">Pulls per <span id="PullActivityDataPoint"></span></h4>
<@tag.tag value='<span id="PullActivityRate"></span>' colour="light" />
</div>
<div class="column is-half-desktop is-full-mobile">
<div class="chart-container" style="position: relative; width: 100%; height: 150px">
<canvas id="ImagePullHistory"></canvas>
</div>
</div>
</div>
</@box.box>

View File

@ -112,7 +112,7 @@
</#if>
<#if context=='image'>
chartManager.populateChart('${image.key}', 'Week');
chartManager.populateChart('${image.key}', 'Month');
</#if>
appManager.init();