mirror of https://github.com/portainer/portainer
Merge branch 'develop' into webpack
commit
dd571a792f
|
@ -3,6 +3,7 @@ package main // import "github.com/portainer/portainer"
|
|||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt"
|
||||
|
@ -136,6 +137,7 @@ func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter
|
|||
CronExpression: "@every " + *flags.SnapshotInterval,
|
||||
JobType: portainer.SnapshotJobType,
|
||||
SnapshotJob: snapshotJob,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
|
||||
snapshotJobContext := cron.NewSnapshotJobContext(endpointService, snapshotter)
|
||||
|
@ -173,6 +175,7 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
|
|||
CronExpression: "@every " + *flags.SyncInterval,
|
||||
JobType: portainer.EndpointSyncJobType,
|
||||
EndpointSyncJob: endpointSyncJob,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
|
||||
endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints)
|
||||
|
@ -194,12 +197,14 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p
|
|||
|
||||
for _, schedule := range schedules {
|
||||
|
||||
jobContext := cron.NewScriptExecutionJobContext(jobService, endpointService, fileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule.ScriptExecutionJob, jobContext)
|
||||
if schedule.JobType == portainer.ScriptExecutionJobType {
|
||||
jobContext := cron.NewScriptExecutionJobContext(jobService, endpointService, fileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule.ScriptExecutionJob, jobContext)
|
||||
|
||||
err = jobScheduler.CreateSchedule(&schedule, jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
err = jobScheduler.CreateSchedule(&schedule, jobRunner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
|
@ -322,16 +321,15 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
|
|||
return block.Bytes, nil
|
||||
}
|
||||
|
||||
// GetScheduleFolder returns the absolute path on the FS for a schedule based
|
||||
// GetScheduleFolder returns the absolute path on the filesystem for a schedule based
|
||||
// on its identifier.
|
||||
func (service *Service) GetScheduleFolder(scheduleIdentifier portainer.ScheduleID) string {
|
||||
return path.Join(service.fileStorePath, ScheduleStorePath, strconv.Itoa(int(scheduleIdentifier)))
|
||||
func (service *Service) GetScheduleFolder(identifier string) string {
|
||||
return path.Join(service.fileStorePath, ScheduleStorePath, identifier)
|
||||
}
|
||||
|
||||
// StoreScheduledJobFileFromBytes creates a subfolder in the ScheduleStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreScheduledJobFileFromBytes(scheduleIdentifier portainer.ScheduleID, data []byte) (string, error) {
|
||||
identifier := strconv.Itoa(int(scheduleIdentifier))
|
||||
func (service *Service) StoreScheduledJobFileFromBytes(identifier string, data []byte) (string, error) {
|
||||
scheduleStorePath := path.Join(ScheduleStorePath, identifier)
|
||||
err := service.createDirectoryInStore(scheduleStorePath)
|
||||
if err != nil {
|
||||
|
|
|
@ -35,6 +35,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/schedules/{id}",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete)
|
||||
|
||||
h.Handle("/schedules/{id}/file",
|
||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package schedules
|
|||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -31,32 +33,32 @@ type scheduleFromFileContentPayload struct {
|
|||
func (payload *scheduleFromFilePayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.New("Invalid name")
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
image, err := request.RetrieveMultiPartFormValue(r, "Image", false)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.New("Invalid image")
|
||||
}
|
||||
payload.Image = image
|
||||
|
||||
cronExpression, err := request.RetrieveMultiPartFormValue(r, "Schedule", false)
|
||||
cronExpression, err := request.RetrieveMultiPartFormValue(r, "CronExpression", false)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.New("Invalid cron expression")
|
||||
}
|
||||
payload.CronExpression = cronExpression
|
||||
|
||||
var endpoints []portainer.EndpointID
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.New("Invalid endpoints")
|
||||
}
|
||||
payload.Endpoints = endpoints
|
||||
|
||||
file, _, err := request.RetrieveMultiPartFormFile(r, "File")
|
||||
file, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid Script file. Ensure that the file is uploaded correctly")
|
||||
return portainer.Error("Invalid script file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.File = file
|
||||
|
||||
|
@ -137,7 +139,7 @@ func (handler *Handler) createScheduleFromFile(w http.ResponseWriter, r *http.Re
|
|||
func (handler *Handler) createSchedule(name, image, cronExpression string, endpoints []portainer.EndpointID, file []byte) (*portainer.Schedule, error) {
|
||||
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())
|
||||
|
||||
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(scheduleIdentifier, file)
|
||||
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(scheduleIdentifier)), file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -155,6 +157,7 @@ func (handler *Handler) createSchedule(name, image, cronExpression string, endpo
|
|||
CronExpression: cronExpression,
|
||||
JobType: portainer.ScriptExecutionJobType,
|
||||
ScriptExecutionJob: job,
|
||||
Created: time.Now().Unix(),
|
||||
}
|
||||
|
||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
||||
|
|
|
@ -3,6 +3,7 @@ package schedules
|
|||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
@ -27,7 +28,7 @@ func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) *
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Cannot remove system schedules", errors.New("Cannot remove system schedule")}
|
||||
}
|
||||
|
||||
scheduleFolder := handler.FileService.GetScheduleFolder(portainer.ScheduleID(scheduleID))
|
||||
scheduleFolder := handler.FileService.GetScheduleFolder(strconv.Itoa(scheduleID))
|
||||
err = handler.FileService.RemoveDirectory(scheduleFolder)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the schedule on the filesystem", err}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type scheduleFileResponse struct {
|
||||
ScheduleFileContent string `json:"ScheduleFileContent"`
|
||||
}
|
||||
|
||||
// GET request on /api/schedules/:id/file
|
||||
func (handler *Handler) scheduleFile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
scheduleID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid schedule identifier route variable", err}
|
||||
}
|
||||
|
||||
schedule, err := handler.ScheduleService.Schedule(portainer.ScheduleID(scheduleID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if schedule.JobType != portainer.ScriptExecutionJobType {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Unable to retrieve script file", errors.New("This type of schedule do not have any associated script file")}
|
||||
}
|
||||
|
||||
scheduleFileContent, err := handler.FileService.GetFileContent(schedule.ScriptExecutionJob.ScriptPath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve schedule script file from disk", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, &scheduleFileResponse{ScheduleFileContent: string(scheduleFileContent)})
|
||||
}
|
|
@ -2,6 +2,7 @@ package schedules
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
@ -15,6 +16,7 @@ type scheduleUpdatePayload struct {
|
|||
Image *string
|
||||
CronExpression *string
|
||||
Endpoints []portainer.EndpointID
|
||||
FileContent *string
|
||||
}
|
||||
|
||||
func (payload *scheduleUpdatePayload) Validate(r *http.Request) error {
|
||||
|
@ -41,8 +43,16 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
|
||||
updateJobSchedule := updateSchedule(schedule, &payload)
|
||||
if updateJobSchedule {
|
||||
|
||||
if payload.FileContent != nil {
|
||||
_, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(scheduleID), []byte(*payload.FileContent))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist script file changes on the filesystem", err}
|
||||
}
|
||||
updateJobSchedule = true
|
||||
}
|
||||
|
||||
if updateJobSchedule {
|
||||
jobContext := cron.NewScriptExecutionJobContext(handler.JobService, handler.EndpointService, handler.FileService)
|
||||
jobRunner := cron.NewScriptExecutionJobRunner(schedule.ScriptExecutionJob, jobContext)
|
||||
err := handler.JobScheduler.UpdateSchedule(schedule, jobRunner)
|
||||
|
|
|
@ -251,6 +251,7 @@ type (
|
|||
ID ScheduleID `json:"Id"`
|
||||
Name string
|
||||
CronExpression string
|
||||
Created int64
|
||||
JobType JobType
|
||||
ScriptExecutionJob *ScriptExecutionJob
|
||||
SnapshotJob *SnapshotJob
|
||||
|
@ -653,8 +654,8 @@ type (
|
|||
LoadKeyPair() ([]byte, []byte, error)
|
||||
WriteJSONToFile(path string, content interface{}) error
|
||||
FileExists(path string) (bool, error)
|
||||
StoreScheduledJobFileFromBytes(scheduleIdentifier ScheduleID, data []byte) (string, error)
|
||||
GetScheduleFolder(scheduleIdentifier ScheduleID) string
|
||||
StoreScheduledJobFileFromBytes(identifier string, data []byte) (string, error)
|
||||
GetScheduleFolder(identifier string) string
|
||||
}
|
||||
|
||||
// GitService represents a service for managing Git
|
||||
|
|
|
@ -6,6 +6,7 @@ angular.module('portainer')
|
|||
.constant('API_ENDPOINT_MOTD', 'api/motd')
|
||||
.constant('API_ENDPOINT_REGISTRIES', 'api/registries')
|
||||
.constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls')
|
||||
.constant('API_ENDPOINT_SCHEDULES', 'api/schedules')
|
||||
.constant('API_ENDPOINT_SETTINGS', 'api/settings')
|
||||
.constant('API_ENDPOINT_STACKS', 'api/stacks')
|
||||
.constant('API_ENDPOINT_STATUS', 'api/status')
|
||||
|
|
|
@ -109,7 +109,7 @@ angular.module('portainer.docker')
|
|||
return $q.when();
|
||||
}
|
||||
ContainerService.prune({ label: ['io.portainer.job.endpoint'] }).then(function success() {
|
||||
Notifications.success('Success', 'Job hisotry cleared');
|
||||
Notifications.success('Success', 'Job history cleared');
|
||||
$state.reload();
|
||||
}).catch(function error(err) {
|
||||
Notifications.error('Failure', err.message, 'Unable to clear job history');
|
||||
|
|
|
@ -6,7 +6,7 @@ angular.module('portainer.docker')
|
|||
var helper = {};
|
||||
|
||||
helper.commandStringToArray = function(command) {
|
||||
return splitargs(command);
|
||||
return splitargs(command, undefined, true);
|
||||
};
|
||||
|
||||
helper.commandArrayToString = function(array) {
|
||||
|
|
|
@ -66,9 +66,17 @@ export function ContainerViewModel(data) {
|
|||
}
|
||||
|
||||
export function ContainerStatsViewModel(data) {
|
||||
this.Date = data.read;
|
||||
this.MemoryUsage = data.memory_stats.usage - data.memory_stats.stats.cache;
|
||||
this.MemoryCache = data.memory_stats.stats.cache;
|
||||
this.read = data.read;
|
||||
this.preread = data.preread;
|
||||
if(data.memory_stats.privateworkingset !== undefined) { // Windows
|
||||
this.MemoryUsage = data.memory_stats.privateworkingset;
|
||||
this.MemoryCache = 0;
|
||||
this.NumProcs = data.num_procs;
|
||||
this.isWindows = true;
|
||||
} else { // Linux
|
||||
this.MemoryUsage = data.memory_stats.usage - data.memory_stats.stats.cache;
|
||||
this.MemoryCache = data.memory_stats.stats.cache;
|
||||
}
|
||||
this.PreviousCPUTotalUsage = data.precpu_stats.cpu_usage.total_usage;
|
||||
this.PreviousCPUSystemUsage = data.precpu_stats.system_cpu_usage;
|
||||
this.CurrentCPUTotalUsage = data.cpu_stats.cpu_usage.total_usage;
|
||||
|
|
|
@ -25,21 +25,21 @@ function ($q, $scope, $transition$, $document, $interval, ContainerService, Char
|
|||
if (stats.Networks.length > 0) {
|
||||
var rx = stats.Networks[0].rx_bytes;
|
||||
var tx = stats.Networks[0].tx_bytes;
|
||||
var label = moment(stats.Date).format('HH:mm:ss');
|
||||
var label = moment(stats.read).format('HH:mm:ss');
|
||||
|
||||
ChartService.UpdateNetworkChart(label, rx, tx, chart);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMemoryChart(stats, chart) {
|
||||
var label = moment(stats.Date).format('HH:mm:ss');
|
||||
var label = moment(stats.read).format('HH:mm:ss');
|
||||
|
||||
ChartService.UpdateMemoryChart(label, stats.MemoryUsage, stats.MemoryCache, chart);
|
||||
}
|
||||
|
||||
function updateCPUChart(stats, chart) {
|
||||
var label = moment(stats.Date).format('HH:mm:ss');
|
||||
var value = calculateCPUPercentUnix(stats);
|
||||
var label = moment(stats.read).format('HH:mm:ss');
|
||||
var value = stats.isWindows ? calculateCPUPercentWindows(stats) : calculateCPUPercentUnix(stats);
|
||||
|
||||
ChartService.UpdateCPUChart(label, value, chart);
|
||||
}
|
||||
|
@ -56,6 +56,17 @@ function ($q, $scope, $transition$, $document, $interval, ContainerService, Char
|
|||
return cpuPercent;
|
||||
}
|
||||
|
||||
function calculateCPUPercentWindows(stats) {
|
||||
var possIntervals = stats.NumProcs * parseFloat(
|
||||
moment(stats.read, 'YYYY-MM-DDTHH:mm:ss.SSSSSSSSSZ').valueOf() - moment(stats.preread, 'YYYY-MM-DDTHH:mm:ss.SSSSSSSSSZ').valueOf());
|
||||
var windowsCpuUsage = 0.0;
|
||||
if(possIntervals > 0) {
|
||||
windowsCpuUsage = parseFloat(stats.CurrentCPUTotalUsage - stats.PreviousCPUTotalUsage) / parseFloat(possIntervals * 100);
|
||||
}
|
||||
return windowsCpuUsage;
|
||||
}
|
||||
|
||||
|
||||
$scope.changeUpdateRepeater = function() {
|
||||
var networkChart = $scope.networkChart;
|
||||
var cpuChart = $scope.cpuChart;
|
||||
|
|
|
@ -242,6 +242,39 @@ angular.module('portainer.app', [])
|
|||
}
|
||||
};
|
||||
|
||||
var schedules = {
|
||||
name: 'portainer.schedules',
|
||||
url: '/schedules',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/schedules/schedules.html',
|
||||
controller: 'SchedulesController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var schedule = {
|
||||
name: 'portainer.schedules.schedule',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/schedules/edit/schedule.html',
|
||||
controller: 'ScheduleController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var scheduleCreation = {
|
||||
name: 'portainer.schedules.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/schedules/create/createschedule.html',
|
||||
controller: 'CreateScheduleController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var settings = {
|
||||
name: 'portainer.settings',
|
||||
url: '/settings',
|
||||
|
@ -428,6 +461,9 @@ angular.module('portainer.app', [])
|
|||
$stateRegistryProvider.register(registry);
|
||||
$stateRegistryProvider.register(registryAccess);
|
||||
$stateRegistryProvider.register(registryCreation);
|
||||
$stateRegistryProvider.register(schedules);
|
||||
$stateRegistryProvider.register(schedule);
|
||||
$stateRegistryProvider.register(scheduleCreation);
|
||||
$stateRegistryProvider.register(settings);
|
||||
$stateRegistryProvider.register(settingsAuthentication);
|
||||
$stateRegistryProvider.register(stacks);
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.schedules.new">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add schedule
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('CronExpression')">
|
||||
Cron expression
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CronExpression' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CronExpression' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Created')">
|
||||
Created
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Created' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)" ng-disabled="item.JobType !== 1"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<span ng-if="item.JobType !== 1">{{ item.Name }}</span>
|
||||
<a ng-if="item.JobType === 1" ui-sref="portainer.schedules.schedule({id: item.Id})">{{ item.Name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.CronExpression }}
|
||||
</td>
|
||||
<td>{{ item.Created | getisodatefromtimestamp }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="3" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="3" class="text-center text-muted">No schedule available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||
</div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</span>
|
||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.app').component('schedulesDatatable', {
|
||||
templateUrl: './schedulesDatatable.html',
|
||||
controller: 'SchedulesDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('SchedulesDatatableController', ['PaginationService', 'DatatableService',
|
||||
function (PaginationService, DatatableService) {
|
||||
|
||||
this.state = {
|
||||
selectAll: false,
|
||||
orderBy: this.orderBy,
|
||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
||||
displayTextFilter: false,
|
||||
selectedItemCount: 0,
|
||||
selectedItems: []
|
||||
};
|
||||
|
||||
this.changeOrderBy = function(orderField) {
|
||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||
this.state.orderBy = orderField;
|
||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
||||
};
|
||||
|
||||
this.selectItem = function(item) {
|
||||
if (item.Checked) {
|
||||
this.state.selectedItemCount++;
|
||||
this.state.selectedItems.push(item);
|
||||
} else {
|
||||
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
|
||||
this.state.selectedItemCount--;
|
||||
}
|
||||
};
|
||||
|
||||
this.selectAll = function() {
|
||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
||||
var item = this.state.filteredDataSet[i];
|
||||
if (item.JobType ===1 && item.Checked !== this.state.selectAll) {
|
||||
item.Checked = this.state.selectAll;
|
||||
this.selectItem(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.changePaginationLimit = function() {
|
||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
||||
};
|
||||
|
||||
this.$onInit = function() {
|
||||
setDefaults(this);
|
||||
|
||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||
if (storedOrder !== null) {
|
||||
this.state.reverseOrder = storedOrder.reverse;
|
||||
this.state.orderBy = storedOrder.orderBy;
|
||||
}
|
||||
};
|
||||
|
||||
function setDefaults(ctrl) {
|
||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
||||
}
|
||||
}]);
|
|
@ -0,0 +1,35 @@
|
|||
angular.module('portainer.app').component('scheduleForm', {
|
||||
templateUrl: './scheduleForm.html',
|
||||
controller: function() {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.state = {
|
||||
formValidationError: ''
|
||||
};
|
||||
|
||||
this.action = function() {
|
||||
ctrl.state.formValidationError = '';
|
||||
|
||||
if (ctrl.model.Job.Method === 'editor' && ctrl.model.Job.FileContent === '') {
|
||||
ctrl.state.formValidationError = 'Script file content must not be empty';
|
||||
return;
|
||||
}
|
||||
|
||||
ctrl.formAction();
|
||||
};
|
||||
|
||||
this.editorUpdate = function(cm) {
|
||||
ctrl.model.Job.FileContent = cm.getValue();
|
||||
};
|
||||
},
|
||||
bindings: {
|
||||
model: '=',
|
||||
endpoints: '<',
|
||||
groups: '<',
|
||||
addLabelAction: '<',
|
||||
removeLabelAction: '<',
|
||||
formAction: '<',
|
||||
formActionLabel: '@',
|
||||
actionInProgress: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,165 @@
|
|||
<form class="form-horizontal" name="scheduleForm">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Schedule configuration
|
||||
</div>
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="schedule_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.model.Name" id="schedule_name" name="schedule_name" placeholder="backup-app-prod" required auto-focus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="scheduleForm.schedule_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="scheduleForm.schedule_name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- cron-input -->
|
||||
<div class="form-group">
|
||||
<label for="schedule_cron" class="col-sm-1 control-label text-left">Cron rule</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.model.CronExpression" id="schedule_cron" name="schedule_cron" placeholder="0 2 * * *" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="scheduleForm.schedule_cron.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="scheduleForm.schedule_cron.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
You can refer to the <a href="https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format" target="_blank">following documentation</a> to get more information about the supported cron expression format.
|
||||
</span>
|
||||
</div>
|
||||
<!-- !cron-input -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Job configuration
|
||||
</div>
|
||||
<!-- image-input -->
|
||||
<div class="form-group">
|
||||
<label for="schedule_image" class="col-sm-1 control-label text-left">Image</label>
|
||||
<div class="col-sm-11">
|
||||
<input type="text" class="form-control" ng-model="$ctrl.model.Job.Image" id="schedule_image" name="schedule_image" placeholder="e.g. ubuntu:latest" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="scheduleForm.schedule_image.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="scheduleForm.schedule_image.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !image-input -->
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the
|
||||
<code>/host</code> folder.
|
||||
</span>
|
||||
</div>
|
||||
<!-- execution-method -->
|
||||
<div ng-if="!$ctrl.model.Id">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Job content
|
||||
</div>
|
||||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<input type="radio" id="method_editor" ng-model="$ctrl.model.Job.Method" value="editor">
|
||||
<label for="method_editor">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Web editor
|
||||
</div>
|
||||
<p>Use our Web editor</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="method_upload" ng-model="$ctrl.model.Job.Method" value="upload">
|
||||
<label for="method_upload">
|
||||
<div class="boxselector_header">
|
||||
<i class="fa fa-upload" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
Upload
|
||||
</div>
|
||||
<p>Upload from your computer</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !execution-method -->
|
||||
<!-- web-editor -->
|
||||
<div ng-show="$ctrl.model.Job.Method === 'editor'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Web editor
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<code-editor
|
||||
identifier="execute-schedule-editor"
|
||||
placeholder="# Define or paste the content of your script file here"
|
||||
on-change="$ctrl.editorUpdate"
|
||||
value="$ctrl.model.Job.FileContent"
|
||||
></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !web-editor -->
|
||||
<!-- upload -->
|
||||
<div ng-show="$ctrl.model.Job.Method === 'upload'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Upload
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small">
|
||||
You can upload a script file from your computer.
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.model.Job.File">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ $ctrl.model.Job.File.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!$ctrl.model.Job.File" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !upload -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Target endpoints
|
||||
</div>
|
||||
<!-- node-selection -->
|
||||
<multi-endpoint-selector
|
||||
ng-if="$ctrl.endpoints && $ctrl.groups"
|
||||
model="$ctrl.model.Job.Endpoints"
|
||||
endpoints="$ctrl.endpoints" groups="$ctrl.groups"
|
||||
></multi-endpoint-selector>
|
||||
<!-- !node-selection -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
ng-disabled="$ctrl.actionInProgress || !scheduleForm.$valid
|
||||
|| $ctrl.model.Job.Endpoints.length === 0
|
||||
|| ($ctrl.model.Job.Method === 'upload' && !$ctrl.model.Job.File)"
|
||||
ng-click="$ctrl.action()" button-spinner="$ctrl.actionInProgress"
|
||||
>
|
||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||
</button>
|
||||
<span class="text-danger" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px;">
|
||||
{{ $ctrl.state.formValidationError }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
|
@ -0,0 +1,9 @@
|
|||
angular.module('portainer.app').component('multiEndpointSelector', {
|
||||
templateUrl: './multiEndpointSelector.html',
|
||||
controller: 'MultiEndpointSelectorController',
|
||||
bindings: {
|
||||
'model': '=',
|
||||
'endpoints': '<',
|
||||
'groups': '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
<ui-select multiple ng-model="$ctrl.model" close-on-select="false" style="margin-top: 55px">
|
||||
<ui-select-match placeholder="Select one or multiple endpoint(s)">
|
||||
<span>
|
||||
{{ $item.Name }}
|
||||
<span ng-if="$item.Tags.length">({{ $item.Tags | arraytostr }})</span>
|
||||
</span>
|
||||
</ui-select-match>
|
||||
<ui-select-choices group-by="$ctrl.groupEndpoints" group-filter="$ctrl.sortGroups" repeat="endpoint.Id as endpoint in $ctrl.endpoints | filter: { Name: $select.search }">
|
||||
<span>
|
||||
{{ endpoint.Name }}
|
||||
<span ng-if="endpoint.Tags.length">({{ endpoint.Tags | arraytostr }})</span>
|
||||
</span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
|
@ -0,0 +1,35 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('MultiEndpointSelectorController', function () {
|
||||
var ctrl = this;
|
||||
|
||||
this.sortGroups = function(groups) {
|
||||
return _.sortBy(groups, ['name']);
|
||||
};
|
||||
|
||||
this.groupEndpoints = function(endpoint) {
|
||||
for (var i = 0; i < ctrl.availableGroups.length; i++) {
|
||||
var group = ctrl.availableGroups[i];
|
||||
|
||||
if (endpoint.GroupId === group.Id) {
|
||||
return group.Name;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.$onInit = function() {
|
||||
this.availableGroups = filterEmptyGroups(this.groups, this.endpoints);
|
||||
};
|
||||
|
||||
function filterEmptyGroups(groups, endpoints) {
|
||||
return groups.filter(function f(group) {
|
||||
for (var i = 0; i < endpoints.length; i++) {
|
||||
|
||||
var endpoint = endpoints[i];
|
||||
if (endpoint.GroupId === group.Id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
export function ScheduleDefaultModel() {
|
||||
this.Name = '';
|
||||
this.CronExpression = '';
|
||||
this.JobType = 1;
|
||||
this.Job = new ScriptExecutionDefaultJobModel();
|
||||
}
|
||||
|
||||
function ScriptExecutionDefaultJobModel() {
|
||||
this.Image = '';
|
||||
this.Endpoints = [];
|
||||
this.FileContent = '';
|
||||
this.File = null;
|
||||
this.Method = 'editor';
|
||||
}
|
||||
|
||||
export function ScheduleModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Name = data.Name;
|
||||
this.JobType = data.JobType;
|
||||
this.CronExpression = data.CronExpression;
|
||||
this.Created = data.Created;
|
||||
if (this.JobType === 1) {
|
||||
this.Job = new ScriptExecutionJobModel(data.ScriptExecutionJob);
|
||||
}
|
||||
}
|
||||
|
||||
function ScriptExecutionJobModel(data) {
|
||||
this.Image = data.Image;
|
||||
this.Endpoints = data.Endpoints;
|
||||
this.FileContent = '';
|
||||
this.Method = 'editor';
|
||||
}
|
||||
|
||||
export function ScheduleCreateRequest(model) {
|
||||
this.Name = model.Name;
|
||||
this.CronExpression = model.CronExpression;
|
||||
this.Image = model.Job.Image;
|
||||
this.Endpoints = model.Job.Endpoints;
|
||||
this.FileContent = model.Job.FileContent;
|
||||
this.File = model.Job.File;
|
||||
}
|
||||
|
||||
export function ScheduleUpdateRequest(model) {
|
||||
this.id = model.Id;
|
||||
this.Name = model.Name;
|
||||
this.CronExpression = model.CronExpression;
|
||||
this.Image = model.Job.Image;
|
||||
this.Endpoints = model.Job.Endpoints;
|
||||
this.FileContent = model.Job.FileContent;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
angular.module('portainer.app')
|
||||
.factory('Schedules', ['$resource', 'API_ENDPOINT_SCHEDULES',
|
||||
function SchedulesFactory($resource, API_ENDPOINT_SCHEDULES) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_SCHEDULES + '/:id/:action', {}, {
|
||||
create: { method: 'POST' },
|
||||
query: { method: 'GET', isArray: true },
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
update: { method: 'PUT', params: { id: '@id' } },
|
||||
remove: { method: 'DELETE', params: { id: '@id'} },
|
||||
file: { method: 'GET', params: { id : '@id', action: 'file' } }
|
||||
});
|
||||
}]);
|
|
@ -0,0 +1,65 @@
|
|||
import { ScheduleModel, ScheduleCreateRequest, ScheduleUpdateRequest } from '../../models/schedule';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.factory('ScheduleService', ['$q', 'Schedules', 'FileUploadService',
|
||||
function ScheduleService($q, Schedules, FileUploadService) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.schedule = function(scheduleId) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Schedules.get({ id: scheduleId }).$promise
|
||||
.then(function success(data) {
|
||||
var schedule = new ScheduleModel(data);
|
||||
deferred.resolve(schedule);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve schedule', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.schedules = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
Schedules.query().$promise
|
||||
.then(function success(data) {
|
||||
var schedules = data.map(function (item) {
|
||||
return new ScheduleModel(item);
|
||||
});
|
||||
deferred.resolve(schedules);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve schedules', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.createScheduleFromFileContent = function(model) {
|
||||
var payload = new ScheduleCreateRequest(model);
|
||||
return Schedules.create({ method: 'string' }, payload).$promise;
|
||||
};
|
||||
|
||||
service.createScheduleFromFileUpload = function(model) {
|
||||
var payload = new ScheduleCreateRequest(model);
|
||||
return FileUploadService.createSchedule(payload);
|
||||
};
|
||||
|
||||
service.updateSchedule = function(model) {
|
||||
var payload = new ScheduleUpdateRequest(model);
|
||||
return Schedules.update(payload).$promise;
|
||||
};
|
||||
|
||||
service.deleteSchedule = function(scheduleId) {
|
||||
return Schedules.remove({ id: scheduleId }).$promise;
|
||||
};
|
||||
|
||||
service.getScriptFile = function(scheduleId) {
|
||||
return Schedules.file({ id: scheduleId }).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -138,6 +138,14 @@ angular.module('portainer.app')
|
|||
});
|
||||
};
|
||||
|
||||
function LimitChartItems(chart, CHART_LIMIT) {
|
||||
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||
chart.data.labels.pop();
|
||||
chart.data.datasets[0].data.pop();
|
||||
chart.data.datasets[1].data.pop();
|
||||
}
|
||||
}
|
||||
|
||||
function UpdateChart(label, value, chart) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(value);
|
||||
|
@ -153,13 +161,15 @@ angular.module('portainer.app')
|
|||
service.UpdateMemoryChart = function UpdateChart(label, memoryValue, cacheValue, chart) {
|
||||
chart.data.labels.push(label);
|
||||
chart.data.datasets[0].data.push(memoryValue);
|
||||
chart.data.datasets[1].data.push(cacheValue);
|
||||
|
||||
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||
chart.data.labels.pop();
|
||||
chart.data.datasets[0].data.pop();
|
||||
|
||||
if(cacheValue) {
|
||||
chart.data.datasets[1].data.push(cacheValue);
|
||||
} else { // cache values are not available for Windows
|
||||
chart.data.datasets.splice(1, 1);
|
||||
}
|
||||
|
||||
LimitChartItems(chart);
|
||||
|
||||
chart.update(0);
|
||||
};
|
||||
service.UpdateCPUChart = UpdateChart;
|
||||
|
@ -169,11 +179,7 @@ angular.module('portainer.app')
|
|||
chart.data.datasets[0].data.push(rx);
|
||||
chart.data.datasets[1].data.push(tx);
|
||||
|
||||
if (chart.data.datasets[0].data.length > CHART_LIMIT) {
|
||||
chart.data.labels.pop();
|
||||
chart.data.datasets[0].data.pop();
|
||||
chart.data.datasets[1].data.pop();
|
||||
}
|
||||
LimitChartItems(chart);
|
||||
|
||||
chart.update(0);
|
||||
};
|
||||
|
|
|
@ -41,6 +41,19 @@ angular.module('portainer.app')
|
|||
});
|
||||
};
|
||||
|
||||
service.createSchedule = function(payload) {
|
||||
return Upload.upload({
|
||||
url: 'api/schedules?method=file',
|
||||
data: {
|
||||
file: payload.File,
|
||||
Name: payload.Name,
|
||||
CronExpression: payload.CronExpression,
|
||||
Image: payload.Image,
|
||||
Endpoints: Upload.json(payload.Endpoints)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
service.createSwarmStack = function(stackName, swarmId, file, env, endpointId) {
|
||||
return Upload.upload({
|
||||
url: 'api/stacks?method=file&type=1&endpointId=' + endpointId,
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { ScheduleDefaultModel } from '../../../models/schedule';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.controller('CreateScheduleController', ['$q', '$scope', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService',
|
||||
function ($q, $scope, $state, Notifications, EndpointService, GroupService, ScheduleService) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.create = create;
|
||||
|
||||
function create() {
|
||||
var model = $scope.model;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
createSchedule(model)
|
||||
.then(function success() {
|
||||
Notifications.success('Schedule successfully created');
|
||||
$state.go('portainer.schedules', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create schedule');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
function createSchedule(model) {
|
||||
if (model.Job.Method === 'editor') {
|
||||
return ScheduleService.createScheduleFromFileContent(model);
|
||||
}
|
||||
return ScheduleService.createScheduleFromFileUpload(model);
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$scope.model = new ScheduleDefaultModel();
|
||||
|
||||
$q.all({
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.endpoints = data.endpoints;
|
||||
$scope.groups = data.groups;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint list');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -0,0 +1,23 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Create schedule"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.schedules">Schedules</a> > Add schedule
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<schedule-form
|
||||
model="model"
|
||||
endpoints="endpoints"
|
||||
groups="groups"
|
||||
form-action="create"
|
||||
form-action-label="Create schedule"
|
||||
action-in-progress="state.actionInProgress"
|
||||
></schedule-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,28 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Schedule details">
|
||||
<a data-toggle="tooltip" title-text="Refresh" ui-sref="portainer.schedules.schedule({id: schedule.Id})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.schedules">Schedules</a> > {{ ::schedule.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<schedule-form
|
||||
ng-if="schedule"
|
||||
model="schedule"
|
||||
endpoints="endpoints"
|
||||
groups="groups"
|
||||
form-action="update"
|
||||
form-action-label="Update schedule"
|
||||
action-in-progress="state.actionInProgress"
|
||||
></schedule-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,54 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('ScheduleController', ['$q', '$scope', '$transition$', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService',
|
||||
function ($q, $scope, $transition$, $state, Notifications, EndpointService, GroupService, ScheduleService) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.update = update;
|
||||
|
||||
function update() {
|
||||
var model = $scope.schedule;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
ScheduleService.updateSchedule(model)
|
||||
.then(function success() {
|
||||
Notifications.success('Schedule successfully updated');
|
||||
$state.go('portainer.schedules', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update schedule');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
var id = $transition$.params().id;
|
||||
var schedule = null;
|
||||
|
||||
$q.all({
|
||||
schedule: ScheduleService.schedule(id),
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
schedule = data.schedule;
|
||||
$scope.endpoints = data.endpoints;
|
||||
$scope.groups = data.groups;
|
||||
|
||||
return ScheduleService.getScriptFile(schedule.Id);
|
||||
})
|
||||
.then(function success(data) {
|
||||
schedule.Job.FileContent = data.ScheduleFileContent;
|
||||
$scope.schedule = schedule;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint list');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -0,0 +1,19 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Schedules list">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.schedules" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Schedules</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<schedules-datatable
|
||||
title-text="Schedules" title-icon="fa-clock"
|
||||
dataset="schedules" table-key="schedules"
|
||||
order-by="Name"
|
||||
remove-action="removeAction"
|
||||
></schedules-datatable>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,50 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('SchedulesController', ['$scope', '$state', 'Notifications', 'ModalService', 'ScheduleService',
|
||||
function ($scope, $state, Notifications, ModalService, ScheduleService) {
|
||||
|
||||
$scope.removeAction = removeAction;
|
||||
|
||||
function removeAction(selectedItems) {
|
||||
ModalService.confirmDeletion(
|
||||
'Do you want to remove the selected schedule(s) ?',
|
||||
function onConfirm(confirmed) {
|
||||
if(!confirmed) { return; }
|
||||
deleteSelectedSchedules(selectedItems);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function deleteSelectedSchedules(schedules) {
|
||||
var actionCount = schedules.length;
|
||||
angular.forEach(schedules, function (schedule) {
|
||||
ScheduleService.deleteSchedule(schedule.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Schedule successfully removed', schedule.Name);
|
||||
var index = $scope.schedules.indexOf(schedule);
|
||||
$scope.schedules.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove schedule ' + schedule.Name);
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
ScheduleService.schedules()
|
||||
.then(function success(data) {
|
||||
$scope.schedules = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve schedules');
|
||||
$scope.schedules = [];
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -37,6 +37,12 @@
|
|||
<a ui-sref="storidge.profiles" ui-sref-active="active">Profiles</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin">
|
||||
<span>Scheduler</span>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||
<a ui-sref="portainer.schedules" ui-sref-active="active">Host jobs <span class="menu-icon fa fa-clock fa-fw"></span></a>
|
||||
</li>
|
||||
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
|
||||
<span>Settings</span>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue