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)
|
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) {
|
func GetTemplateSchedules(w http.ResponseWriter, r *http.Request) {
|
||||||
project := context.Get(r, "project").(db.Project)
|
project := context.Get(r, "project").(db.Project)
|
||||||
templateID, err := helpers.GetIntParam("template_id", w, r)
|
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.GetTemplates).Methods("GET", "HEAD")
|
||||||
projectUserAPI.Path("/templates").HandlerFunc(projects.AddTemplate).Methods("POST")
|
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").HandlerFunc(projects.AddSchedule).Methods("POST")
|
||||||
projectUserAPI.Path("/schedules/validate").HandlerFunc(projects.ValidateScheduleCronFormat).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"`
|
RepositoryID *int `db:"repository_id" json:"repository_id"`
|
||||||
LastCommitHash *string `db:"last_commit_hash" json:"-"`
|
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
|
DeleteTemplate(projectID int, templateID int) error
|
||||||
|
|
||||||
GetSchedules() ([]Schedule, error)
|
GetSchedules() ([]Schedule, error)
|
||||||
|
GetProjectSchedules(projectID int) ([]ScheduleWithTpl, error)
|
||||||
GetTemplateSchedules(projectID int, templateID int) ([]Schedule, error)
|
GetTemplateSchedules(projectID int, templateID int) ([]Schedule, error)
|
||||||
CreateSchedule(schedule Schedule) (Schedule, error)
|
CreateSchedule(schedule Schedule) (Schedule, error)
|
||||||
UpdateSchedule(schedule Schedule) error
|
UpdateSchedule(schedule Schedule) error
|
||||||
|
@ -16,7 +16,7 @@ func (d *BoltDb) GetSchedules() (schedules []db.Schedule, err error) {
|
|||||||
|
|
||||||
for _, proj := range allProjects {
|
for _, proj := range allProjects {
|
||||||
var projSchedules []db.Schedule
|
var projSchedules []db.Schedule
|
||||||
projSchedules, err = d.GetProjectSchedules(proj.ID)
|
projSchedules, err = d.getProjectSchedules(proj.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -26,15 +26,23 @@ func (d *BoltDb) GetSchedules() (schedules []db.Schedule, err error) {
|
|||||||
return
|
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)
|
err = d.getObjects(projectID, db.ScheduleProps, db.RetrieveQueryParams{}, nil, &schedules)
|
||||||
return
|
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) {
|
func (d *BoltDb) GetTemplateSchedules(projectID int, templateID int) (schedules []db.Schedule, err error) {
|
||||||
schedules = make([]db.Schedule, 0)
|
schedules = make([]db.Schedule, 0)
|
||||||
|
|
||||||
projSchedules, err := d.GetProjectSchedules(projectID)
|
projSchedules, err := d.getProjectSchedules(projectID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,15 @@ func (d *SqlDb) GetSchedules() (schedules []db.Schedule, err error) {
|
|||||||
return
|
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) {
|
func (d *SqlDb) GetTemplateSchedules(projectID int, templateID int) (schedules []db.Schedule, err error) {
|
||||||
_, err = d.selectAll(&schedules,
|
_, err = d.selectAll(&schedules,
|
||||||
"select * from project__schedule where project_id=? and template_id=?",
|
"select * from project__schedule where project_id=? and template_id=?",
|
||||||
|
66289
web/package-lock.json
generated
66289
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -13,6 +13,7 @@
|
|||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"axios": "^0.28.0",
|
"axios": "^0.28.0",
|
||||||
"core-js": "^3.23.2",
|
"core-js": "^3.23.2",
|
||||||
|
"cron-parser": "^4.9.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"vue": "^2.6.14",
|
"vue": "^2.6.14",
|
||||||
"vue-codemirror": "^4.0.6",
|
"vue-codemirror": "^4.0.6",
|
||||||
|
@ -216,6 +216,20 @@
|
|||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</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-list-item
|
||||||
v-if="project.type === ''"
|
v-if="project.type === ''"
|
||||||
key="inventory"
|
key="inventory"
|
||||||
@ -547,7 +561,7 @@
|
|||||||
.v-data-table-header {
|
.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;
|
text-transform: uppercase;
|
||||||
white-space: nowrap;
|
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
|
dense
|
||||||
></v-select>
|
></v-select>
|
||||||
|
|
||||||
<v-row>
|
<v-checkbox
|
||||||
<v-col cols="5" class="pr-1">
|
class="mt-0"
|
||||||
<v-text-field
|
:label="$t('iWantToRunATaskByTheCronOnlyForForNewCommitsOfSome')"
|
||||||
style="font-size: 14px"
|
v-model="cronRepositoryIdVisible"
|
||||||
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-row v-if="cronRepositoryIdVisible" class="mb-2">
|
||||||
<v-col cols="7">
|
<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
|
<v-select
|
||||||
style="font-size: 14px"
|
style="font-size: 14px"
|
||||||
v-if="cronRepositoryIdVisible || cronRepositoryId != null"
|
|
||||||
v-model="cronRepositoryId"
|
v-model="cronRepositoryId"
|
||||||
:label="$t('repository2')"
|
:label="$t('repository2')"
|
||||||
:placeholder="$t('cronChecksNewCommitBeforeRun')"
|
:placeholder="$t('cronChecksNewCommitBeforeRun')"
|
||||||
@ -278,16 +260,24 @@
|
|||||||
dense
|
dense
|
||||||
hide-details
|
hide-details
|
||||||
></v-select>
|
></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-col>
|
||||||
</v-row>
|
</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
|
<v-checkbox
|
||||||
class="mt-0"
|
class="mt-0"
|
||||||
:label="$t('suppressSuccessAlerts')"
|
:label="$t('suppressSuccessAlerts')"
|
||||||
@ -356,6 +346,22 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
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,
|
itemTypeIndex: 0,
|
||||||
TEMPLATE_TYPE_ICONS,
|
TEMPLATE_TYPE_ICONS,
|
||||||
TEMPLATE_TYPE_TITLES,
|
TEMPLATE_TYPE_TITLES,
|
||||||
@ -517,9 +523,13 @@ export default {
|
|||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
})).data;
|
})).data;
|
||||||
|
|
||||||
if (this.schedules.length === 1) {
|
if (this.schedules.length > 0) {
|
||||||
this.cronFormat = this.schedules[0].cron_format;
|
const schedule = this.schedules.find((s) => s.repository_id != null);
|
||||||
this.cronRepositoryId = this.schedules[0].repository_id;
|
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);
|
this.itemTypeIndex = Object.keys(TEMPLATE_TYPE_ICONS).indexOf(this.item.type);
|
||||||
@ -566,7 +576,7 @@ export default {
|
|||||||
}
|
}
|
||||||
} else if (this.schedules.length > 1) {
|
} else if (this.schedules.length > 1) {
|
||||||
// do nothing
|
// do nothing
|
||||||
} else if (this.cronFormat == null || this.cronFormat === '') {
|
} else if (this.cronFormat == null || this.cronFormat === '' || !this.cronRepositoryIdVisible) {
|
||||||
// drop schedule
|
// drop schedule
|
||||||
await axios({
|
await axios({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
|
import Schedule from '../views/project/Schedule.vue';
|
||||||
import History from '../views/project/History.vue';
|
import History from '../views/project/History.vue';
|
||||||
import Activity from '../views/project/Activity.vue';
|
import Activity from '../views/project/Activity.vue';
|
||||||
import Settings from '../views/project/Settings.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 Auth from '../views/Auth.vue';
|
||||||
import New from '../views/project/New.vue';
|
import New from '../views/project/New.vue';
|
||||||
import Integrations from '../views/project/Integrations.vue';
|
import Integrations from '../views/project/Integrations.vue';
|
||||||
|
|
||||||
import IntegrationExtractor from '../views/project/IntegrationExtractor.vue';
|
import IntegrationExtractor from '../views/project/IntegrationExtractor.vue';
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
Vue.use(VueRouter);
|
||||||
@ -36,6 +36,10 @@ const routes = [
|
|||||||
path: '/project/:projectId/activity',
|
path: '/project/:projectId/activity',
|
||||||
component: Activity,
|
component: Activity,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/project/:projectId/schedule',
|
||||||
|
component: Schedule,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/project/:projectId/settings',
|
path: '/project/:projectId/settings',
|
||||||
component: 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