1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-08 15:25:22 +02:00

feat(edge-compute): move host jobs to edge (#3840)

* feat(endpoints): create an associated endpoints selector

* feat(schedules): remove edge specific explanations

* refactor(schedule): replace multi-endpoint-selector

* refactor(schedule): move controller to single file

* refactor(endpoints): remove multi-endpoint-selector

* feat(edge): rename host jobs to edge jobs

* feat(edge-jobs): remove edge warning

* refactor(edge-jobs): move schedule pages to edge

* refactor(edge-jobs): mv views to edgeJobs

* refactor(edge-jobs): rename edge jobs

* refactor(edge-jobs): move services to edge

* refactor(edge-jobs): move tasks datatable

* fix(edge-jobs): fix import

* fix(edge-jobs): use right services

* feat(settings): adjust host management description

* feat(edge-jobs): introduce interfaces and types

* feat(edge-jobs): implement bolt service

* refactor(edge-jobs): replace schedule routes

* refactor(edge-job): replace Schedule service

* refactor(edge-jobs): remove job_script_exec

* refactor(host): remove jobs table

* feat(edge-jobs): replace schedule

* feat(edge-jobs): load file on inspect

* fix(edge-job): parse cron correctly

* feat(edge-jobs): show tasks

* feat(host): rename tooltip

* refactor(host): remove old components

* refactor(main): remove schedule types

* refactor(snapshot): replace job service with snapshot service

* refactor(jobs): remove jobs form and datatable

* feat(edge-jobs): create db migration

* fix(main): start snapshot service with correct interval

* feat(settings): change host tooltip

* feat(edge-jobs): load endpoints

* fix(edge-job): disable form submit when form is invalid

* refactor(edge-compute): use const

* refactor(edge-jobs): use generic controller

* refactor(edge-jobs): replace $scope with controllerAs

* refactor(edge-jobs): replace routes with components

* refactor(edge-jobs): replace functions with classes

* refactor(edge-jobs): use async/await

* refactor(edge-jobs): rename functions

* feat(edge-jobs): introduce beta panel

* feat(edge-jobs): allow single character names

* fix(snapshot): run snapshot in coroutine

* feat(edge-jobs): add logs status

* feat(filesystem): add edge job logs methods

* feat(edge-jobs): intoduce edge jobs tasks api

* feat(edge-jobs): remove schedule task model

* fix(fs): build edge job task file path

* fix(edge-jobs): update task meta

* fix(edge-jobs): return a list of endpoints

* feat(edge-jobs): update logs from agent

* feat(edge-jobs): collect logs

* feat(edge-jobs): rename url

* feat(edge-jobs): refresh to same tab

* feat(edge-jobs): remove old info

* refactor(edge-jobs): rename script path json

* fix(edge-job): save file before adding job

* feat(edge-job): show retrieving logs label

* feat(edge-job): replace cron with 5 places

* refactor(edge-jobs): replace tasks with results

* feat(edge-jobs): add auto refresh until logs are collected

* feat(edge-jobs): fix column size

* feat(edge-job): display editor

* feat(edge-job): add name validation

* feat(edge-job): set default time for 1 hour from now

* feat(edge-job): add validation for cron format

* feat(edge-job): add a note about timezone

* fix(edge-job): replace regex

* fix(edge-job): check for every minute cron

* style(edge-jobs): add reference for cron regex

* refactor(edge-jobs): rename migration name

* refactor(edge-job): rename edge job response

* refactor(snapshot): rename snapshot endpoint method

* refactor(edge-jobs): move tasks handler to edgejobs

* feat(security): introduce a middleware for edge compute operations

* feat(edge-job): use edge compute middleware

* feat(edge-groups): filter http actions based on edge setting

* fix(security): return from edge bouncer if failed

* feat(edge-stacks): filter http actions based on edge setting

* feat(edge-groups): show error when failed to load groups

* refactor(db): remove edge-jobs migration

* refactor(migrator): remove unused dependency

Co-authored-by: Anthony Lapenna <lapenna.anthony@gmail.com>
This commit is contained in:
Chaim Lev-Ari 2020-06-25 06:25:51 +03:00 committed by GitHub
parent b6f5d8f90e
commit 24528ecea8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
120 changed files with 2624 additions and 3484 deletions

View file

@ -316,39 +316,6 @@ angular.module('portainer.app', []).config([
},
};
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',
@ -542,9 +509,6 @@ angular.module('portainer.app', []).config([
$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);

View file

@ -0,0 +1,50 @@
<div class="col-sm-12 small text-muted">
You can select which endpoint should be part of this group by moving them to the associated endpoints table. Simply click on any endpoint entry to move it from one table to the
other.
</div>
<div class="col-sm-12" style="margin-top: 20px;">
<!-- available-endpoints -->
<div class="col-sm-6">
<div class="text-center small text-muted">Available endpoints</div>
<div style="margin-top: 10px;">
<group-association-table
loaded="$ctrl.loaded"
page-type="$ctrl.pageType"
table-type="available"
retrieve-page="$ctrl.getPaginatedEndpoints"
dataset="$ctrl.endpoints.available"
entry-click="$ctrl.associateEndpoint"
pagination-state="$ctrl.state.available"
empty-dataset-message="No endpoint available"
tags="$ctrl.tags"
show-tags="true"
groups="$ctrl.groups"
show-groups="true"
has-backend-pagination="true"
></group-association-table>
</div>
</div>
<!-- !available-endpoints -->
<!-- associated-endpoints -->
<div class="col-sm-6">
<div class="text-center small text-muted">Associated endpoints</div>
<div style="margin-top: 10px;">
<group-association-table
loaded="$ctrl.loaded"
page-type="$ctrl.pageType"
table-type="associated"
retrieve-page="$ctrl.getPaginatedEndpoints"
dataset="$ctrl.endpoints.associated"
entry-click="$ctrl.dissociateEndpoint"
pagination-state="$ctrl.state.associated"
empty-dataset-message="No associated endpoint"
tags="$ctrl.tags"
show-tags="true"
groups="$ctrl.groups"
show-groups="true"
has-backend-pagination="true"
></group-association-table>
</div>
</div>
<!-- !associated-endpoints -->
</div>

View file

@ -0,0 +1,16 @@
import angular from 'angular';
import AssociatedEndpointsSelectorController from './associatedEndpointsSelectorController';
angular.module('portainer.app').component('associatedEndpointsSelector', {
templateUrl: './associatedEndpointsSelector.html',
controller: AssociatedEndpointsSelectorController,
bindings: {
endpointIds: '<',
tags: '<',
groups: '<',
hasBackendPagination: '<',
onAssociate: '<',
onDissociate: '<',
},
});

View file

@ -0,0 +1,106 @@
import angular from 'angular';
import _ from 'lodash-es';
class AssoicatedEndpointsSelectorController {
/* @ngInject */
constructor($async, EndpointService) {
this.$async = $async;
this.EndpointService = EndpointService;
this.state = {
available: {
limit: '10',
filter: '',
pageNumber: 1,
totalCount: 0,
},
associated: {
limit: '10',
filter: '',
pageNumber: 1,
totalCount: 0,
},
};
this.endpoints = {
associated: [],
available: null,
};
this.getEndpoints = this.getEndpoints.bind(this);
this.getEndpointsAsync = this.getEndpointsAsync.bind(this);
this.getAssociatedEndpoints = this.getAssociatedEndpoints.bind(this);
this.getAssociatedEndpointsAsync = this.getAssociatedEndpointsAsync.bind(this);
this.associateEndpoint = this.associateEndpoint.bind(this);
this.dissociateEndpoint = this.dissociateEndpoint.bind(this);
}
$onInit() {
this.loadData();
}
$onChanges({ endpointIds }) {
if (endpointIds && endpointIds.currentValue) {
this.loadData();
}
}
loadData() {
this.getAssociatedEndpoints();
this.getEndpoints();
}
getEndpoints() {
return this.$async(this.getEndpointsAsync);
}
async getEndpointsAsync() {
const { start, search, limit } = this.getPaginationData('available');
const query = { search, type: 4 };
const response = await this.EndpointService.endpoints(start, limit, query);
const endpoints = _.filter(response.value, (endpoint) => !_.includes(this.endpointIds, endpoint.Id));
this.setTableData('available', endpoints, response.totalCount);
this.noEndpoints = this.state.available.totalCount === 0;
}
getAssociatedEndpoints() {
return this.$async(this.getAssociatedEndpointsAsync);
}
async getAssociatedEndpointsAsync() {
let response = { value: [], totalCount: 0 };
if (this.endpointIds.length > 0) {
const { start, search, limit } = this.getPaginationData('associated');
const query = { search, type: 4, endpointIds: this.endpointIds };
response = await this.EndpointService.endpoints(start, limit, query);
}
this.setTableData('associated', response.value, response.totalCount);
}
associateEndpoint(endpoint) {
this.onAssociate(endpoint);
}
dissociateEndpoint(endpoint) {
this.onDissociate(endpoint);
}
getPaginationData(tableType) {
const { pageNumber, limit, search } = this.state[tableType];
const start = (pageNumber - 1) * limit + 1;
return { start, search, limit };
}
setTableData(tableType, endpoints, totalCount) {
this.endpoints[tableType] = endpoints;
this.state[tableType].totalCount = parseInt(totalCount, 10);
}
}
angular.module('portainer.app').controller('AssoicatedEndpointsSelectorController', AssoicatedEndpointsSelectorController);
export default AssoicatedEndpointsSelectorController;

View file

@ -0,0 +1,8 @@
<information-panel title-text="Information">
<span class="small">
<p class="text-muted">
<i class="fa fa-flask orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This is a beta feature.
</p>
</span>
</information-panel>

View file

@ -0,0 +1,3 @@
angular.module('portainer.app').component('betaPanel', {
templateUrl: './betaPanel.html',
});

View file

@ -1,105 +0,0 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<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="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 ng-model-options="{ debounce: 300 }" />
</div>
<div class="table-responsive">
<table class="table table-hover table-filters nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Endpoint')">
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Endpoint' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Endpoint' && $ctrl.state.reverseOrder"></i>
Endpoint
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Id')">
Id
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Status')">
State
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('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>
Created
</a>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
>
<td>
{{ item.Endpoint.Name }}
<a ng-if="item.Edge" ng-click="$ctrl.getEdgeTaskLogs(item.EndpointId, item.Id)"
><i class="fa fa-download" aria-hidden="true" style="margin-left: 5px; margin-right: 2px;"></i> Download logs</a
>
</td>
<td>
<a ng-if="!item.Edge" ng-click="$ctrl.goToContainerLogs(item.EndpointId, item.Id)">{{ item.Id | truncate: 32 }}</a>
<span ng-if="item.Edge">-</span>
</td>
<td>
<span ng-if="!item.Edge" class="label label-{{ item.Status | containerstatusbadge }}">{{ item.Status }}</span>
<span ng-if="item.Edge">-</span>
</td>
<td>
<span ng-if="!item.Edge">{{ item.Created | getisodatefromtimestamp }}</span>
<span ng-if="item.Edge">-</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="9" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="9" class="text-center text-muted">No tasks available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<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>
</div>
</div>

View file

@ -1,14 +0,0 @@
angular.module('portainer.docker').component('scheduleTasksDatatable', {
templateUrl: './scheduleTasksDatatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
goToContainerLogs: '<',
getEdgeTaskLogs: '<',
},
});

View file

@ -1,97 +0,0 @@
<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 ng-model-options="{ debounce: 300 }" />
</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-click="$ctrl.selectItem(item, $event)" ng-disabled="!$ctrl.allowSelection(item)" />
<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>

View file

@ -1,13 +0,0 @@
angular.module('portainer.app').component('schedulesDatatable', {
templateUrl: './schedulesDatatable.html',
controller: 'SchedulesDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
removeAction: '<',
},
});

View file

@ -1,48 +0,0 @@
angular.module('portainer.app').controller('SchedulesDatatableController', [
'$scope',
'$controller',
'DatatableService',
function ($scope, $controller, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
/**
* Do not allow items
*/
this.allowSelection = function (item) {
return item.JobType === 1;
};
this.$onInit = function () {
this.setDefaults();
this.prepareTableFromDataset();
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
this.onSettingsRepeaterChange();
};
},
]);

View file

@ -1,74 +0,0 @@
angular.module('portainer.app').controller('JobFormController', [
'$state',
'LocalStorage',
'EndpointService',
'EndpointProvider',
'Notifications',
function ($state, LocalStorage, EndpointService, EndpointProvider, Notifications) {
var ctrl = this;
ctrl.$onInit = onInit;
ctrl.editorUpdate = editorUpdate;
ctrl.executeJob = executeJob;
ctrl.state = {
Method: 'editor',
formValidationError: '',
actionInProgress: false,
};
ctrl.formValues = {
Image: 'ubuntu:latest',
JobFileContent: '',
JobFile: null,
};
function onInit() {
var storedImage = LocalStorage.getJobImage();
if (storedImage) {
ctrl.formValues.Image = storedImage;
}
}
function editorUpdate(cm) {
ctrl.formValues.JobFileContent = cm.getValue();
}
function createJob(image, method) {
var endpointId = EndpointProvider.endpointID();
var nodeName = ctrl.nodeName;
if (method === 'editor') {
var jobFileContent = ctrl.formValues.JobFileContent;
return EndpointService.executeJobFromFileContent(image, jobFileContent, endpointId, nodeName);
}
var jobFile = ctrl.formValues.JobFile;
return EndpointService.executeJobFromFileUpload(image, jobFile, endpointId, nodeName);
}
function executeJob() {
var method = ctrl.state.Method;
if (method === 'editor' && ctrl.formValues.JobFileContent === '') {
ctrl.state.formValidationError = 'Script file content must not be empty';
return;
}
var image = ctrl.formValues.Image;
LocalStorage.storeJobImage(image);
ctrl.state.actionInProgress = true;
createJob(image, method)
.then(function success() {
Notifications.success('Job successfully created');
$state.go('^');
})
.catch(function error(err) {
Notifications.error('Job execution failure', err);
})
.finally(function final() {
ctrl.state.actionInProgress = false;
});
}
},
]);

View file

@ -1,109 +0,0 @@
<form class="form-horizontal" name="executeJobForm">
<!-- image-input -->
<div class="form-group">
<label for="job_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.formValues.Image" id="job_image" name="job_image" placeholder="e.g. ubuntu:latest" required auto-focus />
</div>
</div>
<div class="form-group" ng-show="executeJobForm.job_image.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="executeJobForm.job_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 job will run inside a privileged container on the host. You can access the host filesystem under the
<code>/host</code> folder.
</span>
</div>
<!-- execution-method -->
<div class="col-sm-12 form-section-title">
Job creation
</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.state.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.state.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>
<!-- !execution-method -->
<!-- web-editor -->
<div ng-show="$ctrl.state.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-job-editor" placeholder="# Define or paste the content of your script file here" on-change="($ctrl.editorUpdate)"> </code-editor>
</div>
</div>
</div>
<!-- !web-editor -->
<!-- upload -->
<div ng-show="$ctrl.state.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.formValues.JobFile">Select file</button>
<span style="margin-left: 5px;">
{{ $ctrl.formValues.JobFile.name }}
<i class="fa fa-times red-icon" ng-if="!$ctrl.formValues.JobFile" aria-hidden="true"></i>
</span>
</div>
</div>
</div>
<!-- !upload -->
<!-- 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.state.actionInProgress || !executeJobForm.$valid
|| ($ctrl.state.Method === 'upload' && !$ctrl.formValues.JobFile)"
ng-click="$ctrl.executeJob()"
button-spinner="$ctrl.state.actionInProgress"
>
<span ng-hide="$ctrl.state.actionInProgress">Execute</span>
<span ng-show="$ctrl.state.actionInProgress">Starting job...</span>
</button>
<span class="text-danger" ng-if="$ctrl.state.formValidationError" style="margin-left: 5px;">
{{ $ctrl.state.formValidationError }}
</span>
</div>
</div>
<!-- !actions -->
</form>

View file

@ -1,7 +0,0 @@
angular.module('portainer.app').component('executeJobForm', {
templateUrl: './execute-job-form.html',
controller: 'JobFormController',
bindings: {
nodeName: '<',
},
});

View file

@ -1,81 +0,0 @@
import moment from 'moment';
angular.module('portainer.app').component('scheduleForm', {
templateUrl: './scheduleForm.html',
controller: function () {
var ctrl = this;
ctrl.state = {
formValidationError: '',
};
ctrl.scheduleValues = [
{
displayed: 'Every hour',
cron: '0 * * * *',
},
{
displayed: 'Every 2 hours',
cron: '0 */2 * * *',
},
{
displayed: 'Every day',
cron: '0 0 * * *',
},
];
ctrl.formValues = {
datetime: ctrl.model.CronExpression ? cronToDatetime(ctrl.model.CronExpression) : moment(),
scheduleValue: ctrl.scheduleValues[0],
cronMethod: ctrl.model.Recurring ? 'advanced' : 'basic',
};
function cronToDatetime(cron) {
var strings = cron.split(' ');
if (strings.length !== 5) {
return moment();
}
return moment(cron, 's m H D M');
}
function datetimeToCron(datetime) {
var date = moment(datetime);
return '0 '.concat(date.minutes(), ' ', date.hours(), ' ', date.date(), ' ', date.month() + 1, ' *');
}
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;
}
if (ctrl.formValues.cronMethod === 'basic') {
if (ctrl.model.Recurring === false) {
ctrl.model.CronExpression = datetimeToCron(ctrl.formValues.datetime);
} else {
ctrl.model.CronExpression = ctrl.formValues.scheduleValue.cron;
}
} else {
ctrl.model.Recurring = true;
}
ctrl.formAction();
};
this.editorUpdate = function (cm) {
ctrl.model.Job.FileContent = cm.getValue();
};
},
bindings: {
model: '=',
endpoints: '<',
groups: '<',
tags: '<',
addLabelAction: '<',
removeLabelAction: '<',
formAction: '<',
formActionLabel: '@',
actionInProgress: '<',
},
});

View file

@ -1,303 +0,0 @@
<form class="form-horizontal" name="scheduleForm">
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
<p>
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> Due to how schedules behave differently on Edge endpoints and other
endpoints it is recommended to create specific schedules that will only target one type of endpoint.
</p>
</span>
</div>
<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 -->
<!-- schedule-method-select -->
<div class="col-sm-12 form-section-title">
Schedule configuration
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="config_basic" ng-model="$ctrl.formValues.cronMethod" value="basic" />
<label for="config_basic">
<div class="boxselector_header">
<i class="fa fa-calendar-alt" aria-hidden="true" style="margin-right: 2px;"></i>
Basic configuration
</div>
<p>Select date from calendar</p>
</label>
</div>
<div>
<input type="radio" id="config_advanced" ng-model="$ctrl.formValues.cronMethod" value="advanced" />
<label for="config_advanced">
<div class="boxselector_header">
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
Advanced configuration
</div>
<p>Write your own cron rule</p>
</label>
</div>
</div>
</div>
<!-- !schedule-method-select -->
<!-- basic-schedule -->
<div ng-if="$ctrl.formValues.cronMethod === 'basic'">
<div class="form-group">
<label for="recurring" class="col-sm-2 control-label text-left">Recurring schedule</label>
<div class="col-sm-10">
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="recurring" ng-model="$ctrl.model.Recurring" /><i></i> </label>
</div>
</div>
<!-- not-recurring -->
<div ng-if="!$ctrl.model.Recurring">
<div class="form-group">
<label for="schedule_cron" class="col-sm-2 control-label text-left">Schedule date</label>
<div class="col-sm-10">
<input class="form-control" moment-picker ng-model="$ctrl.formValues.datetime" format="YYYY-MM-DD HH:mm" />
</div>
<div ng-show="scheduleForm.datepicker.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="scheduleForm.datepicker.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
</div>
</div>
<!-- !not-recurring -->
<!-- recurring -->
<div ng-if="$ctrl.model.Recurring">
<div class="form-group">
<label for="schedule_value" class="col-sm-2 control-label text-left">Schedule time</label>
<div class="col-sm-10">
<select
id="schedule_value"
name="schedule_value"
class="form-control"
ng-model="$ctrl.formValues.scheduleValue"
ng-options="value.displayed for value in $ctrl.scheduleValues"
required
></select>
</div>
<div ng-show="scheduleForm.schedule_value.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="scheduleForm.schedule_value.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
</div>
</div>
<!-- !recurring -->
</div>
<!-- !basic-schedule -->
<!-- advanced-schedule -->
<div ng-if="$ctrl.formValues.cronMethod === 'advanced'">
<div class="form-group">
<label for="schedule_cron" class="col-sm-2 control-label text-left">Cron rule</label>
<div class="col-sm-10">
<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">
<p>
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.
</p>
<p>
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> Edge endpoint schedules are managed by <code>cron</code> on the
underlying host. You need to use a valid cron expression that is different from the documentation above.
</p>
</span>
</div>
</div>
<!-- !advanced-schedule -->
<!-- !cron-input -->
<div class="col-sm-12 form-section-title">
Job configuration
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
<p> <i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> This configuration will be ignored for Edge endpoint schedules. </p>
</span>
</div>
<!-- image-input -->
<div class="form-group">
<label for="schedule_image" class="col-sm-2 control-label text-left">Image</label>
<div class="col-sm-10">
<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 -->
<!-- retry-policy -->
<div class="form-group">
<label for="retrycount" class="col-sm-2 control-label text-left">
Retry count
<portainer-tooltip position="bottom" message="Number of retries when it's not possible to reach the endpoint."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-4">
<input type="number" class="form-control" ng-model="$ctrl.model.Job.RetryCount" id="retrycount" name="retrycount" placeholder="3" />
</div>
<label for="retryinterval" class="col-sm-2 control-label text-left">
Retry interval
<portainer-tooltip position="bottom" message="Retry interval in seconds."></portainer-tooltip>
</label>
<div class="col-sm-10 col-md-4">
<input type="number" class="form-control" ng-model="$ctrl.model.Job.RetryInterval" id="retryinterval" name="retryinterval" placeholder="30" />
</div>
</div>
<!-- !retry-policy -->
<!-- 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">
<span class="col-sm-12 text-muted small">
<p>
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.
</p>
<p>
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> Edge endpoint schedules are managed by <code>cron</code> on the
underlying host. You have full access to the filesystem without having to use the <code>/host</code> folder.
</p>
</span>
</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 && $ctrl.tags"
style="margin-top: 55px; display: block;"
model="$ctrl.model.Job.Endpoints"
endpoints="$ctrl.endpoints"
groups="$ctrl.groups"
tags="$ctrl.tags"
></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>

View file

@ -1,10 +0,0 @@
angular.module('portainer.app').component('multiEndpointSelector', {
templateUrl: './multiEndpointSelector.html',
controller: 'MultiEndpointSelectorController',
bindings: {
model: '=',
endpoints: '<',
groups: '<',
tags: '<',
},
});

View file

@ -1,18 +0,0 @@
<ui-select multiple ng-model="$ctrl.model" close-on-select="false">
<ui-select-match placeholder="Select one or multiple endpoint(s)">
<span>
{{ $item.Name }}
<span ng-if="$item.TagIds.length"> - <i class="fa fa-tags"></i> {{ $ctrl.tagIdsToTagNames($item.TagIds) | 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.TagIds.length"> - <i class="fa fa-tags"></i> {{ $ctrl.tagIdsToTagNames(endpoint.TagIds) | arraytostr }}</span>
</span>
</ui-select-choices>
</ui-select>

View file

@ -1,48 +0,0 @@
import _ from 'lodash-es';
import PortainerEndpointTagHelper from 'Portainer/helpers/tagHelper';
import angular from 'angular';
class MultiEndpointSelectorController {
/* @ngInject */
constructor() {
this.sortGroups = this.sortGroups.bind(this);
this.groupEndpoints = this.groupEndpoints.bind(this);
this.tagIdsToTagNames = this.tagIdsToTagNames.bind(this);
}
sortGroups(groups) {
return _.sortBy(groups, ['name']);
}
groupEndpoints(endpoint) {
for (var i = 0; i < this.availableGroups.length; i++) {
var group = this.availableGroups[i];
if (endpoint.GroupId === group.Id) {
return group.Name;
}
}
}
tagIdsToTagNames(tagIds) {
return PortainerEndpointTagHelper.idsToTagNames(this.tags, tagIds);
}
filterEmptyGroups() {
this.availableGroups = _.filter(this.groups, (group) => _.some(this.endpoints, (endpoint) => endpoint.GroupId == group.Id));
}
$onInit() {
this.filterEmptyGroups();
}
$onChanges({ endpoints, groups }) {
if (endpoints || groups) {
this.filterEmptyGroups();
}
}
}
export default MultiEndpointSelectorController;
angular.module('portainer.app').controller('MultiEndpointSelectorController', MultiEndpointSelectorController);

View file

@ -1,67 +1,10 @@
import _ from 'lodash-es';
import { createStatus } from '../../docker/models/container';
export function ScheduleDefaultModel() {
this.Name = '';
this.Recurring = false;
this.CronExpression = '';
this.JobType = 1;
this.Job = new ScriptExecutionDefaultJobModel();
}
function ScriptExecutionDefaultJobModel() {
this.Image = 'ubuntu:latest';
this.Endpoints = [];
this.FileContent = '';
this.File = null;
this.Method = 'editor';
}
export function ScheduleModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Recurring = data.Recurring;
this.JobType = data.JobType;
this.CronExpression = data.CronExpression;
this.Created = data.Created;
this.EdgeSchedule = data.EdgeSchedule;
if (this.JobType === 1) {
this.Job = new ScriptExecutionJobModel(data.ScriptExecutionJob, data.EdgeSchedule);
}
}
function ScriptExecutionJobModel(data, edgeSchedule) {
this.Image = data.Image;
this.Endpoints = data.Endpoints;
if (edgeSchedule !== null) {
this.Endpoints = _.concat(data.Endpoints, edgeSchedule.Endpoints);
}
this.FileContent = '';
this.Method = 'editor';
this.RetryCount = data.RetryCount;
this.RetryInterval = data.RetryInterval;
}
export function ScriptExecutionTaskModel(data) {
this.Id = data.Id;
this.EndpointId = data.EndpointId;
this.Status = createStatus(data.Status);
this.Created = data.Created;
this.Edge = data.Edge;
}
export function ScheduleCreateRequest(model) {
this.Name = model.Name;
this.Recurring = model.Recurring;
this.CronExpression = model.CronExpression;
this.Image = model.Job.Image;
this.Endpoints = model.Job.Endpoints;
this.FileContent = model.Job.FileContent;
this.RetryCount = model.Job.RetryCount;
this.RetryInterval = model.Job.RetryInterval;
this.File = model.Job.File;
this.Endpoints = model.Endpoints;
this.FileContent = model.FileContent;
this.File = model.File;
}
export function ScheduleUpdateRequest(model) {
@ -69,9 +12,6 @@ export function ScheduleUpdateRequest(model) {
this.Name = model.Name;
this.Recurring = model.Recurring;
this.CronExpression = model.CronExpression;
this.Image = model.Job.Image;
this.Endpoints = model.Job.Endpoints;
this.FileContent = model.Job.FileContent;
this.RetryCount = model.Job.RetryCount;
this.RetryInterval = model.Job.RetryInterval;
this.Endpoints = model.Endpoints;
this.FileContent = model.FileContent;
}

View file

@ -20,7 +20,6 @@ angular.module('portainer.app').factory('Endpoints', [
remove: { method: 'DELETE', params: { id: '@id' } },
snapshots: { method: 'POST', params: { action: 'snapshot' } },
snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' } },
executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } },
status: { method: 'GET', params: { id: '@id', action: 'status' } },
}
);

View file

@ -1,20 +0,0 @@
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' } },
tasks: { method: 'GET', isArray: true, params: { id: '@id', action: 'tasks' } },
}
);
},
]);

View file

@ -11,6 +11,9 @@ angular.module('portainer.app').factory('EndpointService', [
};
service.endpoints = function (start, limit, { search, type, tagIds, endpointIds, tagsPartialMatch } = {}) {
if (tagIds && !tagIds.length) {
return Promise.resolve({ value: [], totalCount: 0 });
}
return Endpoints.query({ start, limit, search, type, tagIds: JSON.stringify(tagIds), endpointIds: JSON.stringify(endpointIds), tagsPartialMatch }).$promise;
};
@ -126,19 +129,6 @@ angular.module('portainer.app').factory('EndpointService', [
return deferred.promise;
};
service.executeJobFromFileUpload = function (image, jobFile, endpointId, nodeName) {
return FileUploadService.executeEndpointJob(image, jobFile, endpointId, nodeName);
};
service.executeJobFromFileContent = function (image, jobFileContent, endpointId, nodeName) {
var payload = {
Image: image,
FileContent: jobFileContent,
};
return Endpoints.executeJob({ id: endpointId, method: 'string', nodeName: nodeName }, payload).$promise;
};
return service;
},
]);

View file

@ -1,85 +0,0 @@
import { ScheduleModel, ScheduleCreateRequest, ScheduleUpdateRequest, ScriptExecutionTaskModel } 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.scriptExecutionTasks = function (scheduleId) {
var deferred = $q.defer();
Schedules.tasks({ id: scheduleId })
.$promise.then(function success(data) {
var tasks = data.map(function (item) {
return new ScriptExecutionTaskModel(item);
});
deferred.resolve(tasks);
})
.catch(function error(err) {
deferred.reject({ msg: 'Unable to retrieve tasks associated to the schedule', 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;
},
]);

View file

@ -104,17 +104,6 @@ angular.module('portainer.app').factory('FileUploadService', [
});
};
service.executeEndpointJob = function (imageName, file, endpointId, nodeName) {
return Upload.upload({
url: 'api/endpoints/' + endpointId + '/job?method=file&nodeName=' + nodeName,
data: {
File: file,
Image: imageName,
},
ignoreLoadingBar: true,
});
};
service.createEndpoint = function (name, type, URL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, checkinInterval) {
return Upload.upload({
url: 'api/endpoints',

View file

@ -1,55 +0,0 @@
import { ScheduleDefaultModel } from '../../../models/schedule';
angular
.module('portainer.app')
.controller('CreateScheduleController', function CreateScheduleController($q, $scope, $state, Notifications, EndpointService, GroupService, ScheduleService, TagService) {
$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(),
tags: TagService.tags(),
})
.then(function success(data) {
$scope.endpoints = data.endpoints.value;
$scope.groups = data.groups;
$scope.tags = data.tags;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve endpoint list');
});
}
initView();
});

View file

@ -1,22 +0,0 @@
<rd-header>
<rd-header-title title-text="Create schedule"></rd-header-title>
<rd-header-content> <a ui-sref="portainer.schedules">Schedules</a> &gt; 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"
tags="tags"
form-action="create"
form-action-label="Create schedule"
action-in-progress="state.actionInProgress"
></schedule-form>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -1,61 +0,0 @@
<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> &gt; {{ ::schedule.Name }} </rd-header-content>
</rd-header>
<div class="row">
<div class="col-sm-12">
<rd-widget>
<rd-widget-body>
<uib-tabset active="state.activeTab">
<uib-tab index="0">
<uib-tab-heading> <i class="fa fa-wrench" aria-hidden="true"></i> Configuration </uib-tab-heading>
<schedule-form
ng-if="schedule"
model="schedule"
endpoints="endpoints"
groups="groups"
tags="tags"
form-action="update"
form-action-label="Update schedule"
action-in-progress="state.actionInProgress"
></schedule-form>
</uib-tab>
<uib-tab index="1">
<uib-tab-heading> <i class="fa fa-tasks" aria-hidden="true"></i> Tasks </uib-tab-heading>
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Tasks are retrieved across all endpoints via snapshots. Data available in this view might not be up-to-date.
</span>
</div>
<div class="col-sm-12 form-section-title" style="margin-bottom: 20px;">
Tasks
</div>
<schedule-tasks-datatable
ng-if="tasks"
title-text="Tasks"
title-icon="fa-tasks"
dataset="tasks"
table-key="schedule-tasks"
order-by="Status"
reverse-order="true"
go-to-container-logs="goToContainerLogs"
get-edge-task-logs="getEdgeTaskLogs"
></schedule-tasks-datatable>
</uib-tab>
</uib-tabset>
</rd-widget-body>
</rd-widget>
</div>
</div>

View file

@ -1,113 +0,0 @@
angular
.module('portainer.app')
.controller('ScheduleController', function ScheduleController(
$q,
$scope,
$transition$,
$state,
Notifications,
EndpointService,
GroupService,
ScheduleService,
EndpointProvider,
HostBrowserService,
FileSaver,
TagService
) {
$scope.state = {
actionInProgress: false,
};
$scope.update = update;
$scope.goToContainerLogs = goToContainerLogs;
$scope.getEdgeTaskLogs = getEdgeTaskLogs;
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 goToContainerLogs(endpointId, containerId) {
EndpointProvider.setEndpointID(endpointId);
$state.go('docker.containers.container.logs', { id: containerId });
}
function getEdgeTaskLogs(endpointId, scheduleId) {
var currentId = EndpointProvider.endpointID();
EndpointProvider.setEndpointID(endpointId);
var filePath = '/host/opt/portainer/scripts/' + scheduleId + '.log';
HostBrowserService.get(filePath)
.then(function onFileReceived(data) {
var downloadData = new Blob([data.file], {
type: 'text/plain;charset=utf-8',
});
FileSaver.saveAs(downloadData, scheduleId + '.log');
})
.catch(function notifyOnError(err) {
Notifications.error('Failure', err, 'Unable to download file');
})
.finally(function final() {
EndpointProvider.setEndpointID(currentId);
});
}
function associateEndpointsToTasks(tasks, endpoints) {
for (var i = 0; i < tasks.length; i++) {
var task = tasks[i];
for (var j = 0; j < endpoints.length; j++) {
var endpoint = endpoints[j];
if (task.EndpointId === endpoint.Id) {
task.Endpoint = endpoint;
break;
}
}
}
}
function initView() {
var id = $transition$.params().id;
$q.all({
schedule: ScheduleService.schedule(id),
file: ScheduleService.getScriptFile(id),
tasks: ScheduleService.scriptExecutionTasks(id),
endpoints: EndpointService.endpoints(),
groups: GroupService.groups(),
tags: TagService.tags(),
})
.then(function success(data) {
var schedule = data.schedule;
schedule.Job.FileContent = data.file.ScheduleFileContent;
var endpoints = data.endpoints.value;
var tasks = data.tasks;
associateEndpointsToTasks(tasks, endpoints);
$scope.schedule = schedule;
$scope.tasks = data.tasks;
$scope.endpoints = data.endpoints.value;
$scope.groups = data.groups;
$scope.tags = data.tags;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve endpoint list');
});
}
initView();
});

View file

@ -1,14 +0,0 @@
<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>

View file

@ -1,53 +0,0 @@
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();
},
]);

View file

@ -101,10 +101,7 @@
<div class="col-sm-12">
<label for="toggle_enableHostManagementFeatures" class="control-label text-left">
Enable host management features
<portainer-tooltip
position="bottom"
message="Enables host management features: host scheduler, host browsing and command execution. You'll need to set the CAP_HOST_MANAGEMENT variable when deploying the Portainer agent to use some of these features."
></portainer-tooltip>
<portainer-tooltip position="bottom" message="Enable host management features: host system browsing and advanced host details."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="toggle_enableHostManagementFeatures" ng-model="formValues.enableHostManagementFeatures" /><i></i>

View file

@ -85,12 +85,6 @@
<a ui-sref="storidge.drives" ui-sref-active="active">Drives</a>
</div>
</li>
<li class="sidebar-title" ng-if="isAdmin && applicationState.application.enableHostManagementFeatures">
<span>Scheduler</span>
</li>
<li class="sidebar-list" ng-if="isAdmin && applicationState.application.enableHostManagementFeatures">
<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="isAdmin && applicationState.application.enableEdgeComputeFeatures">
<span>Edge Compute</span>
</li>
@ -100,6 +94,9 @@
<li class="sidebar-list" ng-if="isAdmin && applicationState.application.enableEdgeComputeFeatures">
<a ui-sref="edge.stacks" ui-sref-active="active">Edge Stacks <span class="menu-icon fa fa-layer-group fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="isAdmin && applicationState.application.enableEdgeComputeFeatures">
<a ui-sref="edge.jobs" ui-sref-active="active">Edge Jobs <span class="menu-icon fa fa-clock fa-fw"></span></a>
</li>
<li class="sidebar-title">
<span>Settings</span>
</li>