mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +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:
parent
b6f5d8f90e
commit
24528ecea8
120 changed files with 2624 additions and 3484 deletions
|
@ -71,6 +71,39 @@ angular.module('portainer.edge', []).config(function config($stateRegistryProvid
|
|||
},
|
||||
};
|
||||
|
||||
const edgeJobs = {
|
||||
name: 'edge.jobs',
|
||||
url: '/jobs',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeJobsView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const edgeJob = {
|
||||
name: 'edge.jobs.job',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'edgeJobView',
|
||||
},
|
||||
},
|
||||
params: {
|
||||
tab: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const edgeJobCreation = {
|
||||
name: 'edge.jobs.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'createEdgeJobView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
$stateRegistryProvider.register(edge);
|
||||
|
||||
$stateRegistryProvider.register(groups);
|
||||
|
@ -80,4 +113,8 @@ angular.module('portainer.edge', []).config(function config($stateRegistryProvid
|
|||
$stateRegistryProvider.register(stacks);
|
||||
$stateRegistryProvider.register(stacksNew);
|
||||
$stateRegistryProvider.register(stacksEdit);
|
||||
|
||||
$stateRegistryProvider.register(edgeJobs);
|
||||
$stateRegistryProvider.register(edgeJob);
|
||||
$stateRegistryProvider.register(edgeJobCreation);
|
||||
});
|
||||
|
|
256
app/edge/components/edge-job-form/edgeJobForm.html
Normal file
256
app/edge/components/edge-job-form/edgeJobForm.html
Normal file
|
@ -0,0 +1,256 @@
|
|||
<form class="form-horizontal" name="edgeJobForm">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Edge job configuration
|
||||
</div>
|
||||
<!-- name-input -->
|
||||
<div class="form-group">
|
||||
<label for="edgejob_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"
|
||||
ng-pattern="/^[a-zA-Z0-9][a-zA-Z0-9_.-]+$/"
|
||||
id="edgejob_name"
|
||||
name="edgejob_name"
|
||||
placeholder="backup-app-prod"
|
||||
required
|
||||
auto-focus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="edgeJobForm.edgejob_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="edgeJobForm.edgejob_name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="pattern"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Allowed characters are: [a-zA-Z0-9_.-]</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- cron-input -->
|
||||
<!-- edge-job-method-select -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Edge job 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>
|
||||
<!-- !edge-job-method-select -->
|
||||
<!-- basic-edge-job -->
|
||||
<div ng-if="$ctrl.formValues.cronMethod === 'basic'">
|
||||
<div class="form-group">
|
||||
<label for="recurring" class="col-sm-2 control-label text-left">Recurring Edge job</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="edgejob_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 class="col-sm-12 small text-muted" style="margin-top: 10px;">
|
||||
Time should be set according to the chosen endpoints' timezone.
|
||||
</div>
|
||||
<div ng-show="edgeJobForm.datepicker.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="edgeJobForm.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="edgejob_value" class="col-sm-2 control-label text-left">Edge job time</label>
|
||||
<div class="col-sm-10">
|
||||
<select
|
||||
id="edgejob_value"
|
||||
name="edgejob_value"
|
||||
class="form-control"
|
||||
ng-model="$ctrl.formValues.scheduleValue"
|
||||
ng-options="value.displayed for value in $ctrl.scheduleValues"
|
||||
required
|
||||
></select>
|
||||
</div>
|
||||
<div ng-show="edgeJobForm.edgejob_value.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="edgeJobForm.edgejob_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-edge-job -->
|
||||
<!-- advanced-schedule -->
|
||||
<div ng-if="$ctrl.formValues.cronMethod === 'advanced'">
|
||||
<div class="form-group">
|
||||
<label for="edgejob_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="edgejob_cron"
|
||||
name="edgejob_cron"
|
||||
placeholder="0 2 * * *"
|
||||
required
|
||||
ng-pattern="$ctrl.cronRegex"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-sm-12 small text-muted" style="margin-top: 10px;">
|
||||
Time should be set according to the chosen endpoints' timezone.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="edgeJobForm.edgejob_cron.$invalid && edgeJobForm.edgejob_cron.$dirty">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="edgeJobForm.edgejob_cron.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
<p ng-message="pattern"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field format is invalid.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !advanced-schedule -->
|
||||
|
||||
<!-- 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.formValues.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.formValues.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.formValues.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-edge-job-editor"
|
||||
placeholder="# Define or paste the content of your script file here"
|
||||
on-change="($ctrl.editorUpdate)"
|
||||
value="$ctrl.model.FileContent"
|
||||
></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !web-editor -->
|
||||
<!-- upload -->
|
||||
<div ng-show="$ctrl.formValues.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.File">Select file</button>
|
||||
<span style="margin-left: 5px;">
|
||||
{{ $ctrl.model.File.name }}
|
||||
<i class="fa fa-times red-icon" ng-if="!$ctrl.model.File" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !upload -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Target endpoints
|
||||
</div>
|
||||
<!-- node-selection -->
|
||||
<associated-endpoints-selector
|
||||
endpoint-ids="$ctrl.model.Endpoints"
|
||||
tags="$ctrl.tags"
|
||||
groups="$ctrl.groups"
|
||||
has-backend-pagination="true"
|
||||
on-associate="($ctrl.associateEndpoint)"
|
||||
on-dissociate="($ctrl.dissociateEndpoint)"
|
||||
></associated-endpoints-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 || !edgeJobForm.$valid
|
||||
|| $ctrl.model.Endpoints.length === 0
|
||||
|| ($ctrl.formValues.method === 'upload' && !$ctrl.model.File)
|
||||
|| ($ctrl.formValues.method === 'editor' && !$ctrl.model.FileContent)
|
||||
"
|
||||
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>
|
16
app/edge/components/edge-job-form/edgeJobForm.js
Normal file
16
app/edge/components/edge-job-form/edgeJobForm.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import EdgeJobFormController from './edgeJobFormController';
|
||||
|
||||
angular.module('portainer.edge').component('edgeJobForm', {
|
||||
templateUrl: './edgeJobForm.html',
|
||||
controller: EdgeJobFormController,
|
||||
bindings: {
|
||||
model: '=',
|
||||
groups: '<',
|
||||
tags: '<',
|
||||
addLabelAction: '<',
|
||||
removeLabelAction: '<',
|
||||
formAction: '<',
|
||||
formActionLabel: '@',
|
||||
actionInProgress: '<',
|
||||
},
|
||||
});
|
107
app/edge/components/edge-job-form/edgeJobFormController.js
Normal file
107
app/edge/components/edge-job-form/edgeJobFormController.js
Normal file
|
@ -0,0 +1,107 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import moment from 'moment';
|
||||
|
||||
class EdgeJobFormController {
|
||||
/* @ngInject */
|
||||
constructor() {
|
||||
this.state = {
|
||||
formValidationError: '',
|
||||
};
|
||||
|
||||
this.scheduleValues = [
|
||||
{
|
||||
displayed: 'Every hour',
|
||||
cron: '0 * * * *',
|
||||
},
|
||||
{
|
||||
displayed: 'Every 2 hours',
|
||||
cron: '0 */2 * * *',
|
||||
},
|
||||
{
|
||||
displayed: 'Every day',
|
||||
cron: '0 0 * * *',
|
||||
},
|
||||
];
|
||||
|
||||
this.formValues = {
|
||||
datetime: moment(),
|
||||
scheduleValue: this.scheduleValues[0],
|
||||
cronMethod: 'basic',
|
||||
method: 'editor',
|
||||
};
|
||||
|
||||
// see https://regexr.com/573i2
|
||||
this.cronRegex = /(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ){4,6}((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*))/;
|
||||
|
||||
this.onChangeModel(this.model);
|
||||
|
||||
this.action = this.action.bind(this);
|
||||
this.editorUpdate = this.editorUpdate.bind(this);
|
||||
this.associateEndpoint = this.associateEndpoint.bind(this);
|
||||
this.dissociateEndpoint = this.dissociateEndpoint.bind(this);
|
||||
}
|
||||
|
||||
onChangeModel(model) {
|
||||
const defaultTime = moment().add('hours', 1);
|
||||
this.formValues = {
|
||||
datetime: model.CronExpression ? cronToDatetime(model.CronExpression, defaultTime) : defaultTime,
|
||||
scheduleValue: this.formValues.scheduleValue,
|
||||
cronMethod: model.Recurring ? 'advanced' : 'basic',
|
||||
method: this.formValues.method,
|
||||
};
|
||||
}
|
||||
|
||||
action() {
|
||||
this.state.formValidationError = '';
|
||||
|
||||
if (this.formValues.method === 'editor' && this.model.FileContent === '') {
|
||||
this.state.formValidationError = 'Script file content must not be empty';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.formValues.cronMethod === 'basic') {
|
||||
if (!this.model.Recurring) {
|
||||
this.model.CronExpression = datetimeToCron(this.formValues.datetime);
|
||||
} else {
|
||||
this.model.CronExpression = this.formValues.scheduleValue.cron;
|
||||
}
|
||||
} else {
|
||||
this.model.Recurring = true;
|
||||
}
|
||||
|
||||
this.formAction(this.formValues.method);
|
||||
}
|
||||
|
||||
editorUpdate(cm) {
|
||||
this.model.FileContent = cm.getValue();
|
||||
}
|
||||
|
||||
associateEndpoint(endpoint) {
|
||||
if (!_.includes(this.model.Endpoints, endpoint.Id)) {
|
||||
this.model.Endpoints = [...this.model.Endpoints, endpoint.Id];
|
||||
}
|
||||
}
|
||||
|
||||
dissociateEndpoint(endpoint) {
|
||||
this.model.Endpoints = _.filter(this.model.Endpoints, (id) => id !== endpoint.Id);
|
||||
}
|
||||
}
|
||||
|
||||
function cronToDatetime(cron, defaultTime = moment()) {
|
||||
var strings = cron.split(' ');
|
||||
if (strings.length > 4) {
|
||||
strings = strings.slice(0, 4);
|
||||
} else {
|
||||
return defaultTime;
|
||||
}
|
||||
return moment(cron, 'm H D M');
|
||||
}
|
||||
|
||||
function datetimeToCron(datetime) {
|
||||
var date = moment(datetime);
|
||||
return [date.minutes(), date.hours(), date.date(), date.month() + 1, '*'].join(' ');
|
||||
}
|
||||
|
||||
angular.module('portainer.edge').controller('EdgeJobFormController', EdgeJobFormController);
|
||||
export default EdgeJobFormController;
|
|
@ -0,0 +1,3 @@
|
|||
.edge-job-results-datatable thead th {
|
||||
width: 50%;
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<div class="datatable edge-job-results-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>
|
||||
Actions
|
||||
</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 }}
|
||||
</td>
|
||||
<td>
|
||||
<button ng-if="item.LogsStatus === 0 || item.LogsStatus === 1" class="btn btn-sm btn-primary" ng-click="$ctrl.collectLogs(item.EndpointId)">
|
||||
Retrieve logs
|
||||
</button>
|
||||
<button ng-if="item.LogsStatus === 3" class="btn btn-sm btn-primary" ng-click="$ctrl.onDownloadLogsClick(item.EndpointId)">
|
||||
Download logs
|
||||
</button>
|
||||
<button ng-if="item.LogsStatus === 3" class="btn btn-sm btn-primary" ng-click="$ctrl.onClearLogsClick(item.EndpointId)">
|
||||
Clear logs
|
||||
</button>
|
||||
<span ng-if="item.LogsStatus === 2">
|
||||
Logs marked for collection, please wait until the logs are available.
|
||||
</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 result 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>
|
|
@ -0,0 +1,20 @@
|
|||
import angular from 'angular';
|
||||
import EdgeJobResultsDatatableController from './edgeJobResultsDatatableController';
|
||||
import './edgeJobResultsDatatable.css';
|
||||
|
||||
angular.module('portainer.edge').component('edgeJobResultsDatatable', {
|
||||
templateUrl: './edgeJobResultsDatatable.html',
|
||||
controller: EdgeJobResultsDatatableController,
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
onDownloadLogsClick: '<',
|
||||
onCollectLogsClick: '<',
|
||||
onClearLogsClick: '<',
|
||||
refreshCallback: '<',
|
||||
},
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
export default class EdgeJobResultsDatatableController {
|
||||
/* @ngInject */
|
||||
constructor($controller, $scope, $state) {
|
||||
this.$state = $state;
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope }));
|
||||
}
|
||||
|
||||
collectLogs(...args) {
|
||||
this.settings.repeater.autoRefresh = true;
|
||||
this.settings.repeater.refreshRate = '5';
|
||||
this.onSettingsRepeaterChange();
|
||||
this.onCollectLogsClick(...args);
|
||||
}
|
||||
|
||||
$onChanges({ dataset }) {
|
||||
if (dataset && dataset.currentValue) {
|
||||
this.onDatasetChange(dataset.currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
onDatasetChange(dataset) {
|
||||
const anyCollecting = _.some(dataset, (item) => item.LogsStatus === 2);
|
||||
this.settings.repeater.autoRefresh = anyCollecting;
|
||||
this.settings.repeater.refreshRate = '5';
|
||||
this.onSettingsRepeaterChange();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
<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="edge.jobs.new"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add Edge job </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)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="edge.jobs.job({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 Edge job 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>
|
15
app/edge/components/edge-jobs-datatable/edgeJobsDatatable.js
Normal file
15
app/edge/components/edge-jobs-datatable/edgeJobsDatatable.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.edge').component('edgeJobsDatatable', {
|
||||
templateUrl: './edgeJobsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
},
|
||||
});
|
|
@ -54,55 +54,15 @@
|
|||
Associated endpoints
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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 -->
|
||||
<associated-endpoints-selector
|
||||
endpoint-ids="$ctrl.model.Endpoints"
|
||||
tags="$ctrl.tags"
|
||||
groups="$ctrl.groups"
|
||||
has-backend-pagination="true"
|
||||
|
||||
on-associate="$ctrl.associateEndpoint"
|
||||
on-dissociate="$ctrl.dissociateEndpoint"
|
||||
></associated-endpoints-selector>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -155,9 +115,9 @@
|
|||
loaded="$ctrl.loaded"
|
||||
page-type="$ctrl.pageType"
|
||||
table-type="associated"
|
||||
retrieve-page="$ctrl.getPaginatedEndpoints"
|
||||
dataset="$ctrl.endpoints.associated"
|
||||
pagination-state="$ctrl.state.associated"
|
||||
retrieve-page="$ctrl.getDynamicEndpoints"
|
||||
dataset="$ctrl.endpoints.value"
|
||||
pagination-state="$ctrl.endpoints.state"
|
||||
empty-dataset-message="No associated endpoint"
|
||||
tags="$ctrl.tags"
|
||||
show-tags="true"
|
||||
|
|
|
@ -7,35 +7,27 @@ class EdgeGroupFormController {
|
|||
this.EndpointService = EndpointService;
|
||||
this.$async = $async;
|
||||
|
||||
this.state = {
|
||||
available: {
|
||||
limit: '10',
|
||||
filter: '',
|
||||
pageNumber: 1,
|
||||
totalCount: 0,
|
||||
},
|
||||
associated: {
|
||||
limit: '10',
|
||||
filter: '',
|
||||
pageNumber: 1,
|
||||
totalCount: 0,
|
||||
},
|
||||
};
|
||||
|
||||
this.endpoints = {
|
||||
associated: [],
|
||||
available: null,
|
||||
state: {
|
||||
limit: '10',
|
||||
filter: '',
|
||||
pageNumber: 1,
|
||||
totalCount: 0,
|
||||
},
|
||||
value: null,
|
||||
};
|
||||
|
||||
this.associateEndpoint = this.associateEndpoint.bind(this);
|
||||
this.dissociateEndpoint = this.dissociateEndpoint.bind(this);
|
||||
this.getPaginatedEndpointsAsync = this.getPaginatedEndpointsAsync.bind(this);
|
||||
this.getPaginatedEndpoints = this.getPaginatedEndpoints.bind(this);
|
||||
this.getDynamicEndpointsAsync = this.getDynamicEndpointsAsync.bind(this);
|
||||
this.getDynamicEndpoints = this.getDynamicEndpoints.bind(this);
|
||||
|
||||
$scope.$watch(
|
||||
() => this.model,
|
||||
() => {
|
||||
this.getPaginatedEndpoints(this.pageType, 'associated');
|
||||
if (this.model.Dynamic) {
|
||||
this.getDynamicEndpoints();
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
|
@ -43,50 +35,28 @@ class EdgeGroupFormController {
|
|||
|
||||
associateEndpoint(endpoint) {
|
||||
if (!_.includes(this.model.Endpoints, endpoint.Id)) {
|
||||
this.endpoints.associated.push(endpoint);
|
||||
this.model.Endpoints.push(endpoint.Id);
|
||||
_.remove(this.endpoints.available, { Id: endpoint.Id });
|
||||
this.model.Endpoints = [...this.model.Endpoints, endpoint.Id];
|
||||
}
|
||||
}
|
||||
|
||||
dissociateEndpoint(endpoint) {
|
||||
_.remove(this.endpoints.associated, { Id: endpoint.Id });
|
||||
_.remove(this.model.Endpoints, (id) => id === endpoint.Id);
|
||||
this.endpoints.available.push(endpoint);
|
||||
this.model.Endpoints = _.filter(this.model.Endpoints, (id) => id !== endpoint.Id);
|
||||
}
|
||||
|
||||
getPaginatedEndpoints(pageType, tableType) {
|
||||
return this.$async(this.getPaginatedEndpointsAsync, pageType, tableType);
|
||||
getDynamicEndpoints() {
|
||||
return this.$async(this.getDynamicEndpointsAsync);
|
||||
}
|
||||
|
||||
async getPaginatedEndpointsAsync(pageType, tableType) {
|
||||
const { pageNumber, limit, search } = this.state[tableType];
|
||||
async getDynamicEndpointsAsync() {
|
||||
const { pageNumber, limit, search } = this.endpoints.state;
|
||||
const start = (pageNumber - 1) * limit + 1;
|
||||
const query = { search, type: 4 };
|
||||
if (tableType === 'associated') {
|
||||
if (this.model.Dynamic) {
|
||||
query.tagIds = this.model.TagIds;
|
||||
query.tagsPartialMatch = this.model.PartialMatch;
|
||||
} else {
|
||||
query.endpointIds = this.model.Endpoints;
|
||||
}
|
||||
}
|
||||
const response = await this.fetchEndpoints(start, limit, query);
|
||||
const query = { search, type: 4, tagIds: this.model.TagIds, tagsPartialMatch: this.model.PartialMatch };
|
||||
|
||||
const response = await this.EndpointService.endpoints(start, limit, query);
|
||||
|
||||
const totalCount = parseInt(response.totalCount, 10);
|
||||
this.endpoints[tableType] = response.value;
|
||||
this.state[tableType].totalCount = totalCount;
|
||||
|
||||
if (tableType === 'available') {
|
||||
this.noEndpoints = totalCount === 0;
|
||||
this.endpoints[tableType] = _.filter(response.value, (endpoint) => !_.includes(this.model.Endpoints, endpoint.Id));
|
||||
}
|
||||
}
|
||||
|
||||
fetchEndpoints(start, limit, query) {
|
||||
if (query.tagIds && !query.tagIds.length) {
|
||||
return { value: [], totalCount: 0 };
|
||||
}
|
||||
return this.EndpointService.endpoints(start, limit, query);
|
||||
this.endpoints.value = response.value;
|
||||
this.endpoints.state.totalCount = totalCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
14
app/edge/rest/edge-job-results.js
Normal file
14
app/edge/rest/edge-job-results.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
angular.module('portainer.edge').factory('EdgeJobResults', EdgeJobResultsFactory);
|
||||
|
||||
function EdgeJobResultsFactory($resource, API_ENDPOINT_EDGE_JOBS) {
|
||||
return $resource(
|
||||
API_ENDPOINT_EDGE_JOBS + '/:id/tasks/:taskId/:action',
|
||||
{},
|
||||
{
|
||||
query: { method: 'GET', isArray: true, params: { id: '@id' } },
|
||||
logFile: { method: 'GET', params: { id: '@id', taskId: '@taskId', action: 'logs' } },
|
||||
clearLogs: { method: 'DELETE', params: { id: '@id', taskId: '@taskId', action: 'logs' } },
|
||||
collectLogs: { method: 'POST', params: { id: '@id', taskId: '@taskId', action: 'logs' } },
|
||||
}
|
||||
);
|
||||
}
|
17
app/edge/rest/edge-jobs.js
Normal file
17
app/edge/rest/edge-jobs.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
angular.module('portainer.edge').factory('EdgeJobs', EdgeJobsFactory);
|
||||
|
||||
function EdgeJobsFactory($resource, API_ENDPOINT_EDGE_JOBS) {
|
||||
return $resource(
|
||||
API_ENDPOINT_EDGE_JOBS + '/: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' } },
|
||||
}
|
||||
);
|
||||
}
|
76
app/edge/services/edge-job.js
Normal file
76
app/edge/services/edge-job.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { ScheduleCreateRequest, ScheduleUpdateRequest } from 'Portainer/models/schedule';
|
||||
|
||||
function EdgeJobService(EdgeJobs, EdgeJobResults, FileUploadService) {
|
||||
var service = {};
|
||||
|
||||
service.edgeJob = edgeJob;
|
||||
async function edgeJob(edgeJobId) {
|
||||
try {
|
||||
return await EdgeJobs.get({ id: edgeJobId }).$promise;
|
||||
} catch (err) {
|
||||
throw { msg: 'Unable to retrieve edgeJob', err: err };
|
||||
}
|
||||
}
|
||||
|
||||
service.edgeJobs = edgeJobs;
|
||||
async function edgeJobs() {
|
||||
try {
|
||||
return await EdgeJobs.query().$promise;
|
||||
} catch (err) {
|
||||
throw { msg: 'Unable to retrieve edgeJobs', err: err };
|
||||
}
|
||||
}
|
||||
|
||||
service.jobResults = jobResults;
|
||||
async function jobResults(edgeJobId) {
|
||||
try {
|
||||
return await EdgeJobResults.query({ id: edgeJobId }).$promise;
|
||||
} catch (err) {
|
||||
throw { msg: 'Unable to retrieve results associated to the edgeJob', err: err };
|
||||
}
|
||||
}
|
||||
|
||||
service.logFile = logFile;
|
||||
function logFile(id, taskId) {
|
||||
return EdgeJobResults.logFile({ id, taskId }).$promise;
|
||||
}
|
||||
|
||||
service.collectLogs = collectLogs;
|
||||
function collectLogs(id, taskId) {
|
||||
return EdgeJobResults.collectLogs({ id, taskId }).$promise;
|
||||
}
|
||||
|
||||
service.clearLogs = clearLogs;
|
||||
function clearLogs(id, taskId) {
|
||||
return EdgeJobResults.clearLogs({ id, taskId }).$promise;
|
||||
}
|
||||
|
||||
service.createEdgeJobFromFileContent = function (model) {
|
||||
var payload = new ScheduleCreateRequest(model);
|
||||
return EdgeJobs.create({ method: 'string' }, payload).$promise;
|
||||
};
|
||||
|
||||
service.createEdgeJobFromFileUpload = function (model) {
|
||||
var payload = new ScheduleCreateRequest(model);
|
||||
return FileUploadService.createSchedule(payload);
|
||||
};
|
||||
|
||||
service.updateEdgeJob = function (model) {
|
||||
var payload = new ScheduleUpdateRequest(model);
|
||||
return EdgeJobs.update(payload).$promise;
|
||||
};
|
||||
|
||||
service.remove = function (edgeJobId) {
|
||||
return EdgeJobs.remove({ id: edgeJobId }).$promise;
|
||||
};
|
||||
|
||||
service.getScriptFile = function (edgeJobId) {
|
||||
return EdgeJobs.file({ id: edgeJobId }).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
angular.module('portainer.edge').factory('EdgeJobService', EdgeJobService);
|
21
app/edge/views/edge-jobs/create/createEdgeJobView.html
Normal file
21
app/edge/views/edge-jobs/create/createEdgeJobView.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Create Edge job"></rd-header-title>
|
||||
<rd-header-content> <a ui-sref="edge.jobs">Edge Jobs</a> > Create Edge job </rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<edge-job-form
|
||||
model="$ctrl.model"
|
||||
groups="$ctrl.groups"
|
||||
tags="$ctrl.tags"
|
||||
form-action="$ctrl.create"
|
||||
form-action-label="Create edge job"
|
||||
action-in-progress="$ctrl.state.actionInProgress"
|
||||
></edge-job-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
7
app/edge/views/edge-jobs/create/createEdgeJobView.js
Normal file
7
app/edge/views/edge-jobs/create/createEdgeJobView.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import angular from 'angular';
|
||||
import CreateEdgeJobViewController from './createEdgeJobViewController';
|
||||
|
||||
angular.module('portainer.edge').component('createEdgeJobView', {
|
||||
templateUrl: './createEdgeJobView.html',
|
||||
controller: CreateEdgeJobViewController,
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import angular from 'angular';
|
||||
|
||||
class CreateEdgeJobController {
|
||||
constructor($async, $q, $state, EdgeJobService, GroupService, Notifications, TagService) {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
};
|
||||
|
||||
this.$async = $async;
|
||||
this.$q = $q;
|
||||
this.$state = $state;
|
||||
this.Notifications = Notifications;
|
||||
this.GroupService = GroupService;
|
||||
this.EdgeJobService = EdgeJobService;
|
||||
this.TagService = TagService;
|
||||
|
||||
this.create = this.create.bind(this);
|
||||
this.createEdgeJob = this.createEdgeJob.bind(this);
|
||||
this.createAsync = this.createAsync.bind(this);
|
||||
}
|
||||
|
||||
create(method) {
|
||||
return this.$async(this.createAsync, method);
|
||||
}
|
||||
|
||||
async createAsync(method) {
|
||||
this.state.actionInProgress = true;
|
||||
|
||||
try {
|
||||
await this.createEdgeJob(method, this.model);
|
||||
this.Notifications.success('Edge job successfully created');
|
||||
this.$state.go('edge.jobs', {}, { reload: true });
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to create Edge job');
|
||||
}
|
||||
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
|
||||
createEdgeJob(method, model) {
|
||||
if (method === 'editor') {
|
||||
return this.EdgeJobService.createEdgeJobFromFileContent(model);
|
||||
}
|
||||
return this.EdgeJobService.createEdgeJobFromFileUpload(model);
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
this.model = {
|
||||
Name: '',
|
||||
Recurring: false,
|
||||
CronExpression: '',
|
||||
Endpoints: [],
|
||||
FileContent: '',
|
||||
File: null,
|
||||
};
|
||||
|
||||
try {
|
||||
const [groups, tags] = await Promise.all([this.GroupService.groups(), this.TagService.tags()]);
|
||||
this.groups = groups;
|
||||
this.tags = tags;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve page data');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('portainer.edge').controller('CreateEdgeJobController', CreateEdgeJobController);
|
||||
export default CreateEdgeJobController;
|
23
app/edge/views/edge-jobs/edgeJobsView.html
Normal file
23
app/edge/views/edge-jobs/edgeJobsView.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Edge Jobs">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="edge.jobs" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Edge Jobs</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<beta-panel></beta-panel>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<edge-jobs-datatable
|
||||
title-text="Edge jobs"
|
||||
title-icon="fa-clock"
|
||||
dataset="$ctrl.edgeJobs"
|
||||
table-key="edgeJobs"
|
||||
order-by="Name"
|
||||
remove-action="$ctrl.removeAction"
|
||||
></edge-jobs-datatable>
|
||||
</div>
|
||||
</div>
|
7
app/edge/views/edge-jobs/edgeJobsView.js
Normal file
7
app/edge/views/edge-jobs/edgeJobsView.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import angular from 'angular';
|
||||
import EdgeJobsViewController from './edgeJobsViewController';
|
||||
|
||||
angular.module('portainer.edge').component('edgeJobsView', {
|
||||
templateUrl: './edgeJobsView.html',
|
||||
controller: EdgeJobsViewController,
|
||||
});
|
56
app/edge/views/edge-jobs/edgeJobsViewController.js
Normal file
56
app/edge/views/edge-jobs/edgeJobsViewController.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
class EdgeJobsController {
|
||||
constructor($async, $state, EdgeJobService, ModalService, Notifications) {
|
||||
this.$async = $async;
|
||||
this.$state = $state;
|
||||
this.EdgeJobService = EdgeJobService;
|
||||
this.ModalService = ModalService;
|
||||
this.Notifications = Notifications;
|
||||
|
||||
this.removeAction = this.removeAction.bind(this);
|
||||
this.deleteJobsAsync = this.deleteJobsAsync.bind(this);
|
||||
this.deleteJobs = this.deleteJobs.bind(this);
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
this.ModalService.confirmDeletion('Do you want to remove the selected edge job(s) ?', (confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.deleteJobs(selectedItems);
|
||||
});
|
||||
}
|
||||
|
||||
deleteJobs(edgeJobs) {
|
||||
return this.$async(this.deleteJobsAsync, edgeJobs);
|
||||
}
|
||||
|
||||
async deleteJobsAsync(edgeJobs) {
|
||||
for (let edgeJob of edgeJobs) {
|
||||
try {
|
||||
await this.EdgeJobService.remove(edgeJob.Id);
|
||||
this.Notifications.success('Stack successfully removed', edgeJob.Name);
|
||||
_.remove(this.edgeJobs, edgeJob);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to remove Edge job ' + edgeJob.Name);
|
||||
}
|
||||
}
|
||||
|
||||
this.$state.reload();
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
try {
|
||||
const edgeJobs = await this.EdgeJobService.edgeJobs();
|
||||
this.edgeJobs = edgeJobs;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve Edge jobs');
|
||||
this.edgeJobs = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('portainer.edge').controller('EdgeJobsController', EdgeJobsController);
|
||||
export default EdgeJobsController;
|
52
app/edge/views/edge-jobs/edit/edgeJob.html
Normal file
52
app/edge/views/edge-jobs/edit/edgeJob.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
<rd-header>
|
||||
<rd-header-title title-text="Edge job details">
|
||||
<a data-toggle="tooltip" title-text="Refresh" ui-sref="edge.jobs.job({id: $ctrl.edgeJob.Id, tab: $ctrl.state.activeTab})" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content> <a ui-sref="edge.jobs">Edge jobs</a> > {{ ::$ctrl.edgeJob.Name }} </rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<uib-tabset active="$ctrl.state.activeTab">
|
||||
<uib-tab index="0" select="$ctrl.showEditor()">
|
||||
<uib-tab-heading> <i class="fa fa-wrench" aria-hidden="true"></i> Configuration </uib-tab-heading>
|
||||
|
||||
<edge-job-form
|
||||
ng-if="$ctrl.edgeJob && $ctrl.state.showEditorTab"
|
||||
model="$ctrl.edgeJob"
|
||||
endpoints="endpoints"
|
||||
groups="$ctrl.groups"
|
||||
tags="$ctrl.tags"
|
||||
form-action="$ctrl.update"
|
||||
form-action-label="Update Edge job"
|
||||
action-in-progress="$ctrl.state.actionInProgress"
|
||||
></edge-job-form>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="1">
|
||||
<uib-tab-heading> <i class="fa fa-tasks" aria-hidden="true"></i> Results </uib-tab-heading>
|
||||
|
||||
<edge-job-results-datatable
|
||||
style="display: block; margin-top: 10px;"
|
||||
ng-if="$ctrl.results"
|
||||
title-text="Results"
|
||||
title-icon="fa-tasks"
|
||||
dataset="$ctrl.results"
|
||||
table-key="edge-job-results"
|
||||
order-by="Status"
|
||||
reverse-order="true"
|
||||
refresh-callback="$ctrl.refresh"
|
||||
on-download-logs-click="($ctrl.downloadLogs)"
|
||||
on-collect-logs-click="($ctrl.collectLogs)"
|
||||
on-clear-logs-click="($ctrl.clearLogs)"
|
||||
></edge-job-results-datatable>
|
||||
</uib-tab>
|
||||
</uib-tabset>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
7
app/edge/views/edge-jobs/edit/edgeJob.js
Normal file
7
app/edge/views/edge-jobs/edit/edgeJob.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import angular from 'angular';
|
||||
import EdgeJobController from './edgeJobController';
|
||||
|
||||
angular.module('portainer.edge').component('edgeJobView', {
|
||||
templateUrl: './edgeJob.html',
|
||||
controller: EdgeJobController,
|
||||
});
|
159
app/edge/views/edge-jobs/edit/edgeJobController.js
Normal file
159
app/edge/views/edge-jobs/edit/edgeJobController.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
|
||||
class EdgeJobController {
|
||||
constructor($async, $q, $state, EdgeJobService, EndpointService, FileSaver, GroupService, HostBrowserService, Notifications, TagService) {
|
||||
this.state = {
|
||||
actionInProgress: false,
|
||||
showEditorTab: false,
|
||||
};
|
||||
|
||||
this.$async = $async;
|
||||
this.$q = $q;
|
||||
this.$state = $state;
|
||||
this.EdgeJobService = EdgeJobService;
|
||||
this.EndpointService = EndpointService;
|
||||
this.FileSaver = FileSaver;
|
||||
this.GroupService = GroupService;
|
||||
this.HostBrowserService = HostBrowserService;
|
||||
this.Notifications = Notifications;
|
||||
this.TagService = TagService;
|
||||
|
||||
this.update = this.update.bind(this);
|
||||
this.updateAsync = this.updateAsync.bind(this);
|
||||
this.downloadLogs = this.downloadLogs.bind(this);
|
||||
this.downloadLogsAsync = this.downloadLogsAsync.bind(this);
|
||||
this.collectLogs = this.collectLogs.bind(this);
|
||||
this.collectLogsAsync = this.collectLogsAsync.bind(this);
|
||||
this.clearLogs = this.clearLogs.bind(this);
|
||||
this.clearLogsAsync = this.clearLogsAsync.bind(this);
|
||||
this.refresh = this.refresh.bind(this);
|
||||
this.refreshAsync = this.refreshAsync.bind(this);
|
||||
this.showEditor = this.showEditor.bind(this);
|
||||
}
|
||||
|
||||
update() {
|
||||
return this.$async(this.updateAsync);
|
||||
}
|
||||
|
||||
async updateAsync() {
|
||||
const model = this.edgeJob;
|
||||
this.state.actionInProgress = true;
|
||||
|
||||
try {
|
||||
await this.EdgeJobService.updateEdgeJob(model);
|
||||
this.Notifications.success('Edge job successfully updated');
|
||||
this.$state.go('edge.jobs', {}, { reload: true });
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to update Edge job');
|
||||
}
|
||||
|
||||
this.state.actionInProgress = false;
|
||||
}
|
||||
|
||||
downloadLogs(endpointId) {
|
||||
return this.$async(this.downloadLogsAsync, endpointId);
|
||||
}
|
||||
async downloadLogsAsync(endpointId) {
|
||||
try {
|
||||
const data = await this.EdgeJobService.logFile(this.edgeJob.Id, endpointId);
|
||||
const downloadData = new Blob([data.FileContent], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
const logFileName = `job_${this.edgeJob.Id}_task_${endpointId}.log`;
|
||||
this.FileSaver.saveAs(downloadData, logFileName);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to download file');
|
||||
}
|
||||
}
|
||||
|
||||
associateEndpointsToResults(results, endpoints) {
|
||||
return _.map(results, (result) => {
|
||||
const endpoint = _.find(endpoints, (endpoint) => endpoint.Id === result.EndpointId);
|
||||
result.Endpoint = endpoint;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
collectLogs(endpointId) {
|
||||
return this.$async(this.collectLogsAsync, endpointId);
|
||||
}
|
||||
|
||||
async collectLogsAsync(endpointId) {
|
||||
try {
|
||||
await this.EdgeJobService.collectLogs(this.edgeJob.Id, endpointId);
|
||||
const result = _.find(this.results, (result) => result.EndpointId === endpointId);
|
||||
result.LogsStatus = 2;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to collect logs');
|
||||
}
|
||||
}
|
||||
|
||||
clearLogs(endpointId) {
|
||||
return this.$async(this.clearLogsAsync, endpointId);
|
||||
}
|
||||
async clearLogsAsync(endpointId) {
|
||||
try {
|
||||
await this.EdgeJobService.clearLogs(this.edgeJob.Id, endpointId);
|
||||
const result = _.find(this.results, (result) => result.EndpointId === endpointId);
|
||||
result.LogsStatus = 1;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to clear logs');
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
return this.$async(this.refreshAsync);
|
||||
}
|
||||
async refreshAsync() {
|
||||
const { id } = this.$state.params;
|
||||
const results = await this.EdgeJobService.jobResults(id);
|
||||
if (results.length > 0) {
|
||||
const endpointIds = _.map(results, (result) => result.EndpointId);
|
||||
const endpoints = await this.EndpointService.endpoints(undefined, undefined, { endpointIds });
|
||||
this.results = this.associateEndpointsToResults(results, endpoints.value);
|
||||
} else {
|
||||
this.results = results;
|
||||
}
|
||||
}
|
||||
|
||||
showEditor() {
|
||||
this.state.showEditorTab = true;
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
const { id, tab } = this.$state.params;
|
||||
this.state.activeTab = tab;
|
||||
if (!tab || tab === 0) {
|
||||
this.state.showEditorTab = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const [edgeJob, file, results, groups, tags] = await Promise.all([
|
||||
this.EdgeJobService.edgeJob(id),
|
||||
this.EdgeJobService.getScriptFile(id),
|
||||
this.EdgeJobService.jobResults(id),
|
||||
this.GroupService.groups(),
|
||||
this.TagService.tags(),
|
||||
]);
|
||||
|
||||
edgeJob.FileContent = file.FileContent;
|
||||
this.edgeJob = edgeJob;
|
||||
this.groups = groups;
|
||||
this.tags = tags;
|
||||
|
||||
if (results.length > 0) {
|
||||
const endpointIds = _.map(results, (result) => result.EndpointId);
|
||||
const endpoints = await this.EndpointService.endpoints(undefined, undefined, { endpointIds });
|
||||
this.results = this.associateEndpointsToResults(results, endpoints.value);
|
||||
} else {
|
||||
this.results = results;
|
||||
}
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve endpoint list');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('portainer.edge').controller('EdgeJobController', EdgeJobController);
|
||||
export default EdgeJobController;
|
|
@ -14,7 +14,12 @@ class EdgeGroupsController {
|
|||
}
|
||||
|
||||
async $onInit() {
|
||||
this.items = await this.EdgeGroupService.groups();
|
||||
try {
|
||||
this.items = await this.EdgeGroupService.groups();
|
||||
} catch (err) {
|
||||
this.items = [];
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve Edge groups');
|
||||
}
|
||||
}
|
||||
|
||||
removeAction(selectedItems) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue