mirror of
https://github.com/semaphoreui/semaphore.git
synced 2024-11-23 20:35:24 +01:00
commit
6b945e8c4d
@ -42,6 +42,17 @@ func GetSchedule(w http.ResponseWriter, r *http.Request) {
|
||||
helpers.WriteJSON(w, http.StatusOK, schedule)
|
||||
}
|
||||
|
||||
func GetProjectSchedules(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
|
||||
tplSchedules, err := helpers.Store(r).GetProjectSchedules(project.ID)
|
||||
if err != nil {
|
||||
helpers.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
helpers.WriteJSON(w, http.StatusOK, tplSchedules)
|
||||
}
|
||||
func GetTemplateSchedules(w http.ResponseWriter, r *http.Request) {
|
||||
project := context.Get(r, "project").(db.Project)
|
||||
templateID, err := helpers.GetIntParam("template_id", w, r)
|
||||
|
@ -178,6 +178,7 @@ func Route() *mux.Router {
|
||||
projectUserAPI.Path("/templates").HandlerFunc(projects.GetTemplates).Methods("GET", "HEAD")
|
||||
projectUserAPI.Path("/templates").HandlerFunc(projects.AddTemplate).Methods("POST")
|
||||
|
||||
projectUserAPI.Path("/schedules").HandlerFunc(projects.GetProjectSchedules).Methods("GET", "HEAD")
|
||||
projectUserAPI.Path("/schedules").HandlerFunc(projects.AddSchedule).Methods("POST")
|
||||
projectUserAPI.Path("/schedules/validate").HandlerFunc(projects.ValidateScheduleCronFormat).Methods("POST")
|
||||
|
||||
|
@ -8,3 +8,8 @@ type Schedule struct {
|
||||
RepositoryID *int `db:"repository_id" json:"repository_id"`
|
||||
LastCommitHash *string `db:"last_commit_hash" json:"-"`
|
||||
}
|
||||
|
||||
type ScheduleWithTpl struct {
|
||||
Schedule
|
||||
TemplateName string `db:"tpl_name" json:"tpl_name"`
|
||||
}
|
||||
|
@ -197,6 +197,7 @@ type Store interface {
|
||||
DeleteTemplate(projectID int, templateID int) error
|
||||
|
||||
GetSchedules() ([]Schedule, error)
|
||||
GetProjectSchedules(projectID int) ([]ScheduleWithTpl, error)
|
||||
GetTemplateSchedules(projectID int, templateID int) ([]Schedule, error)
|
||||
CreateSchedule(schedule Schedule) (Schedule, error)
|
||||
UpdateSchedule(schedule Schedule) error
|
||||
|
@ -16,7 +16,7 @@ func (d *BoltDb) GetSchedules() (schedules []db.Schedule, err error) {
|
||||
|
||||
for _, proj := range allProjects {
|
||||
var projSchedules []db.Schedule
|
||||
projSchedules, err = d.GetProjectSchedules(proj.ID)
|
||||
projSchedules, err = d.getProjectSchedules(proj.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@ -26,15 +26,23 @@ func (d *BoltDb) GetSchedules() (schedules []db.Schedule, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetProjectSchedules(projectID int) (schedules []db.Schedule, err error) {
|
||||
func (d *BoltDb) getProjectSchedules(projectID int) (schedules []db.Schedule, err error) {
|
||||
err = d.getObjects(projectID, db.ScheduleProps, db.RetrieveQueryParams{}, nil, &schedules)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetProjectSchedules(projectID int) (schedules []db.ScheduleWithTpl, err error) {
|
||||
err = d.getObjects(projectID, db.ScheduleProps, db.RetrieveQueryParams{}, func(referringObj interface{}) bool {
|
||||
s := referringObj.(db.ScheduleWithTpl)
|
||||
return s.RepositoryID == nil
|
||||
}, &schedules)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *BoltDb) GetTemplateSchedules(projectID int, templateID int) (schedules []db.Schedule, err error) {
|
||||
schedules = make([]db.Schedule, 0)
|
||||
|
||||
projSchedules, err := d.GetProjectSchedules(projectID)
|
||||
projSchedules, err := d.getProjectSchedules(projectID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -72,6 +72,15 @@ func (d *SqlDb) GetSchedules() (schedules []db.Schedule, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetProjectSchedules(projectID int) (schedules []db.ScheduleWithTpl, err error) {
|
||||
_, err = d.selectAll(&schedules,
|
||||
"SELECT ps.*, pt.name as tpl_name FROM project__schedule ps "+
|
||||
"JOIN project__template pt ON pt.id = ps.template_id "+
|
||||
"WHERE ps.repository_id IS NULL AND ps.project_id=?",
|
||||
projectID)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *SqlDb) GetTemplateSchedules(projectID int, templateID int) (schedules []db.Schedule, err error) {
|
||||
_, err = d.selectAll(&schedules,
|
||||
"select * from project__schedule where project_id=? and template_id=?",
|
||||
|
33
web/package-lock.json
generated
33
web/package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^0.28.0",
|
||||
"core-js": "^3.23.2",
|
||||
"cron-parser": "^4.9.0",
|
||||
"moment": "^2.29.4",
|
||||
"vue": "^2.6.14",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
@ -5582,6 +5583,17 @@
|
||||
"sha.js": "^2.4.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
|
||||
"dependencies": {
|
||||
"luxon": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@ -10423,6 +10435,14 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
@ -23003,6 +23023,14 @@
|
||||
"sha.js": "^2.4.8"
|
||||
}
|
||||
},
|
||||
"cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
|
||||
"requires": {
|
||||
"luxon": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@ -26734,6 +26762,11 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"luxon": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA=="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
|
@ -13,6 +13,7 @@
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^0.28.0",
|
||||
"core-js": "^3.23.2",
|
||||
"cron-parser": "^4.9.0",
|
||||
"moment": "^2.29.4",
|
||||
"vue": "^2.6.14",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
|
@ -216,6 +216,20 @@
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="project.type === ''"
|
||||
key="schedule"
|
||||
:to="`/project/${projectId}/schedule`"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-clock-outline</v-icon>
|
||||
</v-list-item-icon>
|
||||
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $t('Schedule') }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item
|
||||
v-if="project.type === ''"
|
||||
key="inventory"
|
||||
@ -547,7 +561,7 @@
|
||||
.v-data-table-header {
|
||||
}
|
||||
|
||||
.theme--light.v-data-table > .v-data-table__wrapper > table > thead > tr:last-child > th {
|
||||
.v-data-table > .v-data-table__wrapper > table > thead > tr:last-child > th {
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
432
web/src/components/ScheduleForm.vue
Normal file
432
web/src/components/ScheduleForm.vue
Normal file
@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<v-form
|
||||
ref="form"
|
||||
lazy-validation
|
||||
v-model="formValid"
|
||||
v-if="templates && item != null"
|
||||
>
|
||||
|
||||
<v-alert
|
||||
:value="formError"
|
||||
color="error"
|
||||
class="pb-2"
|
||||
>{{ formError }}
|
||||
</v-alert>
|
||||
|
||||
<!-- <v-text-field-->
|
||||
<!-- v-model="item.name"-->
|
||||
<!-- :label="$t('Name')"-->
|
||||
<!-- :rules="[v => !!v || $t('name_required')]"-->
|
||||
<!-- required-->
|
||||
<!-- :disabled="formSaving"-->
|
||||
<!-- class="mb-4"-->
|
||||
<!-- ></v-text-field>-->
|
||||
|
||||
<v-select
|
||||
v-model="item.template_id"
|
||||
:label="$t('Template')"
|
||||
:items="templates"
|
||||
item-value="id"
|
||||
:item-text="(itm) => itm.name"
|
||||
:rules="[v => !!v || $t('template_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.cron_format"
|
||||
:label="$t('Cron')"
|
||||
:rules="[v => !!v || $t('Cron required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
@input="refreshCheckboxes()"
|
||||
></v-text-field>
|
||||
|
||||
<div class="mb-4" style="color: limegreen; font-weight: bold;">
|
||||
Next run {{ nextRunTime() | formatDate }}.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<v-select
|
||||
v-model="timing"
|
||||
:label="$t('Timing')"
|
||||
:items="TIMINGS"
|
||||
item-value="id"
|
||||
item-text="title"
|
||||
:rules="[v => !!v || $t('template_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
@change="refreshCron()"
|
||||
/>
|
||||
|
||||
<div v-if="['yearly'].includes(timing)">
|
||||
<div>Months</div>
|
||||
<div class="d-flex flex-wrap">
|
||||
<v-checkbox
|
||||
class="mr-2 mt-0 ScheduleCheckbox"
|
||||
v-for="m in MONTHS" :key="m.id"
|
||||
:value="m.id"
|
||||
:label="m.title"
|
||||
v-model="months"
|
||||
color="white"
|
||||
:class="{'ScheduleCheckbox--active': months.includes(m.id)}"
|
||||
@change="refreshCron()"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="['weekly'].includes(timing)">
|
||||
<div class="mt-4">Weekdays</div>
|
||||
<div class="d-flex flex-wrap">
|
||||
<v-checkbox
|
||||
class="mr-2 mt-0 ScheduleCheckbox"
|
||||
v-for="d in WEEKDAYS" :key="d.id"
|
||||
:value="d.id"
|
||||
:label="d.title"
|
||||
v-model="weekdays"
|
||||
color="white"
|
||||
:class="{'ScheduleCheckbox--active': weekdays.includes(d.id)}"
|
||||
@change="refreshCron()"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="['yearly', 'monthly'].includes(timing)">
|
||||
<div class="mt-4">Days</div>
|
||||
<div class="d-flex flex-wrap">
|
||||
<v-checkbox
|
||||
class="mr-2 mt-0 ScheduleCheckbox"
|
||||
v-for="d in 31"
|
||||
:key="d"
|
||||
:value="d"
|
||||
:label="`${d}`"
|
||||
v-model="days"
|
||||
color="white"
|
||||
:class="{'ScheduleCheckbox--active': days.includes(d)}"
|
||||
@change="refreshCron()"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="['yearly', 'monthly', 'weekly', 'daily'].includes(timing)">
|
||||
<div class="mt-4">Hours</div>
|
||||
<div class="d-flex flex-wrap">
|
||||
<v-checkbox
|
||||
class="mr-2 mt-0 ScheduleCheckbox"
|
||||
v-for="h in 24"
|
||||
:key="h - 1"
|
||||
:value="h - 1"
|
||||
:label="`${h - 1}`"
|
||||
v-model="hours"
|
||||
color="white"
|
||||
:class="{'ScheduleCheckbox--active': hours.includes(h - 1)}"
|
||||
@change="refreshCron()"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="mt-4">Minutes</div>
|
||||
<div class="d-flex flex-wrap">
|
||||
<v-checkbox
|
||||
class="mr-2 mt-0 ScheduleCheckbox"
|
||||
v-for="m in MINUTES"
|
||||
:key="m.id"
|
||||
:value="m.id"
|
||||
:label="m.title"
|
||||
v-model="minutes"
|
||||
color="white"
|
||||
:class="{'ScheduleCheckbox--active': minutes.includes(m.id)}"
|
||||
@change="refreshCron()"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</v-form>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.ScheduleCheckbox {
|
||||
|
||||
.v-input__slot {
|
||||
padding: 4px 6px;
|
||||
font-weight: bold;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.v-messages {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.theme--light {
|
||||
.v-input__slot {
|
||||
background: #e4e4e4;
|
||||
}
|
||||
}
|
||||
|
||||
&.theme--dark {
|
||||
.v-input__slot {
|
||||
background: gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ScheduleCheckbox--active {
|
||||
.v-input__slot {
|
||||
background: #4caf50 !important;
|
||||
}
|
||||
.v-label {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import ItemFormBase from '@/components/ItemFormBase';
|
||||
import axios from 'axios';
|
||||
|
||||
const parser = require('cron-parser');
|
||||
|
||||
const MONTHS = [{
|
||||
id: 1,
|
||||
title: 'Jan',
|
||||
}, {
|
||||
id: 2,
|
||||
title: 'Feb',
|
||||
}, {
|
||||
id: 3,
|
||||
title: 'March',
|
||||
}, {
|
||||
id: 4,
|
||||
title: 'March',
|
||||
}, {
|
||||
id: 5,
|
||||
title: 'March',
|
||||
}, {
|
||||
id: 6,
|
||||
title: 'March',
|
||||
}, {
|
||||
id: 7,
|
||||
title: 'March',
|
||||
}];
|
||||
|
||||
const TIMINGS = [{
|
||||
id: 'yearly',
|
||||
title: 'Yearly',
|
||||
}, {
|
||||
id: 'monthly',
|
||||
title: 'Monthly',
|
||||
}, {
|
||||
id: 'weekly',
|
||||
title: 'Weekly',
|
||||
}, {
|
||||
id: 'daily',
|
||||
title: 'Daily',
|
||||
}, {
|
||||
id: 'hourly',
|
||||
title: 'Hourly',
|
||||
}];
|
||||
|
||||
const WEEKDAYS = [{
|
||||
id: 0,
|
||||
title: 'Sunday',
|
||||
}, {
|
||||
id: 1,
|
||||
title: 'Monday',
|
||||
}, {
|
||||
id: 2,
|
||||
title: 'Tuesday',
|
||||
}, {
|
||||
id: 3,
|
||||
title: 'Wednesday',
|
||||
}, {
|
||||
id: 4,
|
||||
title: 'Thursday',
|
||||
}, {
|
||||
id: 5,
|
||||
title: 'Friday',
|
||||
}, {
|
||||
id: 6,
|
||||
title: 'Saturday',
|
||||
}];
|
||||
|
||||
const MINUTES = [
|
||||
{ id: 0, title: ':00' },
|
||||
{ id: 5, title: ':05' },
|
||||
{ id: 10, title: ':10' },
|
||||
{ id: 15, title: ':15' },
|
||||
{ id: 20, title: ':20' },
|
||||
{ id: 25, title: ':25' },
|
||||
{ id: 30, title: ':30' },
|
||||
{ id: 35, title: ':35' },
|
||||
{ id: 40, title: ':40' },
|
||||
{ id: 45, title: ':45' },
|
||||
{ id: 50, title: ':50' },
|
||||
{ id: 55, title: ':55' },
|
||||
];
|
||||
|
||||
export default {
|
||||
mixins: [ItemFormBase],
|
||||
|
||||
data() {
|
||||
return {
|
||||
templates: null,
|
||||
timing: 'hourly',
|
||||
TIMINGS,
|
||||
MONTHS,
|
||||
WEEKDAYS,
|
||||
MINUTES,
|
||||
minutes: [],
|
||||
hours: [],
|
||||
days: [],
|
||||
months: [],
|
||||
weekdays: [],
|
||||
};
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.templates = (await axios({
|
||||
method: 'get',
|
||||
url: `/api/project/${this.projectId}/templates`,
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
nextRunTime() {
|
||||
return parser.parseExpression(this.item.cron_format).next();
|
||||
},
|
||||
|
||||
refreshCheckboxes() {
|
||||
const fields = JSON.parse(
|
||||
JSON.stringify(parser.parseExpression(this.item.cron_format).fields),
|
||||
);
|
||||
|
||||
if (this.isHourly(this.item.cron_format)) {
|
||||
this.minutes = fields.minute;
|
||||
this.timing = 'hourly';
|
||||
} else {
|
||||
this.minutes = [];
|
||||
}
|
||||
|
||||
if (this.isDaily(this.item.cron_format)) {
|
||||
this.hours = fields.hour;
|
||||
this.timing = 'daily';
|
||||
} else {
|
||||
this.hours = [];
|
||||
}
|
||||
|
||||
if (this.isWeekly(this.item.cron_format)) {
|
||||
this.weekdays = fields.dayOfWeek;
|
||||
this.timing = 'weekly';
|
||||
} else {
|
||||
this.months = [];
|
||||
this.weekdays = [];
|
||||
}
|
||||
|
||||
if (this.isMonthly(this.item.cron_format)) {
|
||||
this.days = fields.dayOfMonth;
|
||||
this.timing = 'monthly';
|
||||
} else {
|
||||
this.months = [];
|
||||
this.weekdays = [];
|
||||
}
|
||||
|
||||
if (this.isYearly(this.item.cron_format)) {
|
||||
this.months = fields.month;
|
||||
this.timing = 'yearly';
|
||||
}
|
||||
},
|
||||
|
||||
afterLoadData() {
|
||||
if (this.isNew) {
|
||||
this.item.cron_format = '* * * * *';
|
||||
}
|
||||
|
||||
this.refreshCheckboxes();
|
||||
},
|
||||
|
||||
isWeekly(s) {
|
||||
return /^\S+\s\S+\s\S+\s\S+\s[^*]\S*$/.test(s);
|
||||
},
|
||||
|
||||
isYearly(s) {
|
||||
return /^\S+\s\S+\s\S+\s[^*]\S*\s\S+$/.test(s);
|
||||
},
|
||||
|
||||
isMonthly(s) {
|
||||
return /^\S+\s\S+\s[^*]\S*\s\S+\s\S+$/.test(s);
|
||||
},
|
||||
|
||||
isDaily(s) {
|
||||
return /^\S+\s[^*]\S*\s\S+\s\S+\s\S+$/.test(s);
|
||||
},
|
||||
|
||||
isHourly(s) {
|
||||
return /^[^*]\S*\s\S+\s\S+\s\S+\s\S+$/.test(s);
|
||||
},
|
||||
|
||||
refreshCron() {
|
||||
const fields = JSON.parse(JSON.stringify(parser.parseExpression('* * * * *').fields));
|
||||
|
||||
switch (this.timing) {
|
||||
case 'hourly':
|
||||
this.months = [];
|
||||
this.weekdays = [];
|
||||
this.days = [];
|
||||
this.hours = [];
|
||||
break;
|
||||
case 'daily':
|
||||
this.days = [];
|
||||
this.months = [];
|
||||
this.weekdays = [];
|
||||
break;
|
||||
case 'monthly':
|
||||
this.months = [];
|
||||
this.weekdays = [];
|
||||
break;
|
||||
case 'weekly':
|
||||
this.months = [];
|
||||
this.days = [];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.months.length > 0) {
|
||||
fields.month = this.months;
|
||||
}
|
||||
|
||||
if (this.weekdays.length > 0) {
|
||||
fields.dayOfWeek = this.weekdays;
|
||||
}
|
||||
|
||||
if (this.days.length > 0) {
|
||||
fields.dayOfMonth = this.days;
|
||||
}
|
||||
|
||||
if (this.hours.length > 0) {
|
||||
fields.hour = this.hours;
|
||||
}
|
||||
|
||||
if (this.minutes.length > 0) {
|
||||
fields.minute = this.minutes;
|
||||
}
|
||||
|
||||
this.item.cron_format = parser.fieldsToExpression(fields).stringify();
|
||||
},
|
||||
|
||||
getItemsUrl() {
|
||||
return `/api/project/${this.projectId}/schedules`;
|
||||
},
|
||||
|
||||
getSingleItemUrl() {
|
||||
return `/api/project/${this.projectId}/schedules/${this.itemId}`;
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<v-form
|
||||
ref="form"
|
||||
lazy-validation
|
||||
v-model="formValid"
|
||||
v-if="item != null"
|
||||
>
|
||||
<v-alert
|
||||
:value="formError"
|
||||
:color="(formError || '').includes('already activated') ? 'warning' : 'error'"
|
||||
>{{ formError }}
|
||||
</v-alert>
|
||||
|
||||
<v-text-field
|
||||
v-model="item.key"
|
||||
label="Subscription Key"
|
||||
:rules="[v => !!v || $t('key_required')]"
|
||||
required
|
||||
:disabled="formSaving"
|
||||
></v-text-field>
|
||||
|
||||
<v-list v-if="item.plan">
|
||||
<v-list-item class="pa-0">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Plan</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.plan }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item class="pa-0">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Expires at</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.expiresAt }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item class="pa-0">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Users</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.users }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-alert color="info" v-else>There is no active subscription.</v-alert>
|
||||
|
||||
</v-form>
|
||||
</template>
|
||||
<script>
|
||||
import ItemFormBase from '@/components/ItemFormBase';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
mixins: [ItemFormBase],
|
||||
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.item = (await axios({
|
||||
method: 'get',
|
||||
url: '/api/subscription',
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
},
|
||||
|
||||
async afterSave() {
|
||||
await this.loadData();
|
||||
},
|
||||
|
||||
getItemsUrl() {
|
||||
return '/api/subscription';
|
||||
},
|
||||
|
||||
getSingleItemUrl() {
|
||||
return '/api/subscription';
|
||||
},
|
||||
|
||||
getRequestOptions() {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/subscription',
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -238,34 +238,16 @@
|
||||
dense
|
||||
></v-select>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="5" class="pr-1">
|
||||
<v-text-field
|
||||
style="font-size: 14px"
|
||||
v-model="cronFormat"
|
||||
:label="$t('cron')"
|
||||
:disabled="formSaving"
|
||||
placeholder="* * * * *"
|
||||
v-if="schedules == null || schedules.length <= 1"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-checkbox
|
||||
class="mt-0"
|
||||
:label="$t('iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome')"
|
||||
v-model="cronRepositoryIdVisible"
|
||||
/>
|
||||
|
||||
<v-row v-if="cronRepositoryIdVisible" class="mb-2">
|
||||
<v-col cols="7">
|
||||
<a
|
||||
v-if="!cronRepositoryIdVisible && cronRepositoryId == null"
|
||||
@click="cronRepositoryIdVisible = true"
|
||||
class="text-caption d-block"
|
||||
style="line-height: 1.1;"
|
||||
>
|
||||
{{ $t('iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome') }}
|
||||
</a>
|
||||
|
||||
<v-select
|
||||
style="font-size: 14px"
|
||||
v-if="cronRepositoryIdVisible || cronRepositoryId != null"
|
||||
v-model="cronRepositoryId"
|
||||
:label="$t('repository2')"
|
||||
:placeholder="$t('cronChecksNewCommitBeforeRun')"
|
||||
@ -278,16 +260,24 @@
|
||||
dense
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="5">
|
||||
<v-select
|
||||
v-model="cronFormat"
|
||||
:label="$t('Interval')"
|
||||
:placeholder="$t('New commit check interval')"
|
||||
item-value="cron"
|
||||
item-text="title"
|
||||
:items="cronFormats"
|
||||
:disabled="formSaving"
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<small class="mt-1 mb-4 d-block">
|
||||
{{ $t('readThe') }}
|
||||
<a target="_blank" href="https://pkg.go.dev/github.com/robfig/cron/v3#hdr-CRON_Expression_Format">{{ $t('docs') }}</a>
|
||||
{{ $t('toLearnMoreAboutCron') }}
|
||||
</small>
|
||||
|
||||
<v-checkbox
|
||||
class="mt-0"
|
||||
:label="$t('suppressSuccessAlerts')"
|
||||
@ -356,6 +346,22 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
cronFormats: [{
|
||||
cron: '* * * * *',
|
||||
title: '1 minute',
|
||||
}, {
|
||||
cron: '*/5 * * * *',
|
||||
title: '5 minutes',
|
||||
}, {
|
||||
cron: '*/10 * * * *',
|
||||
title: '10 minutes',
|
||||
}, {
|
||||
cron: '@hourly',
|
||||
title: '1 hour',
|
||||
}, {
|
||||
cron: '@daily',
|
||||
title: '24 hours',
|
||||
}],
|
||||
itemTypeIndex: 0,
|
||||
TEMPLATE_TYPE_ICONS,
|
||||
TEMPLATE_TYPE_TITLES,
|
||||
@ -517,9 +523,13 @@ export default {
|
||||
responseType: 'json',
|
||||
})).data;
|
||||
|
||||
if (this.schedules.length === 1) {
|
||||
this.cronFormat = this.schedules[0].cron_format;
|
||||
this.cronRepositoryId = this.schedules[0].repository_id;
|
||||
if (this.schedules.length > 0) {
|
||||
const schedule = this.schedules.find((s) => s.repository_id != null);
|
||||
if (schedule != null) {
|
||||
this.cronFormat = schedule.cron_format;
|
||||
this.cronRepositoryId = schedule.repository_id;
|
||||
this.cronRepositoryIdVisible = this.cronRepositoryId != null;
|
||||
}
|
||||
}
|
||||
|
||||
this.itemTypeIndex = Object.keys(TEMPLATE_TYPE_ICONS).indexOf(this.item.type);
|
||||
@ -566,7 +576,7 @@ export default {
|
||||
}
|
||||
} else if (this.schedules.length > 1) {
|
||||
// do nothing
|
||||
} else if (this.cronFormat == null || this.cronFormat === '') {
|
||||
} else if (this.cronFormat == null || this.cronFormat === '' || !this.cronRepositoryIdVisible) {
|
||||
// drop schedule
|
||||
await axios({
|
||||
method: 'delete',
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import Schedule from '../views/project/Schedule.vue';
|
||||
import History from '../views/project/History.vue';
|
||||
import Activity from '../views/project/Activity.vue';
|
||||
import Settings from '../views/project/Settings.vue';
|
||||
@ -14,7 +15,6 @@ import Users from '../views/Users.vue';
|
||||
import Auth from '../views/Auth.vue';
|
||||
import New from '../views/project/New.vue';
|
||||
import Integrations from '../views/project/Integrations.vue';
|
||||
|
||||
import IntegrationExtractor from '../views/project/IntegrationExtractor.vue';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
@ -36,6 +36,10 @@ const routes = [
|
||||
path: '/project/:projectId/activity',
|
||||
component: Activity,
|
||||
},
|
||||
{
|
||||
path: '/project/:projectId/schedule',
|
||||
component: Schedule,
|
||||
},
|
||||
{
|
||||
path: '/project/:projectId/settings',
|
||||
component: Settings,
|
||||
|
141
web/src/views/project/Schedule.vue
Normal file
141
web/src/views/project/Schedule.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
|
||||
<div v-if="items != null">
|
||||
<EditDialog
|
||||
v-model="editDialog"
|
||||
:save-button-text="$t('save')"
|
||||
:title="$t('Edit Schedule')"
|
||||
:max-width="500"
|
||||
@save="loadItems"
|
||||
>
|
||||
<template v-slot:form="{ onSave, onError, needSave, needReset }">
|
||||
<ScheduleForm
|
||||
:project-id="projectId"
|
||||
:item-id="itemId"
|
||||
@save="onSave"
|
||||
@error="onError"
|
||||
:need-save="needSave"
|
||||
:need-reset="needReset"
|
||||
/>
|
||||
</template>
|
||||
</EditDialog>
|
||||
|
||||
<ObjectRefsDialog
|
||||
object-title="schedule"
|
||||
:object-refs="itemRefs"
|
||||
:project-id="projectId"
|
||||
v-model="itemRefsDialog"
|
||||
/>
|
||||
|
||||
<YesNoDialog
|
||||
:title="$t('Delete Schedule')"
|
||||
:text="$t('askDeleteEnv')"
|
||||
v-model="deleteItemDialog"
|
||||
@yes="deleteItem(itemId)"
|
||||
/>
|
||||
|
||||
<v-toolbar flat >
|
||||
<v-app-bar-nav-icon @click="showDrawer()"></v-app-bar-nav-icon>
|
||||
<v-toolbar-title>{{ $t('Schedule') }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="editItem('new')"
|
||||
v-if="can(USER_PERMISSIONS.manageProjectResources)"
|
||||
>{{ $t('New Schedule') }}
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="items"
|
||||
hide-default-footer
|
||||
class="mt-4"
|
||||
:items-per-page="Number.MAX_VALUE"
|
||||
>
|
||||
<template v-slot:item.tpl_name="{ item }">
|
||||
<div class="d-flex">
|
||||
<router-link :to="
|
||||
'/project/' + item.project_id +
|
||||
'/templates/' + item.template_id"
|
||||
>{{ item.tpl_name }}
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div style="white-space: nowrap">
|
||||
<v-btn
|
||||
icon
|
||||
class="mr-1"
|
||||
@click="askDeleteItem(item.id)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
class="mr-1"
|
||||
@click="editItem(item.id)"
|
||||
>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:expanded-item="{ headers, item }">
|
||||
<td
|
||||
:colspan="headers.length"
|
||||
v-if="openedItems.some((template) => template.id === item.id)"
|
||||
>
|
||||
<TaskList
|
||||
style="border: 1px solid lightgray; border-radius: 6px; margin: 10px 0;"
|
||||
:template="item"
|
||||
:limit="5"
|
||||
:hide-footer="true"
|
||||
/>
|
||||
</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script>
|
||||
import ItemListPageBase from '@/components/ItemListPageBase';
|
||||
import ScheduleForm from '@/components/ScheduleForm.vue';
|
||||
import TaskList from '@/components/TaskList.vue';
|
||||
|
||||
export default {
|
||||
components: { TaskList, ScheduleForm },
|
||||
mixins: [ItemListPageBase],
|
||||
data() {
|
||||
return {
|
||||
openedItems: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getHeaders() {
|
||||
return [{
|
||||
text: this.$i18n.t('Cron'),
|
||||
value: 'cron_format',
|
||||
}, {
|
||||
text: this.$i18n.t('Template'),
|
||||
value: 'tpl_name',
|
||||
width: '100%',
|
||||
}, {
|
||||
text: this.$i18n.t('actions'),
|
||||
value: 'actions',
|
||||
sortable: false,
|
||||
}];
|
||||
},
|
||||
getItemsUrl() {
|
||||
return `/api/project/${this.projectId}/schedules`;
|
||||
},
|
||||
getSingleItemUrl() {
|
||||
return `/api/project/${this.projectId}/schedules/${this.itemId}`;
|
||||
},
|
||||
getEventName() {
|
||||
return 'i-schedule';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user