mirror of https://github.com/portainer/portainer
feat(edge/jobs): migrate item view to react [EE-2220] (#11887)
parent
62c2bf86aa
commit
eb6d251a73
|
@ -48,7 +48,7 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
|
||||||
// @failure 500
|
// @failure 500
|
||||||
// @failure 400
|
// @failure 400
|
||||||
// @failure 503 "Edge compute features are disabled"
|
// @failure 503 "Edge compute features are disabled"
|
||||||
// @router /edge_jobs/{id} [post]
|
// @router /edge_jobs/{id} [put]
|
||||||
func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) edgeJobUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -106,15 +106,12 @@ angular
|
||||||
|
|
||||||
const edgeJob = {
|
const edgeJob = {
|
||||||
name: 'edge.jobs.job',
|
name: 'edge.jobs.job',
|
||||||
url: '/:id',
|
url: '/:id?tab',
|
||||||
views: {
|
views: {
|
||||||
'content@': {
|
'content@': {
|
||||||
component: 'edgeJobView',
|
component: 'edgeJobsItemView',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
params: {
|
|
||||||
tab: 0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const edgeJobCreation = {
|
const edgeJobCreation = {
|
||||||
|
|
|
@ -1,206 +0,0 @@
|
||||||
<form class="form-horizontal -mt-4" name="edgeJobForm">
|
|
||||||
<div class="col-sm-12 form-section-title"> Edge job configuration </div>
|
|
||||||
<!-- name-input -->
|
|
||||||
<div class="form-group mt-4">
|
|
||||||
<label for="edgejob_name" class="col-sm-2 control-label required text-left">Name </label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
data-cy="edgejob-name-input"
|
|
||||||
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="e.g. backup-app-prod"
|
|
||||||
required
|
|
||||||
auto-focus
|
|
||||||
/>
|
|
||||||
<div class="help-block" ng-show="edgeJobForm.edgejob_name.$invalid">
|
|
||||||
<div class="small text-warning">
|
|
||||||
<div ng-messages="edgeJobForm.edgejob_name.$error">
|
|
||||||
<p ng-message="required" class="vertical-center">
|
|
||||||
<span><pr-icon icon="'alert-triangle'" class-name="'icon-sm icon-warning'"></pr-icon></span> This field is required.
|
|
||||||
</p>
|
|
||||||
<p ng-message="pattern" class="vertical-center">
|
|
||||||
<span><pr-icon icon="'alert-triangle'" class-name="'icon-sm icon-warning'"></pr-icon></span> Allowed characters are: [a-zA-Z0-9_.-]
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- !name-input -->
|
|
||||||
<!-- cron-input -->
|
|
||||||
<!-- edge-job-method-select -->
|
|
||||||
<div class="col-sm-12 form-section-title"> Edge job configuration </div>
|
|
||||||
|
|
||||||
<box-selector slim="true" radio-name="'configurationold'" value="$ctrl.formValues.cronMethod" options="$ctrl.cronMethods" on-change="($ctrl.onCronMethodChange)"></box-selector>
|
|
||||||
|
|
||||||
<!-- !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">
|
|
||||||
<input type="checkbox" name="recurring" ng-model="$ctrl.model.Recurring" id="recurring" data-cy="recurring-edge-job-checkbox" />
|
|
||||||
<span class="slider round"></span>
|
|
||||||
</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" data-cy="edge-job-date-time-picker" />
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 small text-muted mt-2.5"> Time should be set according to the chosen environments' 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" class="vertical-center">
|
|
||||||
<span><pr-icon icon="'alert-triangle'" class-name="'icon-sm icon-warning'"></pr-icon></span> 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"
|
|
||||||
data-cy="edge-job-time-select"
|
|
||||||
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" class="vertical-center">
|
|
||||||
<span><pr-icon icon="'alert-triangle'" class-name="'icon-sm icon-warning'"></pr-icon></span> 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"
|
|
||||||
data-cy="edge-job-cron-input"
|
|
||||||
class="form-control"
|
|
||||||
ng-model="$ctrl.model.CronExpression"
|
|
||||||
id="edgejob_cron"
|
|
||||||
name="edgejob_cron"
|
|
||||||
placeholder="e.g. 0 2 * * *"
|
|
||||||
required
|
|
||||||
ng-pattern="$ctrl.cronRegex"
|
|
||||||
/>
|
|
||||||
<div class="help-block" ng-show="edgeJobForm.edgejob_cron.$invalid && edgeJobForm.edgejob_cron.$dirty">
|
|
||||||
<div class="small text-warning">
|
|
||||||
<div ng-messages="edgeJobForm.edgejob_cron.$error">
|
|
||||||
<p ng-message="required" class="vertical-center">
|
|
||||||
<span><pr-icon icon="'alert-triangle'" class-name="'icon-sm icon-warning'"></pr-icon></span> This field is required.
|
|
||||||
</p>
|
|
||||||
<p ng-message="pattern" class="vertical-center">
|
|
||||||
<span><pr-icon icon="'alert-triangle'" class-name="'icon-sm icon-warning'"></pr-icon></span> This field format is invalid.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-12 small text-muted mt-2.5"> Time should be set according to the chosen environments' timezone. </div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !advanced-schedule -->
|
|
||||||
|
|
||||||
<!-- execution-method -->
|
|
||||||
<div ng-if="!$ctrl.model.Id">
|
|
||||||
<div class="col-sm-12 form-section-title"> Job content </div>
|
|
||||||
<box-selector value="$ctrl.formValues.method" options="$ctrl.buildMethods" radio-name="'buildMethodolds'" on-change="($ctrl.onBuildMethodChange)" slim="true"></box-selector>
|
|
||||||
</div>
|
|
||||||
<!-- !execution-method -->
|
|
||||||
|
|
||||||
<!-- web-editor -->
|
|
||||||
<!-- TODO use web-editor-form component -->
|
|
||||||
<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"
|
|
||||||
shell="true"
|
|
||||||
></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 type="button" class="btn btn-sm btn-primary" ngf-select ng-model="$ctrl.model.File">Select file</button>
|
|
||||||
<span class="space-left">
|
|
||||||
{{ $ctrl.model.File.name }}
|
|
||||||
<span ng-if="!$ctrl.model.File"><pr-icon icon="'x'" class-name="'icon icon-danger'"></pr-icon></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !upload -->
|
|
||||||
|
|
||||||
<edge-groups-selector ng-if="$ctrl.model.EdgeGroups" value="$ctrl.model.EdgeGroups" on-change="($ctrl.onChangeGroups)"></edge-groups-selector>
|
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title"> Target environments </div>
|
|
||||||
<!-- node-selection -->
|
|
||||||
<associated-edge-environments-selector value="$ctrl.model.Endpoints" on-change="($ctrl.onChangeEnvironments)"></associated-edge-environments-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.model.EdgeGroups.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 space-left" ng-if="$ctrl.state.formValidationError"> {{ $ctrl.state.formValidationError }} </span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !actions -->
|
|
||||||
</form>
|
|
|
@ -1,142 +0,0 @@
|
||||||
import moment from 'moment';
|
|
||||||
import { editor, upload } from '@@/BoxSelector/common-options/build-methods';
|
|
||||||
|
|
||||||
import { cronMethodOptions } from '@/react/edge/edge-jobs/CreateView/cron-method-options';
|
|
||||||
|
|
||||||
export class EdgeJobFormController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor($async, $scope) {
|
|
||||||
this.$scope = $scope;
|
|
||||||
this.$async = $async;
|
|
||||||
|
|
||||||
this.cronMethods = cronMethodOptions.map((o) => ({ ...o, id: o.id + '-old' }));
|
|
||||||
this.buildMethods = [editor, upload].map((o) => ({ ...o, id: o.id + '-old' }));
|
|
||||||
|
|
||||||
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.action = this.action.bind(this);
|
|
||||||
this.editorUpdate = this.editorUpdate.bind(this);
|
|
||||||
this.onChangeEnvironments = this.onChangeEnvironments.bind(this);
|
|
||||||
this.onChangeGroups = this.onChangeGroups.bind(this);
|
|
||||||
this.onChange = this.onChange.bind(this);
|
|
||||||
this.onCronMethodChange = this.onCronMethodChange.bind(this);
|
|
||||||
this.onBuildMethodChange = this.onBuildMethodChange.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(values) {
|
|
||||||
this.$scope.$evalAsync(() => {
|
|
||||||
this.formValues = {
|
|
||||||
...this.formValues,
|
|
||||||
...values,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onBuildMethodChange(value) {
|
|
||||||
this.onChange({ method: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onCronMethodChange(value) {
|
|
||||||
this.onChange({ cronMethod: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeModel(model) {
|
|
||||||
const defaultTime = moment().add('hours', 1);
|
|
||||||
const scheduled = this.scheduleValues.find((v) => v.cron === model.CronExpression);
|
|
||||||
|
|
||||||
this.formValues = {
|
|
||||||
datetime: model.CronExpression ? cronToDatetime(model.CronExpression, defaultTime) : defaultTime,
|
|
||||||
scheduleValue: scheduled || this.scheduleValues[0],
|
|
||||||
cronMethod: model.Recurring && !scheduled ? 'advanced' : 'basic',
|
|
||||||
method: this.formValues.method,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeGroups(groups) {
|
|
||||||
return this.$scope.$evalAsync(() => {
|
|
||||||
this.model.EdgeGroups = groups ? groups : [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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.formValues.datetime === undefined || !this.formValues.datetime.isValid())) {
|
|
||||||
this.state.formValidationError = 'Schedule date must not be empty';
|
|
||||||
return;
|
|
||||||
} else 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(value) {
|
|
||||||
this.model.FileContent = value;
|
|
||||||
this.isEditorDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeEnvironments(value) {
|
|
||||||
return this.$scope.$evalAsync(() => {
|
|
||||||
this.model.Endpoints = value;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
this.onChangeModel(this.model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(' ');
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import { EdgeJobFormController } from './edgeJobFormController';
|
|
||||||
|
|
||||||
angular.module('portainer.edge').component('edgeJobForm', {
|
|
||||||
templateUrl: './edgeJobForm.html',
|
|
||||||
controller: EdgeJobFormController,
|
|
||||||
bindings: {
|
|
||||||
model: '=',
|
|
||||||
groups: '<',
|
|
||||||
tags: '<',
|
|
||||||
edgeGroups: '<',
|
|
||||||
addLabelAction: '<',
|
|
||||||
removeLabelAction: '<',
|
|
||||||
formAction: '<',
|
|
||||||
formActionLabel: '@',
|
|
||||||
actionInProgress: '<',
|
|
||||||
isEditorDirty: '=',
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,18 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
|
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
|
||||||
import { ResultsDatatable } from '@/react/edge/edge-jobs/ItemView/ResultsDatatable/ResultsDatatable';
|
|
||||||
|
|
||||||
export const edgeJobsModule = angular
|
|
||||||
.module('portainer.edge.react.components.edge-jobs', [])
|
|
||||||
.component(
|
|
||||||
'edgeJobResultsDatatable',
|
|
||||||
r2a(withUIRouter(ResultsDatatable), [
|
|
||||||
'dataset',
|
|
||||||
'onClearLogs',
|
|
||||||
'onCollectLogs',
|
|
||||||
'onDownloadLogs',
|
|
||||||
'onRefresh',
|
|
||||||
])
|
|
||||||
).name;
|
|
|
@ -9,10 +9,8 @@ import { EdgeCheckinIntervalField } from '@/react/edge/components/EdgeCheckInInt
|
||||||
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
import { EdgeScriptForm } from '@/react/edge/components/EdgeScriptForm';
|
||||||
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
|
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
|
||||||
|
|
||||||
import { edgeJobsModule } from './edge-jobs';
|
|
||||||
|
|
||||||
const ngModule = angular
|
const ngModule = angular
|
||||||
.module('portainer.edge.react.components', [edgeJobsModule])
|
.module('portainer.edge.react.components', [])
|
||||||
|
|
||||||
.component(
|
.component(
|
||||||
'edgeGroupsSelector',
|
'edgeGroupsSelector',
|
||||||
|
|
|
@ -5,10 +5,15 @@ import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||||
import { ListView } from '@/react/edge/edge-jobs/ListView';
|
import { ListView } from '@/react/edge/edge-jobs/ListView';
|
||||||
import { CreateView } from '@/react/edge/edge-jobs/CreateView/CreateView';
|
import { CreateView } from '@/react/edge/edge-jobs/CreateView/CreateView';
|
||||||
|
import { ItemView } from '@/react/edge/edge-jobs/ItemView/ItemView';
|
||||||
|
|
||||||
export const jobsModule = angular
|
export const jobsModule = angular
|
||||||
.module('portainer.edge.react.views.jobs', [])
|
.module('portainer.edge.react.views.jobs', [])
|
||||||
.component('edgeJobsView', r2a(withUIRouter(withCurrentUser(ListView)), []))
|
.component('edgeJobsView', r2a(withUIRouter(withCurrentUser(ListView)), []))
|
||||||
|
.component(
|
||||||
|
'edgeJobsItemView',
|
||||||
|
r2a(withUIRouter(withCurrentUser(ItemView)), [])
|
||||||
|
)
|
||||||
.component(
|
.component(
|
||||||
'edgeJobsCreateView',
|
'edgeJobsCreateView',
|
||||||
r2a(withUIRouter(withCurrentUser(CreateView)), [])
|
r2a(withUIRouter(withCurrentUser(CreateView)), [])
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
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' } },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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', params: { id: 'create', action: '@method' } },
|
|
||||||
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' } },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
import _ from 'lodash-es';
|
|
||||||
|
|
||||||
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 {
|
|
||||||
const results = await EdgeJobResults.query({ id: edgeJobId }).$promise;
|
|
||||||
|
|
||||||
return _.sortBy(results, ['Id']);
|
|
||||||
} 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);
|
|
|
@ -1,46 +0,0 @@
|
||||||
<page-header title="'Edge job details'" breadcrumbs="[{label:'Edge jobs', link:'edge.jobs'}, $ctrl.edgeJob.Name]" reload="true"> </page-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>
|
|
||||||
<span><pr-icon icon="'wrench'"></pr-icon></span> 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"
|
|
||||||
edge-groups="$ctrl.edgeGroups"
|
|
||||||
form-action="$ctrl.update"
|
|
||||||
form-action-label="Update Edge job"
|
|
||||||
action-in-progress="$ctrl.state.actionInProgress"
|
|
||||||
is-editor-dirty="$ctrl.state.isEditorDirty"
|
|
||||||
></edge-job-form>
|
|
||||||
</uib-tab>
|
|
||||||
|
|
||||||
<uib-tab index="1">
|
|
||||||
<uib-tab-heading>
|
|
||||||
<span><pr-icon icon="'list'"></pr-icon></span> Results
|
|
||||||
</uib-tab-heading>
|
|
||||||
|
|
||||||
<edge-job-results-datatable
|
|
||||||
class="mt-4 block"
|
|
||||||
ng-if="$ctrl.results"
|
|
||||||
dataset="$ctrl.results"
|
|
||||||
on-refresh="($ctrl.refresh)"
|
|
||||||
on-download-logs="($ctrl.downloadLogs)"
|
|
||||||
on-collect-logs="($ctrl.collectLogs)"
|
|
||||||
on-clear-logs="($ctrl.clearLogs)"
|
|
||||||
></edge-job-results-datatable>
|
|
||||||
</uib-tab>
|
|
||||||
</uib-tabset>
|
|
||||||
</rd-widget-body>
|
|
||||||
</rd-widget>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,192 +0,0 @@
|
||||||
import _ from 'lodash-es';
|
|
||||||
import { getEnvironments } from '@/react/portainer/environments/environment.service';
|
|
||||||
import { confirmWebEditorDiscard } from '@@/modals/confirm';
|
|
||||||
|
|
||||||
export class EdgeJobController {
|
|
||||||
/* @ngInject */
|
|
||||||
constructor($async, $q, $state, $window, EdgeJobService, FileSaver, GroupService, HostBrowserService, Notifications, TagService) {
|
|
||||||
this.state = {
|
|
||||||
actionInProgress: false,
|
|
||||||
showEditorTab: false,
|
|
||||||
isEditorDirty: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$async = $async;
|
|
||||||
this.$q = $q;
|
|
||||||
this.$state = $state;
|
|
||||||
this.$window = $window;
|
|
||||||
this.EdgeJobService = EdgeJobService;
|
|
||||||
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('Success', 'Edge job successfully updated');
|
|
||||||
this.state.isEditorDirty = false;
|
|
||||||
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);
|
|
||||||
this.results = this.results.map((result) =>
|
|
||||||
result.EndpointId === endpointId
|
|
||||||
? {
|
|
||||||
...result,
|
|
||||||
LogsStatus: 2,
|
|
||||||
}
|
|
||||||
: result
|
|
||||||
);
|
|
||||||
} 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);
|
|
||||||
this.results = this.results.map((result) =>
|
|
||||||
result.EndpointId === endpointId
|
|
||||||
? {
|
|
||||||
...result,
|
|
||||||
LogsStatus: 1,
|
|
||||||
}
|
|
||||||
: result
|
|
||||||
);
|
|
||||||
} 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 getEnvironments({ query: { endpointIds } });
|
|
||||||
this.results = this.associateEndpointsToResults(results, endpoints.value);
|
|
||||||
} else {
|
|
||||||
this.results = results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showEditor() {
|
|
||||||
this.state.showEditorTab = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async uiCanExit() {
|
|
||||||
if (this.edgeJob && this.edgeJob.FileContent !== this.oldFileContent && this.state.isEditorDirty) {
|
|
||||||
return confirmWebEditorDiscard();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.oldFileContent = edgeJob.FileContent;
|
|
||||||
this.edgeJob = edgeJob;
|
|
||||||
this.groups = groups;
|
|
||||||
this.tags = tags;
|
|
||||||
|
|
||||||
this.edgeJob.EdgeGroups = this.edgeJob.EdgeGroups ? this.edgeJob.EdgeGroups : [];
|
|
||||||
this.edgeJob.Endpoints = this.edgeJob.Endpoints ? this.edgeJob.Endpoints : [];
|
|
||||||
|
|
||||||
if (results.length > 0) {
|
|
||||||
const endpointIds = _.map(results, (result) => result.EndpointId);
|
|
||||||
const endpoints = await getEnvironments({ query: { endpointIds } });
|
|
||||||
this.results = this.associateEndpointsToResults(results, endpoints.value);
|
|
||||||
} else {
|
|
||||||
this.results = results;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.Notifications.error('Failure', err, 'Unable to retrieve environment list');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$window.onbeforeunload = () => {
|
|
||||||
if (this.edgeJob && this.edgeJob.FileContent !== this.oldFileContent && this.state.isEditorDirty) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
$onDestroy() {
|
|
||||||
this.state.isEditorDirty = false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import angular from 'angular';
|
|
||||||
import { EdgeJobController } from './edgeJobController';
|
|
||||||
|
|
||||||
angular.module('portainer.edge').component('edgeJobView', {
|
|
||||||
templateUrl: './edgeJob.html',
|
|
||||||
controller: EdgeJobController,
|
|
||||||
});
|
|
|
@ -75,8 +75,6 @@ export function NavTabs<T extends string | number = string>({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (option.children) {
|
onSelect(option.id);
|
||||||
onSelect(option.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Form, Formik, useFormikContext } from 'formik';
|
import { Form, Formik, useFormikContext } from 'formik';
|
||||||
import { useRouter } from '@uirouter/react';
|
import { useRouter } from '@uirouter/react';
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
|
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
|
||||||
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
|
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
|
||||||
|
@ -14,15 +13,19 @@ import { WebEditorForm } from '@@/WebEditorForm';
|
||||||
import { FileUploadForm } from '@@/form-components/FileUpload';
|
import { FileUploadForm } from '@@/form-components/FileUpload';
|
||||||
|
|
||||||
import { NameField } from '../components/EdgeJobForm/NameField';
|
import { NameField } from '../components/EdgeJobForm/NameField';
|
||||||
import { FormValues } from '../components/EdgeJobForm/types';
|
|
||||||
import { useValidation } from '../components/EdgeJobForm/useValidation';
|
|
||||||
import { JobConfigurationFieldset } from '../components/EdgeJobForm/JobConfigurationFieldset';
|
import { JobConfigurationFieldset } from '../components/EdgeJobForm/JobConfigurationFieldset';
|
||||||
import {
|
import {
|
||||||
BasePayload,
|
BasePayload,
|
||||||
CreateEdgeJobPayload,
|
CreateEdgeJobPayload,
|
||||||
useCreateEdgeJobMutation,
|
useCreateEdgeJobMutation,
|
||||||
} from '../queries/useCreateEdgeJobMutation/useCreateEdgeJobMutation';
|
} from '../queries/useCreateEdgeJobMutation/useCreateEdgeJobMutation';
|
||||||
import { defaultCronExpression } from '../components/EdgeJobForm/RecurringFieldset';
|
import {
|
||||||
|
toRecurringRequest,
|
||||||
|
toRecurringViewModel,
|
||||||
|
} from '../components/EdgeJobForm/parseRecurringValues';
|
||||||
|
|
||||||
|
import { FormValues } from './types';
|
||||||
|
import { useValidation } from './useValidation';
|
||||||
|
|
||||||
export function CreateEdgeJobForm() {
|
export function CreateEdgeJobForm() {
|
||||||
const mutation = useCreateEdgeJobMutation();
|
const mutation = useCreateEdgeJobMutation();
|
||||||
|
@ -35,16 +38,12 @@ export function CreateEdgeJobForm() {
|
||||||
validateOnMount
|
validateOnMount
|
||||||
initialValues={{
|
initialValues={{
|
||||||
name: '',
|
name: '',
|
||||||
recurring: false,
|
|
||||||
cronExpression: '',
|
|
||||||
recurringOption: defaultCronExpression,
|
|
||||||
method: 'editor',
|
method: 'editor',
|
||||||
cronMethod: 'basic',
|
|
||||||
dateTime: new Date(),
|
|
||||||
edgeGroupIds: [],
|
edgeGroupIds: [],
|
||||||
environmentIds: [],
|
environmentIds: [],
|
||||||
file: undefined,
|
file: undefined,
|
||||||
fileContent: '',
|
fileContent: '',
|
||||||
|
...toRecurringViewModel(),
|
||||||
}}
|
}}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
mutation.mutate(getPayload(values.method, values), {
|
mutation.mutate(getPayload(values.method, values), {
|
||||||
|
@ -122,6 +121,7 @@ function InnerForm({ isLoading }: { isLoading: boolean }) {
|
||||||
isValid={isValid}
|
isValid={isValid}
|
||||||
data-cy="edgeJobCreate-addJobButton"
|
data-cy="edgeJobCreate-addJobButton"
|
||||||
loadingText="In progress..."
|
loadingText="In progress..."
|
||||||
|
errors={errors}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
@ -162,42 +162,7 @@ function getPayload(
|
||||||
name: values.name,
|
name: values.name,
|
||||||
edgeGroups: values.edgeGroupIds,
|
edgeGroups: values.edgeGroupIds,
|
||||||
endpoints: values.environmentIds,
|
endpoints: values.environmentIds,
|
||||||
...getRecurringConfig(values),
|
...toRecurringRequest(values),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRecurringConfig(values: FormValues): {
|
|
||||||
recurring: boolean;
|
|
||||||
cronExpression: string;
|
|
||||||
} {
|
|
||||||
if (values.cronMethod !== 'basic') {
|
|
||||||
return {
|
|
||||||
recurring: true,
|
|
||||||
cronExpression: values.cronExpression,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (values.recurring) {
|
|
||||||
return {
|
|
||||||
recurring: true,
|
|
||||||
cronExpression: values.recurringOption,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
recurring: false,
|
|
||||||
cronExpression: dateTimeToCron(values.dateTime),
|
|
||||||
};
|
|
||||||
|
|
||||||
function dateTimeToCron(datetime: Date) {
|
|
||||||
const date = moment(datetime);
|
|
||||||
return [
|
|
||||||
date.minutes(),
|
|
||||||
date.hours(),
|
|
||||||
date.date(),
|
|
||||||
date.month() + 1,
|
|
||||||
'*',
|
|
||||||
].join(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { Calendar, Edit } from 'lucide-react';
|
|
||||||
|
|
||||||
import { BoxSelectorOption } from '@@/BoxSelector';
|
|
||||||
|
|
||||||
export const cronMethodOptions: ReadonlyArray<BoxSelectorOption<string>> = [
|
|
||||||
{
|
|
||||||
id: 'config_basic',
|
|
||||||
value: 'basic',
|
|
||||||
icon: Calendar,
|
|
||||||
iconType: 'badge',
|
|
||||||
label: 'Basic configuration',
|
|
||||||
description: 'Select date from calendar',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'config_advanced',
|
|
||||||
value: 'advanced',
|
|
||||||
icon: Edit,
|
|
||||||
iconType: 'badge',
|
|
||||||
label: 'Advanced configuration',
|
|
||||||
description: 'Write your own cron rule',
|
|
||||||
},
|
|
||||||
] as const;
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { timeOptions } from './RecurringFieldset';
|
import { timeOptions } from '../components/EdgeJobForm/RecurringFieldset';
|
||||||
|
|
||||||
export interface FormValues {
|
export interface FormValues {
|
||||||
name: string;
|
name: string;
|
|
@ -12,17 +12,14 @@ import { useMemo } from 'react';
|
||||||
|
|
||||||
import { file } from '@@/form-components/yup-file-validation';
|
import { file } from '@@/form-components/yup-file-validation';
|
||||||
|
|
||||||
import { EdgeJob } from '../../types';
|
import { useNameValidation } from '../components/EdgeJobForm/NameField';
|
||||||
|
import { cronValidation } from '../components/EdgeJobForm/AdvancedCronFieldset';
|
||||||
|
import { timeOptions } from '../components/EdgeJobForm/RecurringFieldset';
|
||||||
|
|
||||||
import { FormValues } from './types';
|
import { FormValues } from './types';
|
||||||
import { useNameValidation } from './NameField';
|
|
||||||
import { cronValidation } from './AdvancedCronFieldset';
|
|
||||||
import { timeOptions } from './RecurringFieldset';
|
|
||||||
|
|
||||||
export function useValidation({
|
export function useValidation(): SchemaOf<FormValues> {
|
||||||
id,
|
const nameValidation = useNameValidation();
|
||||||
}: { id?: EdgeJob['Id'] } = {}): SchemaOf<FormValues> {
|
|
||||||
const nameValidation = useNameValidation(id);
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
object({
|
object({
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { ListIcon, WrenchIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { useIdParam } from '@/react/hooks/useIdParam';
|
||||||
|
import { useParamState } from '@/react/hooks/useParamState';
|
||||||
|
|
||||||
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
import { Widget } from '@@/Widget';
|
||||||
|
import { NavTabs } from '@@/NavTabs';
|
||||||
|
|
||||||
|
import { useEdgeJob } from '../queries/useEdgeJob';
|
||||||
|
|
||||||
|
import { UpdateEdgeJobForm } from './UpdateEdgeJobForm/UpdateEdgeJobForm';
|
||||||
|
import { ResultsDatatable } from './ResultsDatatable/ResultsDatatable';
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
label: 'Configuration',
|
||||||
|
icon: WrenchIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
label: 'Results',
|
||||||
|
icon: ListIcon,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function ItemView() {
|
||||||
|
const id = useIdParam();
|
||||||
|
|
||||||
|
const [tabId = 0, setTabId] = useParamState('tab', (param) =>
|
||||||
|
param ? parseInt(param, 10) : 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const edgeJobQuery = useEdgeJob(id);
|
||||||
|
|
||||||
|
if (!edgeJobQuery.data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edgeJob = edgeJobQuery.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title="Edge job details"
|
||||||
|
breadcrumbs={[{ label: 'Edge jobs', link: 'edge.jobs' }, edgeJob.Name]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-sm-12">
|
||||||
|
<Widget>
|
||||||
|
<Widget.Body>
|
||||||
|
<NavTabs
|
||||||
|
selectedId={tabId}
|
||||||
|
onSelect={(id) => {
|
||||||
|
setTabId(id);
|
||||||
|
}}
|
||||||
|
options={tabs}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{tabId === tabs[0].id && <UpdateEdgeJobForm edgeJob={edgeJob} />}
|
||||||
|
|
||||||
|
{tabId === tabs[1].id && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<ResultsDatatable jobId={edgeJob.Id} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Widget.Body>
|
||||||
|
</Widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,67 +1,93 @@
|
||||||
import { List } from 'lucide-react';
|
import { List } from 'lucide-react';
|
||||||
import { useEffect } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
import { useEnvironmentList } from '@/react/portainer/environments/queries';
|
||||||
|
|
||||||
import { Datatable } from '@@/datatables';
|
import { Datatable } from '@@/datatables';
|
||||||
import { useTableState } from '@@/datatables/useTableState';
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
import { withMeta } from '@@/datatables/extend-options/withMeta';
|
import { withMeta } from '@@/datatables/extend-options/withMeta';
|
||||||
import { useRepeater } from '@@/datatables/useRepeater';
|
|
||||||
|
|
||||||
import { LogsStatus } from '../../types';
|
import { EdgeJob, JobResult, LogsStatus } from '../../types';
|
||||||
|
import { useJobResults } from '../../queries/jobResults/useJobResults';
|
||||||
|
|
||||||
import { DecoratedJobResult } from './types';
|
|
||||||
import { columns } from './columns';
|
import { columns } from './columns';
|
||||||
import { createStore } from './datatable-store';
|
import { createStore } from './datatable-store';
|
||||||
|
|
||||||
const tableKey = 'edge-job-results';
|
const tableKey = 'edge-job-results';
|
||||||
const store = createStore(tableKey);
|
const store = createStore(tableKey);
|
||||||
|
|
||||||
export function ResultsDatatable({
|
export function ResultsDatatable({ jobId }: { jobId: EdgeJob['Id'] }) {
|
||||||
dataset,
|
|
||||||
onCollectLogs,
|
|
||||||
onClearLogs,
|
|
||||||
onDownloadLogs,
|
|
||||||
onRefresh,
|
|
||||||
}: {
|
|
||||||
dataset: Array<DecoratedJobResult>;
|
|
||||||
|
|
||||||
onCollectLogs(envId: EnvironmentId): void;
|
|
||||||
onDownloadLogs(envId: EnvironmentId): void;
|
|
||||||
onClearLogs(envId: EnvironmentId): void;
|
|
||||||
onRefresh(): void;
|
|
||||||
}) {
|
|
||||||
const anyCollecting = dataset.some(
|
|
||||||
(r) => r.LogsStatus === LogsStatus.Pending
|
|
||||||
);
|
|
||||||
const tableState = useTableState(store, tableKey);
|
const tableState = useTableState(store, tableKey);
|
||||||
|
|
||||||
const { setAutoRefreshRate } = tableState;
|
const jobResultsQuery = useJobResults(jobId, {
|
||||||
|
refetchInterval(dataset) {
|
||||||
|
const anyCollecting = dataset?.some(
|
||||||
|
(r) => r.LogsStatus === LogsStatus.Pending
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
if (anyCollecting) {
|
||||||
setAutoRefreshRate(anyCollecting ? 5 : 0);
|
return 5000;
|
||||||
}, [anyCollecting, setAutoRefreshRate]);
|
}
|
||||||
|
|
||||||
|
return tableState.autoRefreshRate * 1000;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const environmentIds = jobResultsQuery.data?.map(
|
||||||
|
(result) => result.EndpointId
|
||||||
|
);
|
||||||
|
|
||||||
|
const environmentsQuery = useEnvironmentList(
|
||||||
|
{ endpointIds: environmentIds },
|
||||||
|
{ enabled: !!environmentIds && !jobResultsQuery.isLoading }
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataset = useMemo(
|
||||||
|
() =>
|
||||||
|
jobResultsQuery.isLoading || environmentsQuery.isLoading
|
||||||
|
? []
|
||||||
|
: associateEndpointsToResults(
|
||||||
|
jobResultsQuery.data || [],
|
||||||
|
environmentsQuery.environments
|
||||||
|
),
|
||||||
|
[
|
||||||
|
environmentsQuery.environments,
|
||||||
|
environmentsQuery.isLoading,
|
||||||
|
jobResultsQuery.data,
|
||||||
|
jobResultsQuery.isLoading,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
useRepeater(tableState.autoRefreshRate, onRefresh);
|
|
||||||
return (
|
return (
|
||||||
<Datatable
|
<Datatable
|
||||||
disableSelect
|
disableSelect
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataset={dataset}
|
dataset={dataset}
|
||||||
|
isLoading={jobResultsQuery.isLoading || environmentsQuery.isLoading}
|
||||||
title="Results"
|
title="Results"
|
||||||
titleIcon={List}
|
titleIcon={List}
|
||||||
settingsManager={tableState}
|
settingsManager={tableState}
|
||||||
extendTableOptions={withMeta({
|
extendTableOptions={withMeta({
|
||||||
table: 'edge-job-results',
|
table: 'edge-job-results',
|
||||||
collectLogs: handleCollectLogs,
|
jobId,
|
||||||
downloadLogs: onDownloadLogs,
|
|
||||||
clearLogs: onClearLogs,
|
|
||||||
})}
|
})}
|
||||||
data-cy="edge-job-results-datatable"
|
data-cy="edge-job-results-datatable"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
function handleCollectLogs(envId: EnvironmentId) {
|
|
||||||
onCollectLogs(envId);
|
function associateEndpointsToResults(
|
||||||
}
|
results: Array<JobResult>,
|
||||||
|
environments: Array<Environment>
|
||||||
|
) {
|
||||||
|
return results.map((result) => {
|
||||||
|
const environment = environments.find(
|
||||||
|
(environment) => environment.Id === result.EndpointId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
Endpoint: environment,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@ import { CellContext, createColumnHelper } from '@tanstack/react-table';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
|
|
||||||
import { LogsStatus } from '../../types';
|
import { LogsStatus } from '../../types';
|
||||||
|
import { useDownloadLogsMutation } from '../../queries/jobResults/useDownloadLogsMutation';
|
||||||
|
import { useClearLogsMutation } from '../../queries/jobResults/useClearLogsMutation';
|
||||||
|
import { useCollectLogsMutation } from '../../queries/jobResults/useCollectLogsMutation';
|
||||||
|
|
||||||
import { DecoratedJobResult, getTableMeta } from './types';
|
import { DecoratedJobResult, getTableMeta } from './types';
|
||||||
|
|
||||||
|
@ -29,6 +32,11 @@ function ActionsCell({
|
||||||
table,
|
table,
|
||||||
}: CellContext<DecoratedJobResult, unknown>) {
|
}: CellContext<DecoratedJobResult, unknown>) {
|
||||||
const tableMeta = getTableMeta(table.options.meta);
|
const tableMeta = getTableMeta(table.options.meta);
|
||||||
|
const id = tableMeta.jobId;
|
||||||
|
|
||||||
|
const downloadLogsMutation = useDownloadLogsMutation(id);
|
||||||
|
const clearLogsMutations = useClearLogsMutation(id);
|
||||||
|
const collectLogsMutation = useCollectLogsMutation(id);
|
||||||
|
|
||||||
switch (item.LogsStatus) {
|
switch (item.LogsStatus) {
|
||||||
case LogsStatus.Pending:
|
case LogsStatus.Pending:
|
||||||
|
@ -42,14 +50,14 @@ function ActionsCell({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => tableMeta.downloadLogs(item.EndpointId)}
|
onClick={() => downloadLogsMutation.mutate(item.EndpointId)}
|
||||||
data-cy={`edge-job-download-logs-${item.Endpoint.Name}`}
|
data-cy={`edge-job-download-logs-${item.Endpoint?.Name}`}
|
||||||
>
|
>
|
||||||
Download logs
|
Download logs
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => tableMeta.clearLogs(item.EndpointId)}
|
onClick={() => clearLogsMutations.mutate(item.EndpointId)}
|
||||||
data-cy={`edge-job-clear-logs-${item.Endpoint.Name}`}
|
data-cy={`edge-job-clear-logs-${item.Endpoint?.Name}`}
|
||||||
>
|
>
|
||||||
Clear logs
|
Clear logs
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -59,8 +67,8 @@ function ActionsCell({
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => tableMeta.collectLogs(item.EndpointId)}
|
onClick={() => collectLogsMutation.mutate(item.EndpointId)}
|
||||||
data-cy={`edge-job-retrieve-logs-${item.Endpoint.Name}`}
|
data-cy={`edge-job-retrieve-logs-${item.Endpoint?.Name}`}
|
||||||
>
|
>
|
||||||
Retrieve logs
|
Retrieve logs
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,19 +1,14 @@
|
||||||
import {
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
Environment,
|
|
||||||
EnvironmentId,
|
|
||||||
} from '@/react/portainer/environments/types';
|
|
||||||
|
|
||||||
import { JobResult } from '../../types';
|
import { EdgeJob, JobResult } from '../../types';
|
||||||
|
|
||||||
export interface DecoratedJobResult extends JobResult {
|
export interface DecoratedJobResult extends JobResult {
|
||||||
Endpoint: Environment;
|
Endpoint?: Environment;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableMeta {
|
interface TableMeta {
|
||||||
table: 'edge-job-results';
|
table: 'edge-job-results';
|
||||||
collectLogs(envId: EnvironmentId): void;
|
jobId: EdgeJob['Id'];
|
||||||
downloadLogs(envId: EnvironmentId): void;
|
|
||||||
clearLogs(envId: EnvironmentId): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTableMeta(meta: unknown): meta is TableMeta {
|
function isTableMeta(meta: unknown): meta is TableMeta {
|
||||||
|
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { Form, Formik, useFormikContext } from 'formik';
|
||||||
|
import { useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
|
import { EdgeGroupsSelector } from '@/react/edge/edge-stacks/components/EdgeGroupsSelector';
|
||||||
|
import { AssociatedEdgeEnvironmentsSelector } from '@/react/edge/components/AssociatedEdgeEnvironmentsSelector';
|
||||||
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
|
import { FormActions } from '@@/form-components/FormActions';
|
||||||
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
import { WebEditorForm } from '@@/WebEditorForm';
|
||||||
|
|
||||||
|
import { NameField } from '../../components/EdgeJobForm/NameField';
|
||||||
|
import { JobConfigurationFieldset } from '../../components/EdgeJobForm/JobConfigurationFieldset';
|
||||||
|
import {
|
||||||
|
UpdatePayload,
|
||||||
|
useUpdateEdgeJobMutation,
|
||||||
|
} from '../../queries/useUpdateEdgeJobMutation';
|
||||||
|
import {
|
||||||
|
toRecurringRequest,
|
||||||
|
toRecurringViewModel,
|
||||||
|
} from '../../components/EdgeJobForm/parseRecurringValues';
|
||||||
|
import { EdgeJobResponse } from '../../queries/useEdgeJob';
|
||||||
|
import { useEdgeJobFile } from '../../queries/useEdgeJobFile';
|
||||||
|
import { useValidation } from '../useValidation';
|
||||||
|
|
||||||
|
import { FormValues } from './types';
|
||||||
|
|
||||||
|
export function UpdateEdgeJobForm({ edgeJob }: { edgeJob: EdgeJobResponse }) {
|
||||||
|
const fileQuery = useEdgeJobFile(edgeJob.Id);
|
||||||
|
const mutation = useUpdateEdgeJobMutation();
|
||||||
|
const validation = useValidation({ id: edgeJob.Id });
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
if (!fileQuery.isSuccess) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik<FormValues>
|
||||||
|
validationSchema={validation}
|
||||||
|
validateOnMount
|
||||||
|
initialValues={{
|
||||||
|
name: edgeJob.Name,
|
||||||
|
|
||||||
|
edgeGroupIds: edgeJob.EdgeGroups || [],
|
||||||
|
environmentIds: edgeJob.Endpoints || [],
|
||||||
|
fileContent: fileQuery.data,
|
||||||
|
...toRecurringViewModel({
|
||||||
|
cronExpression: edgeJob.CronExpression,
|
||||||
|
recurring: edgeJob.Recurring,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
onSubmit={(values) => {
|
||||||
|
mutation.mutate(
|
||||||
|
{ id: edgeJob.Id, payload: getPayload(values) },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notifySuccess('Success', 'Edge job successfully updated');
|
||||||
|
router.stateService.go('^');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InnerForm isLoading={mutation.isLoading} />
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InnerForm({ isLoading }: { isLoading: boolean }) {
|
||||||
|
const { values, setFieldValue, isValid, errors } =
|
||||||
|
useFormikContext<FormValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form className="form-horizontal">
|
||||||
|
<NameField errors={errors.name} />
|
||||||
|
|
||||||
|
<JobConfigurationFieldset />
|
||||||
|
|
||||||
|
<WebEditorForm
|
||||||
|
data-cy="edge-job-editor"
|
||||||
|
id="edge-job-editor"
|
||||||
|
onChange={(value) => setFieldValue('fileContent', value)}
|
||||||
|
value={values.fileContent}
|
||||||
|
placeholder="Define or paste the content of your script file here"
|
||||||
|
shell
|
||||||
|
error={errors.fileContent}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EdgeGroupsSelector
|
||||||
|
onChange={(value) => setFieldValue('edgeGroupIds', value)}
|
||||||
|
value={values.edgeGroupIds}
|
||||||
|
error={errors.edgeGroupIds}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormSection title="Target environments">
|
||||||
|
<AssociatedEdgeEnvironmentsSelector
|
||||||
|
onChange={(value) => setFieldValue('environmentIds', value)}
|
||||||
|
value={values.environmentIds}
|
||||||
|
/>
|
||||||
|
</FormSection>
|
||||||
|
|
||||||
|
<FormActions
|
||||||
|
submitLabel="Update edge job"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isValid={isValid}
|
||||||
|
data-cy="updateJobButton"
|
||||||
|
loadingText="In progress..."
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPayload(values: FormValues): UpdatePayload {
|
||||||
|
return {
|
||||||
|
name: values.name,
|
||||||
|
edgeGroups: values.edgeGroupIds,
|
||||||
|
endpoints: values.environmentIds,
|
||||||
|
fileContent: values.fileContent,
|
||||||
|
...toRecurringRequest(values),
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { UpdatePayload } from '../../queries/useUpdateEdgeJobMutation';
|
||||||
|
import { toRecurringRequest } from '../../components/EdgeJobForm/parseRecurringValues';
|
||||||
|
|
||||||
|
import { FormValues } from './types';
|
||||||
|
|
||||||
|
export function getPayload(values: FormValues): UpdatePayload {
|
||||||
|
return {
|
||||||
|
name: values.name,
|
||||||
|
edgeGroups: values.edgeGroupIds,
|
||||||
|
endpoints: values.environmentIds,
|
||||||
|
fileContent: values.fileContent,
|
||||||
|
...toRecurringRequest(values),
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { timeOptions } from '../../components/EdgeJobForm/RecurringFieldset';
|
||||||
|
|
||||||
|
export interface FormValues {
|
||||||
|
name: string;
|
||||||
|
recurring: boolean;
|
||||||
|
edgeGroupIds: Array<EdgeGroup['Id']>;
|
||||||
|
environmentIds: Array<EnvironmentId>;
|
||||||
|
|
||||||
|
fileContent: string;
|
||||||
|
|
||||||
|
cronMethod: 'basic' | 'advanced';
|
||||||
|
dateTime: Date; // basic !recurring
|
||||||
|
recurringOption: (typeof timeOptions)[number]['value']; // basic recurring
|
||||||
|
cronExpression: string; // advanced
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import {
|
||||||
|
SchemaOf,
|
||||||
|
array,
|
||||||
|
boolean,
|
||||||
|
date,
|
||||||
|
mixed,
|
||||||
|
number,
|
||||||
|
object,
|
||||||
|
string,
|
||||||
|
} from 'yup';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { EdgeJob } from '../types';
|
||||||
|
import { useNameValidation } from '../components/EdgeJobForm/NameField';
|
||||||
|
import { cronValidation } from '../components/EdgeJobForm/AdvancedCronFieldset';
|
||||||
|
import { timeOptions } from '../components/EdgeJobForm/RecurringFieldset';
|
||||||
|
|
||||||
|
import { FormValues } from './UpdateEdgeJobForm/types';
|
||||||
|
|
||||||
|
export function useValidation({
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: EdgeJob['Id'];
|
||||||
|
}): SchemaOf<FormValues> {
|
||||||
|
const nameValidation = useNameValidation(id);
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
object({
|
||||||
|
name: nameValidation,
|
||||||
|
recurring: boolean().default(false),
|
||||||
|
cronExpression: string().default('').when('cronMethod', {
|
||||||
|
is: 'advanced',
|
||||||
|
then: cronValidation().required(),
|
||||||
|
}),
|
||||||
|
edgeGroupIds: array(number().required()),
|
||||||
|
environmentIds: array(number().required()),
|
||||||
|
|
||||||
|
fileContent: string().required('This field is required.'),
|
||||||
|
|
||||||
|
cronMethod: mixed<'basic' | 'advanced'>()
|
||||||
|
.oneOf(['basic', 'advanced'])
|
||||||
|
.default('basic'),
|
||||||
|
dateTime: date()
|
||||||
|
.default(new Date())
|
||||||
|
.when(['recurring', 'cronMethod'], {
|
||||||
|
is: (recurring: boolean, cronMethod: 'basic' | 'advanced') =>
|
||||||
|
!recurring && cronMethod === 'basic',
|
||||||
|
then: (schema) => schema.required('This field is required.'),
|
||||||
|
}),
|
||||||
|
recurringOption: mixed()
|
||||||
|
.oneOf(timeOptions.map((o) => o.value))
|
||||||
|
.when(['recurring', 'cronMethod'], {
|
||||||
|
is: (recurring: boolean, cronMethod: 'basic' | 'advanced') =>
|
||||||
|
recurring && cronMethod === 'basic',
|
||||||
|
then: (schema) => schema.required('This field is required.'),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
[nameValidation]
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,7 +2,8 @@ import { useFormikContext } from 'formik';
|
||||||
|
|
||||||
import { SwitchField } from '@@/form-components/SwitchField';
|
import { SwitchField } from '@@/form-components/SwitchField';
|
||||||
|
|
||||||
import { FormValues } from './types';
|
import { FormValues } from '../../CreateView/types';
|
||||||
|
|
||||||
import { RecurringFieldset, defaultCronExpression } from './RecurringFieldset';
|
import { RecurringFieldset, defaultCronExpression } from './RecurringFieldset';
|
||||||
import { ScheduledDateFieldset } from './ScheduledDateFieldset';
|
import { ScheduledDateFieldset } from './ScheduledDateFieldset';
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,33 @@
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
|
import { Calendar, Edit } from 'lucide-react';
|
||||||
|
|
||||||
import { FormSection } from '@@/form-components/FormSection';
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
import { BoxSelector } from '@@/BoxSelector';
|
import { BoxSelector, BoxSelectorOption } from '@@/BoxSelector';
|
||||||
|
|
||||||
import { cronMethodOptions } from '../../CreateView/cron-method-options';
|
import { FormValues } from '../../CreateView/types';
|
||||||
|
|
||||||
import { FormValues } from './types';
|
|
||||||
import { AdvancedCronFieldset } from './AdvancedCronFieldset';
|
import { AdvancedCronFieldset } from './AdvancedCronFieldset';
|
||||||
import { BasicCronFieldset } from './BasicCronFieldset';
|
import { BasicCronFieldset } from './BasicCronFieldset';
|
||||||
|
|
||||||
|
export const cronMethodOptions: ReadonlyArray<BoxSelectorOption<string>> = [
|
||||||
|
{
|
||||||
|
id: 'config_basic',
|
||||||
|
value: 'basic',
|
||||||
|
icon: Calendar,
|
||||||
|
iconType: 'badge',
|
||||||
|
label: 'Basic configuration',
|
||||||
|
description: 'Select date from calendar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'config_advanced',
|
||||||
|
value: 'advanced',
|
||||||
|
icon: Edit,
|
||||||
|
iconType: 'badge',
|
||||||
|
label: 'Advanced configuration',
|
||||||
|
description: 'Write your own cron rule',
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
export function JobConfigurationFieldset() {
|
export function JobConfigurationFieldset() {
|
||||||
const { values, setFieldValue } = useFormikContext<FormValues>();
|
const { values, setFieldValue } = useFormikContext<FormValues>();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { addHours, getDate, getHours, getMinutes, getMonth } from 'date-fns';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { defaultCronExpression, timeOptions } from './RecurringFieldset';
|
||||||
|
|
||||||
|
interface RecurringViewModel {
|
||||||
|
cronMethod: 'basic' | 'advanced';
|
||||||
|
cronExpression: string;
|
||||||
|
recurring: boolean;
|
||||||
|
recurringOption: (typeof timeOptions)[number]['value'];
|
||||||
|
dateTime: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecurringRequestModel {
|
||||||
|
recurring: boolean;
|
||||||
|
cronExpression: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRecurringRequest(
|
||||||
|
values: RecurringViewModel
|
||||||
|
): RecurringRequestModel {
|
||||||
|
if (values.cronMethod !== 'basic') {
|
||||||
|
return {
|
||||||
|
recurring: true,
|
||||||
|
cronExpression: values.cronExpression,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.recurring) {
|
||||||
|
return {
|
||||||
|
recurring: true,
|
||||||
|
cronExpression: values.recurringOption,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
recurring: false,
|
||||||
|
cronExpression: dateTimeToCron(values.dateTime),
|
||||||
|
};
|
||||||
|
|
||||||
|
function dateTimeToCron(date: Date) {
|
||||||
|
return [
|
||||||
|
getMinutes(date),
|
||||||
|
getHours(date),
|
||||||
|
getDate(date),
|
||||||
|
getMonth(date) + 1,
|
||||||
|
'*',
|
||||||
|
].join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toRecurringViewModel(
|
||||||
|
{ cronExpression, recurring }: RecurringRequestModel = {
|
||||||
|
cronExpression: defaultCronExpression,
|
||||||
|
recurring: true,
|
||||||
|
}
|
||||||
|
): RecurringViewModel {
|
||||||
|
const defaultTime = addHours(new Date(), 1);
|
||||||
|
const scheduled = timeOptions.find((v) => v.value === cronExpression);
|
||||||
|
|
||||||
|
return {
|
||||||
|
recurring,
|
||||||
|
cronExpression,
|
||||||
|
recurringOption: scheduled?.value || defaultCronExpression,
|
||||||
|
cronMethod: recurring && !scheduled ? 'advanced' : 'basic',
|
||||||
|
dateTime: cronExpression
|
||||||
|
? cronToDateTime(cronExpression, defaultTime)
|
||||||
|
: defaultTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cronToDateTime(cron: string, defaultTime: Date): Date {
|
||||||
|
const strings = cron.split(' ');
|
||||||
|
if (strings.length > 4) {
|
||||||
|
return moment(cron, 'm H D M').toDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultTime;
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { EdgeJob } from '../../types';
|
||||||
|
import { buildUrl as buildEdgeJobUrl } from '../build-url';
|
||||||
|
|
||||||
|
export function buildUrl({
|
||||||
|
action,
|
||||||
|
id,
|
||||||
|
taskId,
|
||||||
|
}: {
|
||||||
|
id: EdgeJob['Id'];
|
||||||
|
action?: 'logs';
|
||||||
|
taskId?: EnvironmentId;
|
||||||
|
}) {
|
||||||
|
const baseUrl = buildEdgeJobUrl({ id, action: 'tasks' });
|
||||||
|
|
||||||
|
if (taskId) {
|
||||||
|
return `${baseUrl}/${taskId}/${action}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseUrl;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { EdgeJob } from '../../types';
|
||||||
|
import { queryKeys as edgeJobQueryKeys } from '../query-keys';
|
||||||
|
|
||||||
|
export const queryKeys = {
|
||||||
|
base: (id: EdgeJob['Id']) =>
|
||||||
|
[...edgeJobQueryKeys.item(id), 'results'] as const,
|
||||||
|
};
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { withInvalidate } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { EdgeJob } from '../../types';
|
||||||
|
|
||||||
|
import { buildUrl } from './build-url';
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
export function useClearLogsMutation(id: EdgeJob['Id']) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (environmentId: EnvironmentId) =>
|
||||||
|
clearLogsMutation(id, environmentId),
|
||||||
|
...withInvalidate(queryClient, [queryKeys.base(id)]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearLogsMutation(
|
||||||
|
id: EdgeJob['Id'],
|
||||||
|
environmentId: EnvironmentId
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await axios.delete(buildUrl({ id, action: 'logs', taskId: environmentId }));
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err, 'Failed clearing edge job result logs');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { withInvalidate } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { EdgeJob } from '../../types';
|
||||||
|
|
||||||
|
import { buildUrl } from './build-url';
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
export function useCollectLogsMutation(id: EdgeJob['Id']) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (environmentId: EnvironmentId) =>
|
||||||
|
collectLogsMutation(id, environmentId),
|
||||||
|
...withInvalidate(queryClient, [queryKeys.base(id)]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectLogsMutation(
|
||||||
|
id: EdgeJob['Id'],
|
||||||
|
environmentId: EnvironmentId
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await axios.post(buildUrl({ id, action: 'logs', taskId: environmentId }));
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err, 'Unable to collect logs');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { withInvalidate } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { EdgeJob } from '../../types';
|
||||||
|
|
||||||
|
import { buildUrl } from './build-url';
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
export function useDownloadLogsMutation(id: EdgeJob['Id']) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (environmentId: EnvironmentId) =>
|
||||||
|
downloadLogsMutation(id, environmentId),
|
||||||
|
...withInvalidate(queryClient, [queryKeys.base(id)]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadLogsMutation(
|
||||||
|
id: EdgeJob['Id'],
|
||||||
|
environmentId: EnvironmentId
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<{ FileContent: string }>(
|
||||||
|
buildUrl({ id, action: 'logs', taskId: environmentId })
|
||||||
|
);
|
||||||
|
const downloadData = new Blob([data.FileContent], {
|
||||||
|
type: 'text/plain;charset=utf-8',
|
||||||
|
});
|
||||||
|
const logFileName = `job_${id}_task_${environmentId}.log`;
|
||||||
|
saveAs(downloadData, logFileName);
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err, 'Unable to download file');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
|
||||||
|
import { EdgeJob, JobResult } from '../../types';
|
||||||
|
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
import { buildUrl } from './build-url';
|
||||||
|
|
||||||
|
export function useJobResults(
|
||||||
|
id: EdgeJob['Id'],
|
||||||
|
{
|
||||||
|
refetchInterval,
|
||||||
|
}: {
|
||||||
|
refetchInterval?:
|
||||||
|
| number
|
||||||
|
| false
|
||||||
|
| ((data: Array<JobResult> | undefined) => number | false);
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: queryKeys.base(id),
|
||||||
|
queryFn: () => getJobResults(id),
|
||||||
|
refetchInterval,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJobResults(id: EdgeJob['Id']) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<Array<JobResult>>(buildUrl({ id }));
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err, 'Failed fetching edge job results');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,7 @@
|
||||||
|
import { EdgeJob } from '../types';
|
||||||
|
|
||||||
export const queryKeys = {
|
export const queryKeys = {
|
||||||
base: () => ['edge', 'jobs'] as const,
|
base: () => ['edge', 'jobs'] as const,
|
||||||
|
item: (id: EdgeJob['Id']) => [...queryKeys.base(), id] as const,
|
||||||
|
file: (id: EdgeJob['Id']) => [...queryKeys.item(id), 'file'] as const,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { withInvalidate } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { queryKeys } from '../query-keys';
|
||||||
|
|
||||||
import { createJobFromFile } from './createJobFromFile';
|
import { createJobFromFile } from './createJobFromFile';
|
||||||
import { createJobFromFileContent } from './createJobFromFileContent';
|
import { createJobFromFileContent } from './createJobFromFileContent';
|
||||||
|
|
||||||
export function useCreateEdgeJobMutation() {
|
export function useCreateEdgeJobMutation() {
|
||||||
return useMutation(createEdgeJob);
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: createEdgeJob,
|
||||||
|
...withInvalidate(queryClient, [queryKeys.base()]),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BasePayload = {
|
export type BasePayload = {
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { EdgeJob } from '../types';
|
||||||
|
|
||||||
|
import { buildUrl } from './build-url';
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
export interface EdgeJobResponse extends Omit<EdgeJob, 'Endpoints'> {
|
||||||
|
Endpoints: Array<EnvironmentId> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEdgeJob(id: EdgeJobResponse['Id']) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<EdgeJobResponse>(buildUrl({ id }));
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err, 'Failed fetching edge job');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEdgeJob<T = EdgeJobResponse>(
|
||||||
|
id: EdgeJobResponse['Id'],
|
||||||
|
{
|
||||||
|
select,
|
||||||
|
}: {
|
||||||
|
select?: (job: EdgeJobResponse) => T;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
return useQuery(queryKeys.item(id), () => getEdgeJob(id), { select });
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
import { EdgeJob } from '../types';
|
||||||
|
|
||||||
|
import { buildUrl } from './build-url';
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
export interface EdgeJobResponse extends Omit<EdgeJob, 'Endpoints'> {
|
||||||
|
Endpoints: Array<EnvironmentId>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEdgeJobFile(id: EdgeJobResponse['Id']) {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get<{ FileContent: string }>(
|
||||||
|
buildUrl({ id, action: 'file' })
|
||||||
|
);
|
||||||
|
return data.FileContent;
|
||||||
|
} catch (err) {
|
||||||
|
throw parseAxiosError(err, 'Failed fetching edge job file');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useEdgeJobFile(id: EdgeJobResponse['Id']) {
|
||||||
|
return useQuery(queryKeys.file(id), () => getEdgeJobFile(id));
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
import { withInvalidate } from '@/react-tools/react-query';
|
||||||
|
|
||||||
|
import { EdgeGroup } from '../../edge-groups/types';
|
||||||
|
|
||||||
|
import { queryKeys } from './query-keys';
|
||||||
|
|
||||||
|
export interface UpdatePayload {
|
||||||
|
name?: string;
|
||||||
|
cronExpression?: string;
|
||||||
|
recurring?: boolean;
|
||||||
|
endpoints?: Array<EnvironmentId>;
|
||||||
|
edgeGroups?: Array<EdgeGroup['Id']>;
|
||||||
|
fileContent?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateEdgeJobMutation() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: updateEdgeJob,
|
||||||
|
...withInvalidate(queryClient, [queryKeys.base()]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEdgeJob({
|
||||||
|
id,
|
||||||
|
payload,
|
||||||
|
}: {
|
||||||
|
id: number;
|
||||||
|
payload: UpdatePayload;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
await axios.put(`/edge_jobs/${id}`, payload);
|
||||||
|
} catch (error) {
|
||||||
|
throw parseAxiosError(error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ export interface EdgeJob {
|
||||||
Created: number;
|
Created: number;
|
||||||
CronExpression: string;
|
CronExpression: string;
|
||||||
Endpoints: Record<EnvironmentId, EndpointMeta>;
|
Endpoints: Record<EnvironmentId, EndpointMeta>;
|
||||||
EdgeGroups: number[];
|
EdgeGroups: number[] | null;
|
||||||
Name: string;
|
Name: string;
|
||||||
ScriptPath: string;
|
ScriptPath: string;
|
||||||
Recurring: boolean;
|
Recurring: boolean;
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function useParamState<T>(
|
||||||
return [
|
return [
|
||||||
state,
|
state,
|
||||||
(value?: T) => {
|
(value?: T) => {
|
||||||
router.stateService.go('.', { [param]: value }, {});
|
router.stateService.go('.', { [param]: value }, { reload: false });
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue