Compare commits
53 Commits
v5.x.all.i
...
v4.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a921cb2d0d | ||
|
|
f3aaa363d8 | ||
|
|
45a79e1920 | ||
|
|
6fd9b2a453 | ||
|
|
01d8e89a71 | ||
|
|
c89fa63910 | ||
|
|
9fc5c49dbf | ||
|
|
7dfc269df9 | ||
|
|
76d0b397db | ||
|
|
5413f887af | ||
|
|
b3d0c61f0e | ||
|
|
4ce0441d68 | ||
|
|
72be34e18d | ||
|
|
d2961b7650 | ||
|
|
fdca1bbf72 | ||
|
|
ab7a2f9dee | ||
|
|
7b72857a3b | ||
|
|
4787146658 | ||
|
|
430f9356c3 | ||
|
|
70a3b3518f | ||
|
|
c0944c17e0 | ||
|
|
da1b2a91e7 | ||
|
|
aa27492713 | ||
|
|
afe589dec3 | ||
|
|
978d140c8f | ||
|
|
2ce213b62c | ||
|
|
7748266078 | ||
|
|
83783d07a1 | ||
|
|
49a1f2c7c5 | ||
|
|
ddfc0151fc | ||
|
|
81c508e13c | ||
|
|
7195cfc3cf | ||
|
|
93fe5e2cf7 | ||
|
|
a2bf795d12 | ||
|
|
c8d78f39e0 | ||
|
|
d9ab8a1c8b | ||
|
|
5125ad4889 | ||
|
|
951e85b04b | ||
|
|
711d922695 | ||
|
|
3692ffcde7 | ||
|
|
b049420c59 | ||
|
|
241103c369 | ||
|
|
2128367113 | ||
|
|
f555c8190d | ||
|
|
d5df633def | ||
|
|
fe7dc859e3 | ||
|
|
569c5046c6 | ||
|
|
e0210ae2d8 | ||
|
|
f85dc3b7e7 | ||
|
|
92d4363120 | ||
|
|
6c69220de2 | ||
|
|
3a1229b072 | ||
|
|
45538c9f62 |
@@ -1,8 +1,10 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
- '0.12'
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
|
||||
before_install:
|
||||
- npm i -g npm
|
||||
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,5 +1,45 @@
|
||||
# ChangeLog
|
||||
|
||||
## **4.10.0** (2015-11-27)
|
||||
|
||||
Job management, email notifications, CoreOS/Docker, Quiesce snapshots...
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Job management ([xo-web#487](https://github.com/vatesfr/xo-web/issues/487))
|
||||
- Patch upload on all connected servers ([xo-web#168](https://github.com/vatesfr/xo-web/issues/168)
|
||||
- Emergency shutdown ([xo-web#185](https://github.com/vatesfr/xo-web/issues/185))
|
||||
- CoreOS/docker template install ([xo-web#246](https://github.com/vatesfr/xo-web/issues/246))
|
||||
- Email for backups ([xo-web#308](https://github.com/vatesfr/xo-web/issues/308))
|
||||
- Console Clipboard ([xo-web#408](https://github.com/vatesfr/xo-web/issues/408))
|
||||
- Logs from CLI ([xo-web#486](https://github.com/vatesfr/xo-web/issues/486))
|
||||
- Save disconnected servers ([xo-web#489](https://github.com/vatesfr/xo-web/issues/489))
|
||||
- Snapshot with quiesce ([xo-web#491](https://github.com/vatesfr/xo-web/issues/491))
|
||||
- Start VM in reovery mode ([xo-web#495](https://github.com/vatesfr/xo-web/issues/495))
|
||||
- Username in logs ([xo-web#498](https://github.com/vatesfr/xo-web/issues/498))
|
||||
- Delete associated tokens with user ([xo-web#500](https://github.com/vatesfr/xo-web/issues/500))
|
||||
- Validate plugin configuration ([xo-web#503](https://github.com/vatesfr/xo-web/issues/503))
|
||||
- Avoid non configured plugins to be loaded ([xo-web#504](https://github.com/vatesfr/xo-web/issues/504))
|
||||
- Verbose API logs if configured ([xo-web#505](https://github.com/vatesfr/xo-web/issues/505))
|
||||
- Better backup overview ([xo-web#512](https://github.com/vatesfr/xo-web/issues/512))
|
||||
- VM auto power on ([xo-web#519](https://github.com/vatesfr/xo-web/issues/519))
|
||||
- Title property supported in config schema ([xo-web#522](https://github.com/vatesfr/xo-web/issues/522))
|
||||
- Start VM export only when necessary ([xo-web#534](https://github.com/vatesfr/xo-web/issues/534))
|
||||
- Input type should be number ([xo-web#538](https://github.com/vatesfr/xo-web/issues/538))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Numbers/int support in plugins config ([xo-web#531](https://github.com/vatesfr/xo-web/issues/531))
|
||||
- Boolean support in plugins config ([xo-web#528](https://github.com/vatesfr/xo-web/issues/528))
|
||||
- Keyboard unusable outside console ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
|
||||
- UsernameField for SAML ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
|
||||
- Wrong display of "no plugin found" ([xo-web#508](https://github.com/vatesfr/xo-web/issues/508))
|
||||
- Bower build error ([xo-web#488](https://github.com/vatesfr/xo-web/issues/488))
|
||||
- VM cloning should require SR permission ([xo-web#472](https://github.com/vatesfr/xo-web/issues/472))
|
||||
- Xen tools status ([xo-web#471](https://github.com/vatesfr/xo-web/issues/471))
|
||||
- Can't delete ghost user ([xo-web#464](https://github.com/vatesfr/xo-web/issues/464))
|
||||
- Stats with old versions of Node ([xo-web#463](https://github.com/vatesfr/xo-web/issues/463))
|
||||
|
||||
## **4.9.0** (2015-11-13)
|
||||
|
||||
Automated DR, restore backup, VM copy
|
||||
|
||||
10
README.md
10
README.md
@@ -38,8 +38,8 @@ Otherwise, please consider using the [bugtracker of the general repository](http
|
||||
## Process for new release
|
||||
|
||||
```bash
|
||||
# Switch to the master branch.
|
||||
git checkout master
|
||||
# Switch to the stable branch.
|
||||
git checkout stable
|
||||
|
||||
# Fetches latest changes.
|
||||
git pull --ff-only
|
||||
@@ -53,12 +53,12 @@ npm version minor
|
||||
# Go back to the next-release branch.
|
||||
git checkout next-release
|
||||
|
||||
# Fetches the last changes (the merge and version bump) from master to
|
||||
# Fetches the last changes (the merge and version bump) from stable to
|
||||
# next-release.
|
||||
git merge --ff-only master
|
||||
git merge --ff-only stable
|
||||
|
||||
# Push the changes on git.
|
||||
git push --follow-tags origin master next-release
|
||||
git push --follow-tags origin stable next-release
|
||||
|
||||
# Publish this release to npm.
|
||||
npm publish
|
||||
|
||||
@@ -28,6 +28,7 @@ import newVmState from './modules/new-vm'
|
||||
import poolState from './modules/pool'
|
||||
import settingsState from './modules/settings'
|
||||
import srState from './modules/sr'
|
||||
import taskScheduler from './modules/task-scheduler'
|
||||
import treeState from './modules/tree'
|
||||
import updater from './modules/updater'
|
||||
import vmState from './modules/vm'
|
||||
@@ -63,6 +64,7 @@ export default angular.module('xoWebApp', [
|
||||
poolState,
|
||||
settingsState,
|
||||
srState,
|
||||
taskScheduler,
|
||||
treeState,
|
||||
updater,
|
||||
vmState,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import forEach from 'lodash.foreach'
|
||||
import indexOf from 'lodash.indexof'
|
||||
import later from 'later'
|
||||
import moment from 'moment'
|
||||
import prettyCron from 'prettycron'
|
||||
import remove from 'lodash.remove'
|
||||
import scheduler from 'scheduler'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
later.date.localTime()
|
||||
@@ -18,7 +13,6 @@ import restore from './restore'
|
||||
import rollingSnapshot from './rolling-snapshot'
|
||||
|
||||
import view from './view'
|
||||
import scheduler from './scheduler'
|
||||
|
||||
export default angular.module('backup', [
|
||||
uiRouter,
|
||||
@@ -28,7 +22,8 @@ export default angular.module('backup', [
|
||||
management,
|
||||
mount,
|
||||
restore,
|
||||
rollingSnapshot
|
||||
rollingSnapshot,
|
||||
scheduler
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('backup', {
|
||||
@@ -49,302 +44,4 @@ export default angular.module('backup', [
|
||||
})
|
||||
})
|
||||
|
||||
.directive('xoScheduler', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: scheduler,
|
||||
controller: 'XoScheduler as ctrl',
|
||||
bindToController: true,
|
||||
scope: {
|
||||
data: '=',
|
||||
api: '='
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.controller('XoScheduler', function () {
|
||||
this.init = () => {
|
||||
let i, j
|
||||
|
||||
const minutes = []
|
||||
for (i = 0; i < 6; i++) {
|
||||
minutes[i] = []
|
||||
for (j = 0; j < 10; j++) {
|
||||
minutes[i].push(10 * i + j)
|
||||
}
|
||||
}
|
||||
this.minutes = minutes
|
||||
|
||||
const hours = []
|
||||
for (i = 0; i < 3; i++) {
|
||||
hours[i] = []
|
||||
for (j = 0; j < 8; j++) {
|
||||
hours[i].push(8 * i + j)
|
||||
}
|
||||
}
|
||||
this.hours = hours
|
||||
|
||||
const days = []
|
||||
for (i = 0; i < 4; i++) {
|
||||
days[i] = []
|
||||
for (j = 1; j < 8; j++) {
|
||||
days[i].push(7 * i + j)
|
||||
}
|
||||
}
|
||||
days.push([29, 30, 31])
|
||||
this.days = days
|
||||
|
||||
this.months = [
|
||||
[
|
||||
{v: 1, l: 'Jan'},
|
||||
{v: 2, l: 'Feb'},
|
||||
{v: 3, l: 'Mar'},
|
||||
{v: 4, l: 'Apr'},
|
||||
{v: 5, l: 'May'},
|
||||
{v: 6, l: 'Jun'}
|
||||
],
|
||||
[
|
||||
{v: 7, l: 'Jul'},
|
||||
{v: 8, l: 'Aug'},
|
||||
{v: 9, l: 'Sep'},
|
||||
{v: 10, l: 'Oct'},
|
||||
{v: 11, l: 'Nov'},
|
||||
{v: 12, l: 'Dec'}
|
||||
]
|
||||
]
|
||||
|
||||
this.dayWeeks = [
|
||||
{v: 0, l: 'Sun'},
|
||||
{v: 1, l: 'Mon'},
|
||||
{v: 2, l: 'Tue'},
|
||||
{v: 3, l: 'Wed'},
|
||||
{v: 4, l: 'Thu'},
|
||||
{v: 5, l: 'Fri'},
|
||||
{v: 6, l: 'Sat'}
|
||||
]
|
||||
this.resetData()
|
||||
}
|
||||
|
||||
this.selectMinute = function (minute) {
|
||||
if (this.isSelectedMinute(minute)) {
|
||||
remove(this.data.minSelect, v => String(v) === String(minute))
|
||||
} else {
|
||||
this.data.minSelect.push(minute)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMinute = function (minute) {
|
||||
return indexOf(this.data.minSelect, minute) > -1 || indexOf(this.data.minSelect, String(minute)) > -1
|
||||
}
|
||||
|
||||
this.selectHour = function (hour) {
|
||||
if (this.isSelectedHour(hour)) {
|
||||
remove(this.data.hourSelect, v => String(v) === String(hour))
|
||||
} else {
|
||||
this.data.hourSelect.push(hour)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedHour = function (hour) {
|
||||
return indexOf(this.data.hourSelect, hour) > -1 || indexOf(this.data.hourSelect, String(hour)) > -1
|
||||
}
|
||||
|
||||
this.selectDay = function (day) {
|
||||
if (this.isSelectedDay(day)) {
|
||||
remove(this.data.daySelect, v => String(v) === String(day))
|
||||
} else {
|
||||
this.data.daySelect.push(day)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDay = function (day) {
|
||||
return indexOf(this.data.daySelect, day) > -1 || indexOf(this.data.daySelect, String(day)) > -1
|
||||
}
|
||||
|
||||
this.selectMonth = function (month) {
|
||||
if (this.isSelectedMonth(month)) {
|
||||
remove(this.data.monthSelect, v => String(v) === String(month))
|
||||
} else {
|
||||
this.data.monthSelect.push(month)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMonth = function (month) {
|
||||
return indexOf(this.data.monthSelect, month) > -1 || indexOf(this.data.monthSelect, String(month)) > -1
|
||||
}
|
||||
|
||||
this.selectDayWeek = function (dayWeek) {
|
||||
if (this.isSelectedDayWeek(dayWeek)) {
|
||||
remove(this.data.dayWeekSelect, v => String(v) === String(dayWeek))
|
||||
} else {
|
||||
this.data.dayWeekSelect.push(dayWeek)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDayWeek = function (dayWeek) {
|
||||
return indexOf(this.data.dayWeekSelect, dayWeek) > -1 || indexOf(this.data.dayWeekSelect, String(dayWeek)) > -1
|
||||
}
|
||||
|
||||
this.noMinutePlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the minSelect array with lodash.remove
|
||||
return this.data.min === 'select' && this.data.minSelect.length === 1 && String(this.data.minSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.minSelect = [0]
|
||||
this.data.min = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.noHourPlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the hourSelect array with lodash.remove
|
||||
return this.data.hour === 'select' && this.data.hourSelect.length === 1 && String(this.data.hourSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.hourSelect = [0]
|
||||
this.data.hour = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.resetData = () => {
|
||||
this.data.minRange = 5
|
||||
this.data.hourRange = 2
|
||||
this.data.minSelect = [0]
|
||||
this.data.hourSelect = []
|
||||
this.data.daySelect = []
|
||||
this.data.monthSelect = []
|
||||
this.data.dayWeekSelect = []
|
||||
this.data.min = 'select'
|
||||
this.data.hour = 'all'
|
||||
this.data.day = 'all'
|
||||
this.data.month = 'all'
|
||||
this.data.dayWeek = 'all'
|
||||
this.data.cronPattern = '* * * * *'
|
||||
this.data.summary = []
|
||||
this.data.previewLimit = 0
|
||||
|
||||
this.update()
|
||||
}
|
||||
|
||||
this.update = () => {
|
||||
const d = this.data
|
||||
const i = (d.min === 'all' && '*') ||
|
||||
(d.min === 'range' && ('*/' + d.minRange)) ||
|
||||
(d.min === 'select' && d.minSelect.join(',')) ||
|
||||
'*'
|
||||
const h = (d.hour === 'all' && '*') ||
|
||||
(d.hour === 'range' && ('*/' + d.hourRange)) ||
|
||||
(d.hour === 'select' && d.hourSelect.join(',')) ||
|
||||
'*'
|
||||
const dm = (d.day === 'all' && '*') ||
|
||||
(d.day === 'select' && d.daySelect.join(',')) ||
|
||||
'*'
|
||||
const m = (d.month === 'all' && '*') ||
|
||||
(d.month === 'select' && d.monthSelect.join(',')) ||
|
||||
'*'
|
||||
const dw = (d.dayWeek === 'all' && '*') ||
|
||||
(d.dayWeek === 'select' && d.dayWeekSelect.join(',')) ||
|
||||
'*'
|
||||
this.data.cronPattern = i + ' ' + h + ' ' + dm + ' ' + m + ' ' + dw
|
||||
|
||||
const tabState = {
|
||||
min: {
|
||||
all: d.min === 'all',
|
||||
range: d.min === 'range',
|
||||
select: d.min === 'select'
|
||||
},
|
||||
hour: {
|
||||
all: d.hour === 'all',
|
||||
range: d.hour === 'range',
|
||||
select: d.hour === 'select'
|
||||
},
|
||||
day: {
|
||||
all: d.day === 'all',
|
||||
range: d.day === 'range',
|
||||
select: d.day === 'select'
|
||||
},
|
||||
month: {
|
||||
all: d.month === 'all',
|
||||
select: d.month === 'select'
|
||||
},
|
||||
dayWeek: {
|
||||
all: d.dayWeek === 'all',
|
||||
select: d.dayWeek === 'select'
|
||||
}
|
||||
}
|
||||
this.tabs = tabState
|
||||
this.summarize()
|
||||
}
|
||||
|
||||
this.summarize = () => {
|
||||
const schedule = later.parse.cron(this.data.cronPattern)
|
||||
const occurences = later.schedule(schedule).next(25)
|
||||
this.data.summary = []
|
||||
forEach(occurences, occurence => {
|
||||
this.data.summary.push(moment(occurence).format('LLLL'))
|
||||
})
|
||||
}
|
||||
|
||||
const cronToData = (data, cron) => {
|
||||
const d = Object.create(null)
|
||||
const cronItems = cron.split(' ')
|
||||
|
||||
if (cronItems[0] === '*') {
|
||||
d.min = 'all'
|
||||
} else if (cronItems[0].indexOf('/') !== -1) {
|
||||
d.min = 'range'
|
||||
const [, range] = cronItems[0].split('/')
|
||||
d.minRange = range
|
||||
} else {
|
||||
d.min = 'select'
|
||||
d.minSelect = cronItems[0].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[1] === '*') {
|
||||
d.hour = 'all'
|
||||
} else if (cronItems[1].indexOf('/') !== -1) {
|
||||
d.hour = 'range'
|
||||
const [, range] = cronItems[1].split('/')
|
||||
d.hourRange = range
|
||||
} else {
|
||||
d.hour = 'select'
|
||||
d.hourSelect = cronItems[1].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[2] === '*') {
|
||||
d.day = 'all'
|
||||
} else {
|
||||
d.day = 'select'
|
||||
d.daySelect = cronItems[2].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[3] === '*') {
|
||||
d.month = 'all'
|
||||
} else {
|
||||
d.month = 'select'
|
||||
d.monthSelect = cronItems[3].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[4] === '*') {
|
||||
d.dayWeek = 'all'
|
||||
} else {
|
||||
d.dayWeek = 'select'
|
||||
d.dayWeekSelect = cronItems[4].split(',')
|
||||
}
|
||||
|
||||
assign(data, d)
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
this.api.setCron = cron => {
|
||||
cronToData(this.data, cron)
|
||||
this.update()
|
||||
}
|
||||
this.api.resetData = this.resetData.bind(this)
|
||||
|
||||
this.init()
|
||||
})
|
||||
|
||||
.name
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
@@ -21,15 +22,17 @@ export default angular.module('backup.management', [
|
||||
})
|
||||
.controller('ManagementCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
const mapJobKeyToState = {
|
||||
'rollingSnapshot': 'rollingsnapshot',
|
||||
'rollingBackup': 'backup',
|
||||
'disasterRecovery': 'disasterrecovery'
|
||||
rollingSnapshot: 'rollingsnapshot',
|
||||
rollingBackup: 'backup',
|
||||
disasterRecovery: 'disasterrecovery',
|
||||
__none: 'index'
|
||||
}
|
||||
|
||||
const mapJobKeyToJobDisplay = {
|
||||
'rollingSnapshot': 'Rolling Snapshot',
|
||||
'rollingBackup': 'Backup',
|
||||
'disasterRecovery': 'Disaster Recovery'
|
||||
rollingSnapshot: 'Rolling Snapshot',
|
||||
rollingBackup: 'Backup',
|
||||
disasterRecovery: 'Disaster Recovery',
|
||||
__none: '[unknown]'
|
||||
}
|
||||
|
||||
this.currentLogPage = 1
|
||||
@@ -37,7 +40,7 @@ export default angular.module('backup.management', [
|
||||
|
||||
const refreshSchedules = () => {
|
||||
xo.schedule.getAll()
|
||||
.then(schedules => this.schedules = schedules)
|
||||
.then(schedules => this.schedules = filter(schedules, schedule => this.jobs[schedule.job] && this.jobs[schedule.job].key in mapJobKeyToState))
|
||||
xo.scheduler.getScheduleTable()
|
||||
.then(table => this.scheduleTable = table)
|
||||
}
|
||||
@@ -45,12 +48,10 @@ export default angular.module('backup.management', [
|
||||
const getLogs = () => {
|
||||
xo.logs.get('jobs').then(logs => {
|
||||
const viewLogs = {}
|
||||
|
||||
forEach(logs, (log, logKey) => {
|
||||
const data = log.data
|
||||
const [time] = logKey.split(':')
|
||||
|
||||
if (data.event === 'job.start') {
|
||||
if (data.event === 'job.start' && data.key in mapJobKeyToState) {
|
||||
viewLogs[logKey] = {
|
||||
logKey,
|
||||
jobId: data.jobId,
|
||||
@@ -62,31 +63,33 @@ export default angular.module('backup.management', [
|
||||
}
|
||||
} else {
|
||||
const runJobId = data.runJobId
|
||||
const entry = viewLogs[runJobId]
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.event === 'job.end') {
|
||||
const entry = viewLogs[runJobId]
|
||||
|
||||
if (data.error) {
|
||||
entry.error = data.error
|
||||
}
|
||||
|
||||
entry.end = time
|
||||
entry.duration = time - entry.start
|
||||
entry.status = 'Terminated'
|
||||
} else if (data.event === 'jobCall.start') {
|
||||
viewLogs[runJobId].calls[logKey] = {
|
||||
entry.calls[logKey] = {
|
||||
callKey: logKey,
|
||||
params: resolveParams(data.params),
|
||||
method: data.method,
|
||||
time
|
||||
}
|
||||
} else if (data.event === 'jobCall.end') {
|
||||
const call = viewLogs[runJobId].calls[data.runCallId]
|
||||
const call = entry.calls[data.runCallId]
|
||||
|
||||
if (data.error) {
|
||||
call.error = data.error
|
||||
viewLogs[runJobId].hasErrors = true
|
||||
entry.hasErrors = true
|
||||
} else {
|
||||
call.returnedValue = data.returnedValue
|
||||
call.returnedValue = resolveReturn(data.returnedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,17 +107,24 @@ export default angular.module('backup.management', [
|
||||
|
||||
const resolveParams = params => {
|
||||
for (let key in params) {
|
||||
if (key === 'id') {
|
||||
const xoObject = xoApi.get(params[key])
|
||||
if (xoObject) {
|
||||
params[xoObject.type] = xoObject.name_label
|
||||
delete params[key]
|
||||
}
|
||||
const xoObject = xoApi.get(params[key])
|
||||
if (xoObject) {
|
||||
const newKey = xoObject.type || key
|
||||
params[newKey] = xoObject.name_label || xoObject.name || params[key]
|
||||
newKey !== key && delete params[key]
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const resolveReturn = returnValue => {
|
||||
const xoObject = xoApi.get(returnValue)
|
||||
let xoName = xoObject && (xoObject.name_label || xoObject.name)
|
||||
xoName && (xoName += xoObject.type && ` (${xoObject.type})` || '')
|
||||
returnValue = xoName || returnValue
|
||||
return returnValue
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
const refreshJobs = () => {
|
||||
@@ -151,9 +161,10 @@ export default angular.module('backup.management', [
|
||||
.finally(() => { this.working[id] = false })
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
this.resolveJobKey = schedule => mapJobKeyToState[this.jobs[schedule.job].key]
|
||||
this.displayJobKey = schedule => mapJobKeyToJobDisplay[this.jobs[schedule.job].key]
|
||||
this.resolveJobKey = schedule => mapJobKeyToState[this.jobs[schedule.job] && this.jobs[schedule.job].key || '__none']
|
||||
this.displayJobKey = schedule => mapJobKeyToJobDisplay[this.jobs[schedule.job] && this.jobs[schedule.job].key || '__none']
|
||||
this.displayLogKey = log => mapJobKeyToJobDisplay[log.key]
|
||||
this.resolveScheduleJobTag = schedule => this.jobs[schedule.job] && this.jobs[schedule.job].paramsVector && this.jobs[schedule.job].paramsVector.items[0].values[0].tag || schedule.id
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.working = {}
|
||||
|
||||
@@ -16,25 +16,22 @@
|
||||
td.text-center No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
th ID
|
||||
th Job
|
||||
th Scheduling
|
||||
th Tag
|
||||
th.hidden-xs Scheduling
|
||||
th State
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
td: a(ui-sref = 'backup.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ schedule.id }}
|
||||
td {{ ctrl.displayJobKey(schedule) }}
|
||||
td {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td: a(ui-sref = 'backup.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ ctrl.resolveScheduleJobTag(schedule) }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td
|
||||
span.text-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true')
|
||||
| enabled
|
||||
i.fa.fa-cogs
|
||||
span.text-muted(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
|
||||
span.text-warning(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') ?
|
||||
td.text-right
|
||||
fieldset(ng-disabled = 'ctrl.working[schedule.id]')
|
||||
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)') Enable
|
||||
button.btn.btn-warning(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)') Disable
|
||||
span.label.label-success.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === true') enabled
|
||||
span.label.label-default.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
|
||||
span.label.label-warning.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') unknown
|
||||
fieldset.pull-right(ng-disabled = 'ctrl.working[schedule.id]')
|
||||
button.btn(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)'): i.fa.fa-toggle-off
|
||||
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)'): i.fa.fa-toggle-on
|
||||
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
@@ -53,8 +50,8 @@
|
||||
tbody(ng-repeat = 'log in ctrl.logs | map | filter:ctrl.logSearch | orderBy:"-time" | slice:(ctrl.logPageSize * (ctrl.currentLogPage - 1)):(ctrl.logPageSize * ctrl.currentLogPage) track by log.logKey')
|
||||
tr
|
||||
td
|
||||
| {{ log.jobId }}
|
||||
button.btn.btn-sm(type = 'button', tooltip = 'See calls', ng-click = 'seeCalls = !seeCalls', ng-class = '{"btn-default": !log.hasErrors, "btn-danger": log.hasErrors}'): i.fa(ng-class = '{"fa-caret-down": !seeCalls, "fa-caret-up": seeCalls}')
|
||||
| {{ log.jobId }}
|
||||
td {{ ctrl.displayLogKey(log) }}
|
||||
td {{ log.start | date:'medium' }}
|
||||
td {{ log.end | date:'medium' }}
|
||||
@@ -68,6 +65,7 @@
|
||||
td(colspan = '6')
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'call in log.calls | map | orderBy:"-time" track by call.callKey')
|
||||
strong.text-info {{ call.method }}: 
|
||||
span(ng-repeat = '(key, param) in call.params')
|
||||
strong {{ key }}:
|
||||
| {{ param }}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
angular = require 'angular'
|
||||
forEach = require('lodash.foreach')
|
||||
includes = require('lodash.includes')
|
||||
Clipboard = require('clipboard')
|
||||
|
||||
isoDevice = require('iso-device')
|
||||
|
||||
@@ -89,5 +90,13 @@ module.exports = angular.module 'xoWebApp.console', [
|
||||
$scope.insert = (disc_id) ->
|
||||
xo.vm.insertCd id, disc_id, true
|
||||
|
||||
$scope.vmClipboard = ''
|
||||
$scope.setClipboard = (text) ->
|
||||
$scope.vmClipboard = text
|
||||
$scope.$applyAsync()
|
||||
|
||||
clipboard = new Clipboard('.copy')
|
||||
clipboard.on('error', (e) -> console.log('Clipboard', e))
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -15,15 +15,22 @@
|
||||
|
||||
//- Toolbar
|
||||
.list-group-item: .row.text-center
|
||||
.col-sm-6: iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
|
||||
.col-sm-3: button.btn.btn-default(
|
||||
.col-sm-4: iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
|
||||
.col-sm-2: button.btn.btn-default(
|
||||
ng-click = 'vncRemote.sendCtrlAltDel()'
|
||||
)
|
||||
i.fa.fa-keyboard-o
|
||||
|
|
||||
| Ctrl+Alt+Del
|
||||
.col-sm-4
|
||||
.input-group
|
||||
input#vm-clipboard.form-control(ng-model='vmClipboard' ng-change='vncRemote.pasteToClipboard(vmClipboard)')
|
||||
span.input-group-btn
|
||||
button.btn.btn-default.copy(data-clipboard-target='#vm-clipboard' tooltip="Copy text into local clipboard")
|
||||
i.fa.fa-clipboard
|
||||
| Copy
|
||||
//- Action panel
|
||||
.col-sm-3
|
||||
.col-sm-2
|
||||
.btn-group
|
||||
button.btn.btn-default.inversed(
|
||||
ng-if = "VM.power_state == ('Running' || 'Paused')"
|
||||
@@ -50,5 +57,6 @@
|
||||
.list-group-item
|
||||
no-vnc(
|
||||
url = '{{consoleUrl}}'
|
||||
remote-control = 'vncRemote'
|
||||
remote-control = 'vncRemote',
|
||||
on-clipboard-change = 'setClipboard(clipboardContent)'
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ export default angular.module('dashboard.overview', [
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('Overview', function ($scope, $window, xoApi, xo, $timeout, bytesToSizeFilter) {
|
||||
.controller('Overview', function ($scope, $window, xoApi, xo, $timeout, bytesToSizeFilter, modal) {
|
||||
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
angular.extend($scope, {
|
||||
pools: {
|
||||
@@ -47,6 +47,32 @@ export default angular.module('dashboard.overview', [
|
||||
cpu: [0, 0],
|
||||
srs: []
|
||||
})
|
||||
|
||||
$scope.installAllPatches = function () {
|
||||
modal.confirm({
|
||||
title: 'Install all the missing patches',
|
||||
message: 'Are you sure you want to install all the missing patches? This could take a while...'
|
||||
}).then(() =>
|
||||
foreach($scope.pools.all, function (pool, pool_id) {
|
||||
let pool_hosts = $scope.hostsByPool[pool_id]
|
||||
foreach(pool_hosts, function (host, host_id) {
|
||||
console.log('Installing all missing patches on host ', host_id)
|
||||
xo.host.installAllPatches(host_id)
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
$scope.installHostPatches = function (hostId) {
|
||||
modal.confirm({
|
||||
title: 'Update host (' + $scope.nbUpdates[hostId] + ' patch(es))',
|
||||
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
|
||||
}).then(() => {
|
||||
console.log('Installing all missing patches on host ', hostId)
|
||||
xo.host.installAllPatches(hostId)
|
||||
})
|
||||
}
|
||||
|
||||
function populateChartsData () {
|
||||
let pools,
|
||||
vmsByContainer,
|
||||
@@ -74,11 +100,22 @@ export default angular.module('dashboard.overview', [
|
||||
srs = []
|
||||
|
||||
// update vdi, set them to the right host
|
||||
pools = xoApi.getView('pools')
|
||||
$scope.pools = pools = xoApi.getView('pools')
|
||||
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
vmsByContainer = xoApi.getIndex('vmsByContainer')
|
||||
hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
$scope.hostsByPool = hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
$scope.nbUpdates = {}
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
let pool_hosts = hostsByPool[pool_id]
|
||||
foreach(pool_hosts, function (host, host_id) {
|
||||
xo.host.listMissingPatches(host_id)
|
||||
.then(result => {
|
||||
$scope.nbUpdates[host_id] = result.length
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
foreach(pools.all, function (pool, pool_id) {
|
||||
nb_pools++
|
||||
|
||||
@@ -100,4 +100,34 @@
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
|
||||
|
||||
.grid-sm
|
||||
.grid-cell
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-refresh
|
||||
| Updates
|
||||
span.quick-edit(
|
||||
tooltip="Update all"
|
||||
ng-click="installAllPatches()"
|
||||
)
|
||||
i.fa.fa-download.fa-fw
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Pool
|
||||
th Host
|
||||
th Description
|
||||
th Missing patches
|
||||
th Install
|
||||
tbody(ng-repeat="pool in pools.all | orderBy:'name_label'")
|
||||
tr( ng-repeat="host in hostsByPool[pool.id]" ng-if="nbUpdates[host.id]")
|
||||
td.oneliner
|
||||
| {{ pool.name_label }}
|
||||
td.oneliner
|
||||
| {{ host.name_label }}
|
||||
td.oneliner
|
||||
| {{ host.name_description }}
|
||||
td {{ nbUpdates[host.id] }}
|
||||
td
|
||||
button.btn.btn-success(ng-click="installHostPatches(host.id)" tooltip="Install {{ nbUpdates[host.id] }} patch(es)")
|
||||
| Update host
|
||||
|
||||
@@ -6,6 +6,8 @@ omit = require 'lodash.omit'
|
||||
sum = require 'lodash.sum'
|
||||
throttle = require 'lodash.throttle'
|
||||
find = require 'lodash.find'
|
||||
filter = require 'lodash.filter'
|
||||
pluck = require 'lodash.pluck'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -213,6 +215,14 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
}).then ->
|
||||
xo.host.stop id
|
||||
|
||||
|
||||
$scope.emergencyShutdownHost = (hostId) ->
|
||||
modal.confirm({
|
||||
title: 'Shutdown host'
|
||||
message: 'Are you sure you want to suspend all the VMs on this host and shut the host down?'
|
||||
}).then ->
|
||||
xo.host.emergencyShutdownHost hostId
|
||||
|
||||
$scope.saveHost = ($data) ->
|
||||
{host} = $scope
|
||||
{name_label, name_description, enabled} = $data
|
||||
@@ -410,5 +420,23 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
netOnly: false,
|
||||
loadOnly: false
|
||||
}
|
||||
|
||||
$scope.canAdmin = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'administrate') || false
|
||||
|
||||
$scope.canOperate = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'operate') || false
|
||||
|
||||
$scope.canView = (id = undefined) ->
|
||||
if id == undefined
|
||||
id = $scope.host && $scope.host.id
|
||||
|
||||
return id && xoApi.canInteract(id, 'view') || false
|
||||
# A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| General
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()")
|
||||
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()", ng-if = '!hostSettings.$visible && canAdmin()')
|
||||
i.fa.fa-edit.fa-fw
|
||||
span.quick-edit(ng-if="hostSettings.$visible", tooltip="Cancel Edition", ng-click="hostSettings.$cancel()")
|
||||
i.fa.fa-undo.fa-fw
|
||||
.panel-body
|
||||
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
|
||||
dl.dl-horizontal
|
||||
@@ -186,7 +188,7 @@
|
||||
i.xo-icon-loading
|
||||
| Fetching stats...
|
||||
//- Action panel
|
||||
.grid-sm
|
||||
.grid-sm(ng-if = 'canOperate()')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash
|
||||
@@ -195,35 +197,39 @@
|
||||
.grid-sm.grid--gutters
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})")
|
||||
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})", ng-if = 'canAdmin()')
|
||||
i.xo-icon-sr.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})")
|
||||
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})", ng-if = 'canAdmin()')
|
||||
i.xo-icon-vm.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Reboot host", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootHost(host.id)")
|
||||
button.btn(tooltip="Reboot host", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="shutdownHost(host.id)")
|
||||
button.btn(tooltip="Shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="shutdownHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-power-off.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Suspend all VMs and shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="emergencyShutdownHost(host.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-exclamation-triangle.fa-2x.fa-fw
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group(ng-if="host.enabled")
|
||||
button.btn(tooltip="Disable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="disableHost(host.id)")
|
||||
button.btn(tooltip="Disable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="disableHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-times-circle.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="!host.enabled")
|
||||
button.btn(tooltip="Enable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="enableHost(host.id)")
|
||||
button.btn(tooltip="Enable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="enableHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-check-circle.fa-2x.fa-fw
|
||||
.grid.grid-cell
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Restart toolstack", tooltip-placement="top", type="button", style="width: 90%", xo-click="restartToolStack(host.id)")
|
||||
button.btn(tooltip="Restart toolstack", tooltip-placement="top", type="button", style="width: 90%", xo-click="restartToolStack(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-retweet.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="pool.name_label && (hostsByPool[pool.id] | count)>1")
|
||||
button.btn(tooltip="Remove from pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)")
|
||||
button.btn(tooltip="Remove from pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-cloud-upload.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="pool.name_label && (hostsByPool[pool.id] | count)==1"
|
||||
dropdown
|
||||
)
|
||||
button.btn.dropdown-toggle(
|
||||
ng-if = 'canAdmin()'
|
||||
dropdown-toggle
|
||||
tooltip="Move host to another pool"
|
||||
tooltip-placement="top"
|
||||
@@ -238,10 +244,11 @@
|
||||
i.xo-icon-host.fa-fw
|
||||
| To {{p.name_label}}
|
||||
.grid-cell.btn-group(ng-if="!pool.name_label")
|
||||
button.btn(tooltip="Add to pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_addHost(host.id)")
|
||||
button.btn(tooltip="Add to pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_addHost(host.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-cloud-download.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(style="margin-bottom: 0.5em")
|
||||
button.btn(
|
||||
ng-if = 'canAdmin()'
|
||||
tooltip="Import VM"
|
||||
tooltip-placement="top"
|
||||
type="button"
|
||||
@@ -305,14 +312,14 @@
|
||||
td(ng-if="SRsToPBDs[SR.id].attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.id].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Local SR
|
||||
//- TODO: migrate to SRs and not PBDs when implemented in xo-server spec
|
||||
@@ -330,14 +337,14 @@
|
||||
td(ng-if="SRsToPBDs[SR.id].attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.id].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Networks/Interfaces panel
|
||||
.grid-sm
|
||||
@@ -355,7 +362,7 @@
|
||||
th.col-md-1 Link status
|
||||
tr(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('name_label') track by PIF.id")
|
||||
td
|
||||
| {{PIF.device}}
|
||||
| {{PIF.device}}
|
||||
span.label.label-primary(ng-if="PIF.management") XAPI
|
||||
td
|
||||
span(ng-if="PIF.vlan > -1")
|
||||
@@ -368,23 +375,23 @@
|
||||
td(ng-if="PIF.attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this interface", xo-click="disconnectPIF(PIF.id)")
|
||||
a(tooltip="Disconnect this interface", xo-click="disconnectPIF(PIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!PIF.attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)")
|
||||
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)")
|
||||
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.text-right
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork")
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork", ng-hide = '!canAdmin()', ng-disabled = '!canAdmin()')
|
||||
i.fa.fa-plus(ng-if = '!creatingNetwork')
|
||||
i.fa.fa-minus(ng-if = 'creatingNetwork')
|
||||
| Create Network
|
||||
br
|
||||
form.form-inline.text-right#createNetworkForm(ng-if = 'creatingNetwork', name = 'createNetworkForm', ng-submit = 'createNetwork(newNetworkName, newNetworkDescription, newNetworkPIF, newNetworkMTU, newNetworkVlan)')
|
||||
fieldset(ng-attr-disabled = '{{ createNetworkWaiting ? true : undefined }}')
|
||||
fieldset(ng-disabled = 'createNetworkWaiting || !canAdmin()')
|
||||
.form-group
|
||||
label(for = 'newNetworkPIF') Interface
|
||||
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in host.$PIFs')
|
||||
@@ -439,7 +446,7 @@
|
||||
| {{task.progress*100 | number:1}}%
|
||||
td.oneliner
|
||||
| {{task.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
span.pull-right.btn-group.quick-buttons(ng-if = 'canAdmin()')
|
||||
a(xo-click="cancelTask(task.id)")
|
||||
i.fa.fa-times.fa-lg(tooltip="Cancel this task")
|
||||
a(xo-click="destroyTask(task.id)")
|
||||
@@ -451,7 +458,7 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments
|
||||
| Logs
|
||||
span.quick-edit(ng-if="host.messages | isNotEmpty", tooltip="Remove all logs", ng-click="deleteAllLog()")
|
||||
span.quick-edit(ng-if="(host.messages | isNotEmpty) && canAdmin()", tooltip="Remove all logs", ng-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="host.messages | isEmpty") No recent logs
|
||||
@@ -462,7 +469,7 @@
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
span.pull-right.btn-group.quick-buttons(ng-if = 'canAdmin()')
|
||||
a(xo-click="deleteLog(message.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.center(ng-if = '(host.messages | count) > 5 || currentLogPage > 1')
|
||||
@@ -475,7 +482,7 @@
|
||||
| Patches
|
||||
span.quick-edit(ng-click="listMissingPatches(host.id)", tooltip="Check for updates")
|
||||
i.fa.fa-question-circle
|
||||
span.quick-edit(ng-click="installAllPatches(host.id)", tooltip="Install all the missing patches", style="margin-right:5px")
|
||||
span.quick-edit(ng-click="installAllPatches(host.id)", tooltip="Install all the missing patches", style="margin-right:5px", ng-if = 'canAdmin()')
|
||||
i.fa.fa-download
|
||||
.panel-body
|
||||
table.table.table-hover(ng-if="poolPatches || updates")
|
||||
@@ -494,8 +501,9 @@
|
||||
td.oneliner {{patch.date | date:"medium"}}
|
||||
td -
|
||||
td
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host")
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host", ng-if = 'canAdmin()')
|
||||
span.label.label-danger Missing
|
||||
span.label.label-danger(ng-if = '!canAdmin()') Missing
|
||||
tr(ng-repeat="patch in poolPatches | map | slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
|
||||
td.oneliner {{patch.name}}
|
||||
td.oneliner {{patch.description}}
|
||||
@@ -505,8 +513,10 @@
|
||||
td
|
||||
span(ng-if="isPoolPatchApplied(patch)")
|
||||
span.label.label-success Applied
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", ng-if="!isPoolPatchApplied(patch)", tooltip="Click to apply the patch on this host")
|
||||
span.label.label-warning Not applied
|
||||
span(ng-if="!isPoolPatchApplied(patch)")
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to apply the patch on this host", ng-if = 'canAdmin()')
|
||||
span.label.label-warning Not applied
|
||||
span.label.label-warning(ng-if = '!canAdmin()') Not applied
|
||||
.center(ng-if = '(poolPatches | count) > 5 || currentPatchPage > 1')
|
||||
pagination(boundary-links="true", total-items="poolPatches | count", ng-model="$parent.currentPatchPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
|
||||
|
||||
@@ -92,15 +92,15 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
a(ui-sref="dashboard.index")
|
||||
i.fa.fa-dashboard
|
||||
| Dashboard
|
||||
//- li.disabled(ui-sref-active="active")
|
||||
//- a(ui-sref="graph")
|
||||
//- i.fa.fa-sitemap
|
||||
//- | Graphs view
|
||||
li.divider
|
||||
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
|
||||
a(ui-sref = 'backup.index')
|
||||
i.fa.fa-archive
|
||||
| Backup
|
||||
li
|
||||
a(ui-sref = 'taskscheduler.index')
|
||||
i.fa.fa-cogs
|
||||
| Job Manager
|
||||
li.divider
|
||||
li(
|
||||
ui-sref-active = 'active'
|
||||
|
||||
@@ -114,6 +114,7 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
$scope.name_description = ''
|
||||
$scope.name_label = ''
|
||||
$scope.template = ''
|
||||
$scope.firstSR = ''
|
||||
$scope.VDIs = []
|
||||
$scope.VIFs = []
|
||||
$scope.isDiskTemplate = false
|
||||
@@ -150,6 +151,8 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
# When the selected template changes, updates other variables.
|
||||
$scope.$watch 'template', (template) ->
|
||||
return unless template
|
||||
# After each template change, initialize cloudConfig to empty
|
||||
$scope.cloudConfig = ''
|
||||
|
||||
{install_methods} = template.template_info
|
||||
availableMethods = $scope.availableMethods = Object.create null
|
||||
@@ -173,6 +176,15 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
VDI.id = VDI_id++
|
||||
VDI.size = bytesToSizeFilter VDI.size
|
||||
VDI.SR or= default_SR
|
||||
# store the first SR for later use (e.g: CloudConfig)
|
||||
if VDI.id == 0
|
||||
$scope.firstSR = VDI.SR or= default_SR
|
||||
# if the template is labeled CoreOS
|
||||
# we'll use config drive setup
|
||||
if template.name_label == 'CoreOS'
|
||||
return xo.vm.getCloudInitConfig template.id
|
||||
.then (result) ->
|
||||
$scope.cloudConfig = result
|
||||
|
||||
$scope.createVM = ->
|
||||
{
|
||||
@@ -270,6 +282,9 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
# FIXME: handles invalid entries.
|
||||
data.memory = memory
|
||||
|
||||
if $scope.cloudConfig
|
||||
xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.cloudConfig).then ->
|
||||
xo.docker.register id
|
||||
xoApi.call('vm.set', data).then -> id
|
||||
.then (id) ->
|
||||
$state.go 'VMs_view', { id }
|
||||
|
||||
@@ -154,6 +154,18 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
i.fa.fa-plus
|
||||
| Add interface
|
||||
//- end of misc and interface panel
|
||||
//- Cloud config panel
|
||||
.grid(ng-if = 'cloudConfig')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cloud
|
||||
| Cloud config
|
||||
.pull-right.small
|
||||
button.btn.btn-default(type = 'button', ng-click = 'isExpanded = !isExpanded'): i.fa(ng-class = '{"fa-plus": !isExpanded, "fa-minus": isExpanded}')
|
||||
.panel-body
|
||||
textarea.form-control(rows="20", collapse= '!isExpanded', ng-model='$parent.cloudConfig', name='cloudConfig')
|
||||
| {{cloudConfig}}
|
||||
|
||||
//- Disk panel
|
||||
.grid(ng-if="!isDiskTemplate")
|
||||
.panel.panel-default
|
||||
|
||||
@@ -56,8 +56,14 @@ function cleanUpConfiguration (schema, configuration) {
|
||||
configuration[key] = item
|
||||
if (!keepItem(item) || !schema.properties || !(key in schema.properties)) {
|
||||
delete configuration[key]
|
||||
} else if (schema.properties && schema.properties[key] && schema.properties[key].type === 'object') {
|
||||
cleanUpConfiguration(schema.properties[key], item)
|
||||
} else if (schema.properties && schema.properties[key]) {
|
||||
const type = schema.properties[key].type
|
||||
|
||||
if (type === 'integer' || type === 'number') {
|
||||
configuration[key] = +configuration[key]
|
||||
} else if (type === 'object') {
|
||||
cleanUpConfiguration(schema.properties[key], item)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -172,7 +178,7 @@ export default angular.module('settings.plugins', [
|
||||
}
|
||||
})
|
||||
|
||||
.directive('objectInput', () => {
|
||||
.directive('confObjectInput', () => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: objectInputView,
|
||||
@@ -181,12 +187,12 @@ export default angular.module('settings.plugins', [
|
||||
schema: '=',
|
||||
required: '='
|
||||
},
|
||||
controller: 'ObjectInput as ctrl',
|
||||
controller: 'ConfObjectInput as ctrl',
|
||||
bindToController: true
|
||||
}
|
||||
})
|
||||
|
||||
.controller('ObjectInput', function ($scope, xo, xoApi) {
|
||||
.controller('ConfObjectInput', function ($scope, xo, xoApi) {
|
||||
const prepareModel = () => {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = {
|
||||
|
||||
@@ -5,10 +5,25 @@
|
||||
fieldset(ng-disabled = '!ctrl.required && !ctrl.model.__use', ng-hide = '!ctrl.required && !ctrl.model.__use')
|
||||
ul(style = 'padding-left: 0;')
|
||||
li.list-group-item(ng-repeat = '(key, value) in ctrl.schema.properties track by key')
|
||||
.input-group
|
||||
.input-group(ng-if = 'value.type != "boolean"')
|
||||
span.input-group-addon
|
||||
| {{key}}
|
||||
| {{value.title || key}}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired(key, ctrl.schema)') *
|
||||
input.form-control.input-sm(ng-if = '!ctrl.isPassword(key)', type = 'text', ng-model = 'ctrl.model[key]', ng-required = 'ctrl.isRequired(key, ctrl.schema)')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.isPassword(key)', type = 'password', ng-model = 'ctrl.model[key]', ng-required = 'ctrl.isRequired(key, ctrl.schema)')
|
||||
input.form-control.input-sm(
|
||||
ng-if = 'value.type != "number" && value.type != "integer"',
|
||||
type = '{{ctrl.isPassword(key) ? "password" : "text"}}',
|
||||
ng-model = 'ctrl.model[key]',
|
||||
ng-required = 'ctrl.isRequired(key, ctrl.schema)'
|
||||
)
|
||||
input.form-control.input-sm(
|
||||
ng-if = 'value.type == "number" || value.type == "integer"',
|
||||
type = 'number',
|
||||
ng-model = 'ctrl.model[key]',
|
||||
ng-required = 'ctrl.isRequired(key, ctrl.schema)'
|
||||
)
|
||||
.form-inline(ng-if = 'value.type == "boolean"')
|
||||
.checkbox.small('style="color: #31708F;"') {{value.title || key}} :
|
||||
label('style="color: #A7AFB0;"')
|
||||
i.fa.fa-2x(ng-class = '{"fa-toggle-on": ctrl.model[key], "fa-toggle-off": !ctrl.model[key]}')
|
||||
input.hidden(type = 'checkbox', ng-model = 'ctrl.model[key]')
|
||||
.help-block(ng-bind-html = 'ctrl.schema.properties[key].description | md2html')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
p.text-center(ng-if = '!ctrl.plugins || !crtl.plugins.length') No plugins found
|
||||
p.text-center(ng-if = '!ctrl.plugins || !ctrl.plugins.length') No plugins found
|
||||
div(ng-repeat = 'plugin in ctrl.plugins | orderBy:"name" track by plugin.id')
|
||||
h3.form-inline.clearfix
|
||||
span.text-info {{ plugin.name }}
|
||||
@@ -30,15 +30,15 @@
|
||||
form.form-horizontal(ng-if = 'plugin.configurationSchema', ng-submit = 'ctrl.configure(plugin)')
|
||||
fieldset(ng-disabled = 'ctrl.disabled[plugin.id]')
|
||||
.form-group(ng-repeat = '(key, prop) in plugin.configurationSchema.properties')
|
||||
label.col-md-2.control-label
|
||||
| {{key}}
|
||||
label.col-md-2.control-label
|
||||
| {{prop.title || key}}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired(key, plugin.configurationSchema)') *
|
||||
.col-md-5
|
||||
input.form-control(ng-if = 'prop.type === "string" && !ctrl.isPassword(key)', type = 'text', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)', placeholder = '{{ plugin.configurationSchema.properties[key].default }}')
|
||||
input.form-control(ng-if = 'prop.type === "string" && ctrl.isPassword(key)', type = 'password', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
multi-string-input(ng-if = 'prop.type === "array" && prop.items.type === "string"', model = 'plugin.configuration[key]')
|
||||
.checkbox(ng-if = 'prop.type === "boolean"'): label: input(type = 'checkbox', ng-model = 'plugin.configuration[key]')
|
||||
object-input(ng-if = 'prop.type === "object"', model = 'plugin.configuration[key]', schema = 'prop', required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
conf-object-input(ng-if = 'prop.type === "object"', model = 'plugin.configuration[key]', schema = 'prop', required = 'ctrl.isRequired(key, plugin.configurationSchema)')
|
||||
.col-md-5
|
||||
span.help-block(ng-bind-html = 'prop.description | md2html')
|
||||
.form-group
|
||||
|
||||
41
app/modules/task-scheduler/index.js
Normal file
41
app/modules/task-scheduler/index.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import angular from 'angular'
|
||||
import later from 'later'
|
||||
import scheduler from 'scheduler'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import job from './job'
|
||||
import overview from './overview'
|
||||
import schedule from './schedule'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('taskScheduler', [
|
||||
uiRouter,
|
||||
scheduler,
|
||||
|
||||
job,
|
||||
overview,
|
||||
schedule
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler', {
|
||||
abstract: true,
|
||||
data: {
|
||||
requireAdmin: true
|
||||
},
|
||||
template: view,
|
||||
url: '/taskscheduler'
|
||||
})
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('taskscheduler.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('taskscheduler.overview')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.name
|
||||
19
app/modules/task-scheduler/job/array-input-view.jade
Normal file
19
app/modules/task-scheduler/job/array-input-view.jade
Normal file
@@ -0,0 +1,19 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ul(style = 'padding-left: 0;')
|
||||
li.list-group-item.clearfix(ng-repeat = 'item in ctrl.model track by $index')
|
||||
| {{item}}
|
||||
a.pull-right(ng-click = 'ctrl.remove($index)'): i.fa.fa-times
|
||||
form(ng-submit = 'ctrl.add(ctrl.newItem); ctrl.newItem = ""')
|
||||
.input-group
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "string"', type = 'text', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "integer"', type = 'number', step = '1', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "number"', type = 'number', step = 'any', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
|
||||
span.input-group-addon(ng-if = 'ctrl.getType(ctrl.property.items) === "boolean"')
|
||||
input(type = 'checkbox', ng-model = 'ctrl.newItem')
|
||||
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "boolean"', disabled)
|
||||
span.input-group-btn
|
||||
button.btn.btn-primary.btn-sm(type = 'submit') Add
|
||||
6
app/modules/task-scheduler/job/boolean-input-view.jade
Normal file
6
app/modules/task-scheduler/job/boolean-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input(form = '{{ ctrl.form }}', type = 'checkbox', ng-model = 'ctrl.model')
|
||||
15
app/modules/task-scheduler/job/host-input-view.jade
Normal file
15
app/modules/task-scheduler/job/host-input-view.jade
Normal file
@@ -0,0 +1,15 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Hosts
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose hosts')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "host"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if = 'object.$container') ({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})
|
||||
827
app/modules/task-scheduler/job/index.js
Normal file
827
app/modules/task-scheduler/job/index.js
Normal file
@@ -0,0 +1,827 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import map from 'lodash.map'
|
||||
import mapValues from 'lodash.mapvalues'
|
||||
import remove from 'lodash.remove'
|
||||
import trim from 'lodash.trim'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import Bluebird from 'bluebird'
|
||||
Bluebird.longStackTraces()
|
||||
|
||||
import arrayInputView from './array-input-view'
|
||||
import booleanInputView from './boolean-input-view'
|
||||
import hostInputView from './host-input-view'
|
||||
import integerInputView from './integer-input-view'
|
||||
import numberInputView from './number-input-view'
|
||||
import objectInputView from './object-input-view'
|
||||
import poolInputView from './pool-input-view'
|
||||
import srInputView from './sr-input-view'
|
||||
import stringInputView from './string-input-view'
|
||||
import view from './view'
|
||||
import vmInputView from './vm-input-view'
|
||||
import xoEntityInputView from './xo-entity-input-view'
|
||||
import xoObjectInputView from './xo-object-input-view'
|
||||
import xoRoleInputView from './xo-role-input-view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
const JOB_KEY = 'genericTask'
|
||||
|
||||
const jobCompliantMethods = [
|
||||
'acl.add',
|
||||
'acl.remove',
|
||||
'host.detach',
|
||||
'host.disable',
|
||||
'host.enable',
|
||||
'host.installAllPatches',
|
||||
'host.restart',
|
||||
'host.restartAgent',
|
||||
'host.set',
|
||||
'host.start',
|
||||
'host.stop',
|
||||
'job.runSequence',
|
||||
'vm.attachDisk',
|
||||
'vm.backup',
|
||||
'vm.clone',
|
||||
'vm.convert',
|
||||
'vm.copy',
|
||||
'vm.creatInterface',
|
||||
'vm.delete',
|
||||
'vm.migrate',
|
||||
'vm.migrate_pool',
|
||||
'vm.restart',
|
||||
'vm.resume',
|
||||
'vm.revert',
|
||||
'vm.rollingBackup',
|
||||
'vm.rollingDrCopy',
|
||||
'vm.rollingSnapshot',
|
||||
'vm.set',
|
||||
'vm.setBootOrder',
|
||||
'vm.snapshot',
|
||||
'vm.start',
|
||||
'vm.stop',
|
||||
'vm.suspend'
|
||||
]
|
||||
|
||||
const getType = function (param) {
|
||||
if (!param) {
|
||||
return
|
||||
}
|
||||
if (Array.isArray(param.type)) {
|
||||
if (includes(param.type, 'integer')) {
|
||||
return 'integer'
|
||||
} else if (includes(param.type, 'number')) {
|
||||
return 'number'
|
||||
} else {
|
||||
return 'string'
|
||||
}
|
||||
}
|
||||
return param.type
|
||||
}
|
||||
|
||||
const isRequired = function (param) {
|
||||
if (!param) {
|
||||
return
|
||||
}
|
||||
return (!param.optional && !(includes(['boolean', 'array'], getType(param))))
|
||||
}
|
||||
/**
|
||||
* Takes care of unfilled not-required data and unwanted white-spaces
|
||||
*/
|
||||
const cleanUpData = function (data) {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
function sanitizeItem (item) {
|
||||
if (typeof item === 'string') {
|
||||
item = trim(item)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
function keepItem (item) {
|
||||
if ((item === undefined) || (item === null) || (item === '') || (Array.isArray(item) && item.length === 0) || item.__use === false) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
forEach(data, (item, key) => {
|
||||
item = sanitizeItem(item)
|
||||
data[key] = item
|
||||
if (!keepItem(item)) {
|
||||
delete data[key]
|
||||
} else if (typeof item === 'object') {
|
||||
cleanUpData(item)
|
||||
}
|
||||
})
|
||||
|
||||
delete data.__use
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries extracting XO Object targeted property
|
||||
*/
|
||||
const reduceXoObject = function (value, propertyName = 'id') {
|
||||
return value && value[propertyName] || value
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts all data "arrayed" by UI-multiple-selectors to job's cross-product trick
|
||||
*/
|
||||
const dataToParamVectorItems = function (params, data) {
|
||||
const items = []
|
||||
forEach(params, (param, name) => {
|
||||
if (Array.isArray(data[name]) && getType(param) !== 'array') {
|
||||
const values = []
|
||||
if (data[name].length === 1) { // One value, no need to engage cross-product
|
||||
data[name] = data[name].pop()
|
||||
} else {
|
||||
forEach(data[name], value => {
|
||||
values.push({[name]: reduceXoObject(value, name)})
|
||||
})
|
||||
if (values.length) { // No values at all
|
||||
items.push({
|
||||
type: 'set',
|
||||
values
|
||||
})
|
||||
}
|
||||
delete data[name]
|
||||
}
|
||||
}
|
||||
})
|
||||
if (Object.keys(data).length) {
|
||||
items.push({
|
||||
type: 'set',
|
||||
values: [mapValues(data, reduceXoObject)]
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
const actionGroup = {
|
||||
group: undefined,
|
||||
get: function () {
|
||||
return this.group
|
||||
},
|
||||
set: function (group) {
|
||||
this.group = group
|
||||
}
|
||||
}
|
||||
|
||||
const _initXoObjectInput = function () {
|
||||
if (this.model === undefined) {
|
||||
this.model = []
|
||||
}
|
||||
if (!Array.isArray(this.model)) {
|
||||
this.model = [this.model]
|
||||
}
|
||||
this.intraModel = map(this.model, value => find(this.objects, object => object.id === value) || value)
|
||||
}
|
||||
|
||||
const _exportRemove = function (removedItem) {
|
||||
remove(this.model, item => item === reduceXoObject(removedItem))
|
||||
}
|
||||
|
||||
const _exportSelect = function (addedItem) {
|
||||
const addOn = reduceXoObject(addedItem)
|
||||
if (!find(this.model, item => item === addOn)) {
|
||||
this.model.push(addOn)
|
||||
}
|
||||
}
|
||||
|
||||
export default angular.module('xoWebApp.taskscheduler.job', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler.job', {
|
||||
url: '/job/:id',
|
||||
controller: 'JobCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
|
||||
.controller('JobCtrl', function ($scope, xo, xoApi, notify, $stateParams) {
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
this.running = {}
|
||||
this.ready = false
|
||||
|
||||
let comesForEditing = $stateParams.id
|
||||
|
||||
this.resetData = () => {
|
||||
this.formData = {}
|
||||
}
|
||||
this.resetForm = () => {
|
||||
this.resetData()
|
||||
this.editedJobId = undefined
|
||||
this.jobName = undefined
|
||||
this.selectedAction = undefined
|
||||
}
|
||||
this.resetForm()
|
||||
|
||||
$scope.$watch(() => this.selectedAction, newAction => actionGroup.set(newAction && newAction.group))
|
||||
|
||||
const loadActions = () => xoApi.call('system.getMethodsInfo')
|
||||
.then(response => {
|
||||
const actions = []
|
||||
|
||||
for (let method in response) {
|
||||
if (includes(jobCompliantMethods, method)) {
|
||||
let [group, command] = method.split('.')
|
||||
response[method].properties = response[method].params
|
||||
response[method].type = 'object'
|
||||
delete response[method].params
|
||||
actions.push({
|
||||
method,
|
||||
group,
|
||||
command,
|
||||
info: response[method]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.actions = actions
|
||||
this.ready = true
|
||||
})
|
||||
|
||||
const loadJobs = () => xo.job.getAll().then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
if (job.key === JOB_KEY) {
|
||||
j[job.id] = job
|
||||
}
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
|
||||
const refresh = () => loadJobs()
|
||||
const getReady = () => loadActions().then(refresh).then(() => this.ready = true)
|
||||
getReady().then(() => {
|
||||
if (comesForEditing) {
|
||||
this.edit(comesForEditing)
|
||||
comesForEditing = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const saveNew = (name, action, data) => {
|
||||
const job = {
|
||||
type: 'call',
|
||||
name,
|
||||
key: JOB_KEY,
|
||||
method: action.method,
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: dataToParamVectorItems(action.info.properties, data)
|
||||
}
|
||||
}
|
||||
return xo.job.create(job)
|
||||
}
|
||||
|
||||
const save = (id, name, action, data) => {
|
||||
const job = this.jobs[id]
|
||||
job.name = name
|
||||
job.method = action.method
|
||||
job.paramsVector = {
|
||||
type: 'crossProduct',
|
||||
items: dataToParamVectorItems(action.info.properties, data)
|
||||
}
|
||||
return xo.job.set(job)
|
||||
}
|
||||
|
||||
this.save = (id, name, action, data) => {
|
||||
const dataClone = cleanUpData(cloneDeep(data))
|
||||
const saved = (id !== undefined) ? save(id, name, action, dataClone) : saveNew(name, action, dataClone)
|
||||
return saved
|
||||
.then(() => this.resetForm())
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
this.edit = id => {
|
||||
this.resetForm()
|
||||
try {
|
||||
const job = this.jobs[id]
|
||||
if (job) {
|
||||
this.editedJobId = id
|
||||
this.jobName = job.name
|
||||
this.selectedAction = find(this.actions, action => action.method === job.method)
|
||||
const data = {}
|
||||
const paramsVector = job.paramsVector
|
||||
if (paramsVector) {
|
||||
if (paramsVector.type !== 'crossProduct') {
|
||||
throw new Error(`Unknown parameter-vector type ${paramsVector.type}`)
|
||||
}
|
||||
forEach(paramsVector.items, item => {
|
||||
if (item.type !== 'set') {
|
||||
throw new Error(`Unknown parameter-vector item type ${item.type}`)
|
||||
}
|
||||
if (item.values.length === 1) {
|
||||
assign(data, item.values[0])
|
||||
} else {
|
||||
forEach(item.values, valueItem => {
|
||||
forEach(valueItem, (value, key) => {
|
||||
if (data[key] === undefined) {
|
||||
data[key] = []
|
||||
}
|
||||
data[key].push(value)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
this.formData = data
|
||||
}
|
||||
} catch (error) {
|
||||
this.resetForm()
|
||||
notify.error({
|
||||
title: 'Unhandled Job',
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.delete = id => xo.job.delete(id).then(refresh).then(() => {
|
||||
if (id === this.editedJobId) {
|
||||
this.resetForm()
|
||||
}
|
||||
})
|
||||
|
||||
this.run = id => {
|
||||
this.running[id] = true
|
||||
notify.info({
|
||||
title: 'Run Job',
|
||||
message: 'One shot running started. See overview for logs.'
|
||||
})
|
||||
return xo.job.runSequence([id]).finally(() => delete this.running[id])
|
||||
}
|
||||
})
|
||||
|
||||
.directive('stringInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && !includes(['id', 'host', 'host_id', 'target_host_id', 'sr', 'target_sr_id', 'vm', 'pool', 'subject', 'object', 'action'], this.key)
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: stringInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('booleanInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'boolean'
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: booleanInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('integerInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'integer'
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: integerInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('numberInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
bindToController: true,
|
||||
controller: function () {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'number'
|
||||
},
|
||||
controllerAs: 'ctrl',
|
||||
template: numberInputView
|
||||
}
|
||||
})
|
||||
|
||||
.directive('arrayInput', function ($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
controller: 'ArrayInput as ctrl',
|
||||
bindToController: true,
|
||||
link: function (scope, element, attrs) {
|
||||
const updateElement = () => {
|
||||
if (scope.ctrl.property.items) {
|
||||
element.append(arrayInputView)
|
||||
}
|
||||
$compile(element.contents())(scope)
|
||||
}
|
||||
|
||||
updateElement()
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('ArrayInput', function ($scope) {
|
||||
this.isRequired = () => false
|
||||
this.getType = getType
|
||||
this.active = () => getType(this.property) === 'array'
|
||||
this.add = value => {
|
||||
const type = getType(this.property.items)
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
value = Boolean(value)
|
||||
break
|
||||
case 'string':
|
||||
value = trim(value)
|
||||
break
|
||||
}
|
||||
this.model.push(value)
|
||||
}
|
||||
this.remove = index => this.model.splice(index, 1)
|
||||
const init = () => {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = []
|
||||
}
|
||||
}
|
||||
|
||||
if (this.active()) {
|
||||
init()
|
||||
if (!Array.isArray(this.model)) {
|
||||
throw new Error('arrayInput directive model must be an array')
|
||||
}
|
||||
$scope.$watch(() => this.model, init)
|
||||
}
|
||||
})
|
||||
|
||||
.directive('objectInput', function ($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
model: '=',
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '='
|
||||
},
|
||||
controller: 'ObjectInput as ctrl',
|
||||
bindToController: true,
|
||||
link: function (scope, element, attrs) {
|
||||
const updateElement = () => {
|
||||
if (scope.ctrl.property.properties) {
|
||||
element.append(objectInputView)
|
||||
}
|
||||
$compile(element.contents())(scope)
|
||||
}
|
||||
|
||||
updateElement()
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('ObjectInput', function ($scope) {
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'object' && (this.key !== 'object' || actionGroup.get() !== 'acl')
|
||||
const init = () => {
|
||||
if (this.model === undefined || this.model === null) {
|
||||
this.model = {
|
||||
__use: this.isRequired()
|
||||
}
|
||||
}
|
||||
if (typeof this.model !== 'object' || Array.isArray(this.model)) {
|
||||
throw new Error('objectInput directive model must be a plain object')
|
||||
}
|
||||
const use = this.model.__use
|
||||
delete this.model.__use
|
||||
this.model.__use = Object.keys(this.model).length > 0 || use
|
||||
forEach(this.property.properties, (property, key) => {
|
||||
if (getType(property) === 'boolean') {
|
||||
this.model[key] = Boolean(this.model[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (this.active()) {
|
||||
init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('vmInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'VmInput as ctrl',
|
||||
bindToController: true,
|
||||
template: vmInputView
|
||||
}
|
||||
})
|
||||
.controller('VmInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && (this.key === 'vm' || (actionGroup.get() === 'vm' && this.key === 'id'))
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('hostInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'HostInput as ctrl',
|
||||
bindToController: true,
|
||||
template: hostInputView
|
||||
}
|
||||
})
|
||||
.controller('HostInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && (includes(['host', 'host_id', 'target_host_id'], this.key) || (actionGroup.get() === 'host' && this.key === 'id'))
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('srInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'SrInput as ctrl',
|
||||
bindToController: true,
|
||||
template: srInputView
|
||||
}
|
||||
})
|
||||
.controller('SrInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && includes(['sr', 'sr_id', 'target_sr_id'], this.key)
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('poolInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'PoolInput as ctrl',
|
||||
bindToController: true,
|
||||
template: poolInputView
|
||||
}
|
||||
})
|
||||
.controller('PoolInput', function ($scope, xoApi) {
|
||||
this.objects = xoApi.all
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && includes(['pool', 'pool_id', 'target_pool_id'], this.key)
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
.directive('xoEntityInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'XoEntityInput as ctrl',
|
||||
bindToController: true,
|
||||
template: xoEntityInputView
|
||||
}
|
||||
})
|
||||
.controller('XoEntityInput', function ($scope, xo) {
|
||||
this.ready = false
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => this.ready && getType(this.property) === 'string' && this.key === 'subject' && actionGroup.get() === 'acl'
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
Bluebird.props({
|
||||
users: xo.user.getAll(),
|
||||
groups: xo.group.getAll()
|
||||
})
|
||||
.then(p => {
|
||||
this.objects = p.users.concat(p.groups)
|
||||
this.ready = true
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.directive('xoRoleInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'XoRoleInput as ctrl',
|
||||
bindToController: true,
|
||||
template: xoRoleInputView
|
||||
}
|
||||
})
|
||||
.controller('XoRoleInput', function ($scope, xo) {
|
||||
this.ready = false
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => this.ready && getType(this.property) === 'string' && this.key === 'action' && actionGroup.get() === 'acl'
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
xo.role.getAll()
|
||||
.then(roles => {
|
||||
this.objects = roles
|
||||
this.ready = true
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
.directive('xoObjectInput', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
form: '=',
|
||||
key: '=',
|
||||
property: '=',
|
||||
model: '='
|
||||
},
|
||||
controller: 'XoObjectInput as ctrl',
|
||||
bindToController: true,
|
||||
template: xoObjectInputView
|
||||
}
|
||||
})
|
||||
.controller('XoObjectInput', function ($scope, xoApi, filterFilter, selectHighLevelFilter) {
|
||||
const HIGH_LEVEL_OBJECTS = {
|
||||
pool: true,
|
||||
host: true,
|
||||
VM: true,
|
||||
SR: true,
|
||||
network: true
|
||||
}
|
||||
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
|
||||
this.objects = xoApi.all
|
||||
|
||||
this.isRequired = () => isRequired(this.property)
|
||||
this.active = () => getType(this.property) === 'string' && this.key === 'object' && actionGroup.get() === 'acl'
|
||||
this.toggleType = (toggle, type) => {
|
||||
const selectedObjects = this.intraModel && this.intraModel.slice() || []
|
||||
if (toggle) {
|
||||
const objects = filterFilter(selectHighLevelFilter(this.objects), {type})
|
||||
forEach(objects, object => { selectedObjects.indexOf(object) === -1 && selectedObjects.push(object) })
|
||||
this.intraModel = selectedObjects
|
||||
} else {
|
||||
const keptObjects = []
|
||||
for (let index in selectedObjects) {
|
||||
const object = selectedObjects[index]
|
||||
if (object.type !== type) {
|
||||
keptObjects.push(object)
|
||||
}
|
||||
}
|
||||
this.intraModel = keptObjects
|
||||
}
|
||||
this.model.length = 0
|
||||
forEach(this.intraModel, item => this.model.push(reduceXoObject(item)))
|
||||
}
|
||||
|
||||
this.init = _initXoObjectInput
|
||||
this.exportRemove = _exportRemove
|
||||
this.exportSelect = _exportSelect
|
||||
|
||||
if (this.active()) {
|
||||
this.init()
|
||||
$scope.$watch(() => this.model, (newVal, oldVal) => {
|
||||
if (newVal !== oldVal) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
6
app/modules/task-scheduler/job/integer-input-view.jade
Normal file
6
app/modules/task-scheduler/job/integer-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input.form-control(form = '{{ ctrl.form }}', type = 'number', step = '1', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')
|
||||
6
app/modules/task-scheduler/job/number-input-view.jade
Normal file
6
app/modules/task-scheduler/job/number-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input.form-control(form = '{{ ctrl.form }}', type = 'number', step = 'any', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')
|
||||
30
app/modules/task-scheduler/job/object-input-view.jade
Normal file
30
app/modules/task-scheduler/job/object-input-view.jade
Normal file
@@ -0,0 +1,30 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
.checkbox(ng-if = '!ctrl.isRequired()')
|
||||
label
|
||||
input(type = 'checkbox', ng-model = 'ctrl.model.__use')
|
||||
| Fill informations (optional)
|
||||
hr
|
||||
.help-block(ng-if = 'ctrl.isRequired()')
|
||||
span.text-warning Fill required informations
|
||||
hr
|
||||
fieldset.form-horizontal(ng-disabled = '!ctrl.isRequired() && !ctrl.model.__use', ng-hide = '!ctrl.isRequired() && !ctrl.model.__use')
|
||||
.form-group(ng-if = '(ctrl.property.properties | count) < 1')
|
||||
p.help-block No parameters
|
||||
div(ng-repeat = '(key, property) in ctrl.property.properties')
|
||||
array-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
boolean-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
host-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
integer-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
number-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
object-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
pool-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
sr-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
string-input(form = 'ctrl.form', model = 'ctrl.model[key]', key = 'key', property = 'property')
|
||||
vm-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
xo-entity-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
xo-object-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
xo-role-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
|
||||
15
app/modules/task-scheduler/job/pool-input-view.jade
Normal file
15
app/modules/task-scheduler/job/pool-input-view.jade
Normal file
@@ -0,0 +1,15 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Pools
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose pools')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "pool"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if = 'object.$container') ({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})
|
||||
15
app/modules/task-scheduler/job/sr-input-view.jade
Normal file
15
app/modules/task-scheduler/job/sr-input-view.jade
Normal file
@@ -0,0 +1,15 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| SRs
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose Storage Repositories')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "sr"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{ object.name_label }}
|
||||
span(ng-if = 'object.$container') ({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})
|
||||
6
app/modules/task-scheduler/job/string-input-view.jade
Normal file
6
app/modules/task-scheduler/job/string-input-view.jade
Normal file
@@ -0,0 +1,6 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| {{ ctrl.key }}
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
input.form-control(form = '{{ ctrl.form }}', type = 'text', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')
|
||||
68
app/modules/task-scheduler/job/view.jade
Normal file
68
app/modules/task-scheduler/job/view.jade
Normal file
@@ -0,0 +1,68 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-cogs
|
||||
| Jobs
|
||||
form#jobform(ng-submit = 'ctrl.save(ctrl.editedJobId, ctrl.jobName, ctrl.selectedAction, ctrl.formData)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-wrench
|
||||
| {{ ctrl.editedJobId ? "Edit" : "Create" }}
|
||||
.panel-body
|
||||
.alert.alert-warning(ng-if = 'ctrl.editedJobId') Editing Job ID: {{ ctrl.editedJobId }}
|
||||
fieldset.form-horizontal(ng-disabled = '!ctrl.ready')
|
||||
.form-group
|
||||
label.col-sm-2.control-label Job Name
|
||||
.col-sm-10
|
||||
input.form-control(form = 'jobform', type = 'text', ng-model = 'ctrl.jobName', required, placeholder = 'An explicit name for your job')
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.selectedAction ? (ctrl.selectedAction.group + ".") : "Action" }}
|
||||
.col-sm-10
|
||||
select.form-control(form = 'jobform', ng-model = 'ctrl.selectedAction', ng-options = 'action.command group by action.group for action in ctrl.actions', ng-change = 'ctrl.resetData()', required)
|
||||
option(value = '') -- Choose an action --
|
||||
p.help-block(ng-if = 'ctrl.selectedAction.info.description') {{ ctrl.selectedAction.info.description }}
|
||||
.form-group(ng-if = 'ctrl.selectedAction.info.permission')
|
||||
label.col-sm-2.control-label Permission
|
||||
.col-sm-10: p.form-control-static {{ ctrl.selectedAction.info.permission }}
|
||||
fieldset.form-horizontal(ng-if = 'ctrl.selectedAction', ng-disabled = '!ctrl.ready')
|
||||
legend Parameters
|
||||
object-input(form = '"jobform"', property = 'ctrl.selectedAction.info', model = 'ctrl.formData')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'jobform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetForm()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul
|
||||
| Jobs
|
||||
.panel-body
|
||||
.text-center(ng-if = '!(ctrl.jobs | count)') No jobs found
|
||||
table.table(ng-if = 'ctrl.jobs | count')
|
||||
tr
|
||||
th Name
|
||||
th Action
|
||||
th
|
||||
tr(ng-repeat = 'job in ctrl.jobs | map | orderBy:"name" track by job.id')
|
||||
td
|
||||
| {{ job.name }} 
|
||||
span.text-muted.hidden-xs ({{ job.id }})
|
||||
td {{ job.method }}
|
||||
td
|
||||
span.pull-left
|
||||
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(job.id)', ng-disabled = 'ctrl.running[job.id]'): i.fa.fa-play
|
||||
span.pull-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(job.id)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(job.id)'): i.fa.fa-trash
|
||||
17
app/modules/task-scheduler/job/vm-input-view.jade
Normal file
17
app/modules/task-scheduler/job/vm-input-view.jade
Normal file
@@ -0,0 +1,17 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| VMs
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose VMs')
|
||||
i.xo-icon-working(ng-if = 'isVMWorking($item)')
|
||||
i(ng-class = '"xo-icon-" + ($item.power_state | lowercase)', ng-if = '!isVMWorking($item)')
|
||||
| {{ $item.name_label }}
|
||||
span(ng-if = '$item.$container') ({{ ($item.$container | resolve).name_label }})
|
||||
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "vm"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
|
||||
div
|
||||
i.xo-icon-working(ng-if = 'isVMWorking(vm)', tooltip = '{{ vm.power_state }} and {{ (vm.current_operations | map)[0] }}')
|
||||
i(ng-class = '"xo-icon-" + (vm.power_state | lowercase)', ng-if = '!isVMWorking(vm)', tooltip = '{{ vm.power_state }}')
|
||||
| {{ vm.name_label }}
|
||||
span(ng-if = 'vm.$container') ({{ (vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label }})
|
||||
21
app/modules/task-scheduler/job/xo-entity-input-view.jade
Normal file
21
app/modules/task-scheduler/job/xo-entity-input-view.jade
Normal file
@@ -0,0 +1,21 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Users / Groups
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose users or groups')
|
||||
span(ng-if = '$item.email')
|
||||
i.xo-icon-user.fa-fw
|
||||
| {{$item.email}}
|
||||
span(ng-if = '$item.name')
|
||||
i.xo-icon-group.fa-fw
|
||||
| {{$item.name}}
|
||||
ui-select-choices(repeat = 'entity in ctrl.objects | filter:{ permission: "!admin" } | filter:$select.search')
|
||||
div
|
||||
span(ng-if = 'entity.email')
|
||||
i.xo-icon-user.fa-fw
|
||||
| {{entity.email}}
|
||||
span(ng-if = 'entity.name')
|
||||
i.xo-icon-group.fa-fw
|
||||
| {{entity.name}}
|
||||
28
app/modules/task-scheduler/job/xo-object-input-view.jade
Normal file
28
app/modules/task-scheduler/job/xo-object-input-view.jade
Normal file
@@ -0,0 +1,28 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Objects
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose an object')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name_label}}
|
||||
span(ng-if="($item.type === 'SR' || $item.type === 'VM') && $item.$container")
|
||||
| ({{($item.$container | resolve).name_label}})
|
||||
span(ng-if="$item.type === 'network'")
|
||||
| ({{($item.$poolId | resolve).name_label}})
|
||||
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]')
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{object.name_label}}
|
||||
span(ng-if="(object.type === 'SR' || object.type === 'VM') && object.$container")
|
||||
| ({{(object.$container | resolve).name_label}})
|
||||
span(ng-if="object.type === 'network'")
|
||||
| ({{(object.$poolId | resolve).name_label}})
|
||||
.text-center
|
||||
span(ng-repeat = 'type in ctrl.types')
|
||||
label(tooltip = 'select/deselect all {{type}}s', style = 'cursor: pointer')
|
||||
input.hidden(type = 'checkbox', ng-model = 'selectedTypes[type]', ng-change = 'ctrl.toggleType(selectedTypes[type], type)')
|
||||
span.fa-stack
|
||||
i(class = 'xo-icon-{{type | lowercase}}').fa-stack-1x
|
||||
i.fa.fa-square-o.fa-stack-2x.text-info(ng-if = 'selectedTypes[type]')
|
||||
13
app/modules/task-scheduler/job/xo-role-input-view.jade
Normal file
13
app/modules/task-scheduler/job/xo-role-input-view.jade
Normal file
@@ -0,0 +1,13 @@
|
||||
.form-group(ng-if = 'ctrl.active()')
|
||||
label.col-md-2.control-label(ng-if = 'ctrl.key')
|
||||
| Roles
|
||||
span.text-warning(ng-if = 'ctrl.isRequired()') *
|
||||
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
|
||||
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
|
||||
ui-select-match(placeholder = 'Choose a role')
|
||||
i(class = 'xo-icon-{{$item.type | lowercase}}')
|
||||
| {{$item.name}}
|
||||
ui-select-choices(repeat = 'role in ctrl.objects | filter:$select.search | orderBy:"name"')
|
||||
div
|
||||
i(class = 'xo-icon-{{role.type | lowercase}}')
|
||||
| {{role.name}}
|
||||
160
app/modules/task-scheduler/overview/index.js
Normal file
160
app/modules/task-scheduler/overview/index.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import forEach from 'lodash.foreach'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
const JOB_KEY = 'genericTask'
|
||||
|
||||
export default angular.module('taskscheduler.overview', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler.overview', {
|
||||
url: '/overview',
|
||||
controller: 'OverviewCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('OverviewCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
this.currentLogPage = 1
|
||||
this.logPageSize = 10
|
||||
|
||||
const refreshSchedules = () => {
|
||||
xo.schedule.getAll()
|
||||
.then(schedules => this.schedules = filter(schedules, schedule => this.jobs[schedule.job] && this.jobs[schedule.job].key === JOB_KEY))
|
||||
xo.scheduler.getScheduleTable()
|
||||
.then(table => this.scheduleTable = table)
|
||||
}
|
||||
|
||||
const getLogs = () => {
|
||||
xo.logs.get('jobs').then(logs => {
|
||||
const viewLogs = {}
|
||||
forEach(logs, (log, logKey) => {
|
||||
const data = log.data
|
||||
const [time] = logKey.split(':')
|
||||
if (data.event === 'job.start' && data.key === JOB_KEY) {
|
||||
viewLogs[logKey] = {
|
||||
logKey,
|
||||
jobId: data.jobId,
|
||||
key: data.key,
|
||||
userId: data.userId,
|
||||
start: time,
|
||||
calls: {},
|
||||
time
|
||||
}
|
||||
} else {
|
||||
const runJobId = data.runJobId
|
||||
const entry = viewLogs[runJobId]
|
||||
if (!entry) {
|
||||
return
|
||||
}
|
||||
|
||||
if (data.event === 'job.end') {
|
||||
if (data.error) {
|
||||
entry.error = data.error
|
||||
}
|
||||
entry.end = time
|
||||
entry.duration = time - entry.start
|
||||
entry.status = 'Terminated'
|
||||
} else if (data.event === 'jobCall.start') {
|
||||
entry.calls[logKey] = {
|
||||
callKey: logKey,
|
||||
params: resolveParams(data.params),
|
||||
method: data.method,
|
||||
time
|
||||
}
|
||||
} else if (data.event === 'jobCall.end') {
|
||||
const call = entry.calls[data.runCallId]
|
||||
|
||||
if (data.error) {
|
||||
call.error = data.error
|
||||
entry.hasErrors = true
|
||||
} else {
|
||||
call.returnedValue = resolveReturn(data.returnedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
forEach(viewLogs, log => {
|
||||
if (log.end === undefined) {
|
||||
log.status = 'In progress'
|
||||
}
|
||||
})
|
||||
|
||||
this.logs = viewLogs
|
||||
})
|
||||
}
|
||||
|
||||
const resolveParams = params => {
|
||||
for (let key in params) {
|
||||
const xoObject = xoApi.get(params[key])
|
||||
if (xoObject) {
|
||||
const newKey = xoObject.type || key
|
||||
params[newKey] = xoObject.name_label || xoObject.name || params[key]
|
||||
newKey !== key && delete params[key]
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
const resolveReturn = returnValue => {
|
||||
const xoObject = xoApi.get(returnValue)
|
||||
let xoName = xoObject && (xoObject.name_label || xoObject.name)
|
||||
xoName && (xoName += xoObject.type && ` (${xoObject.type})` || '')
|
||||
returnValue = xoName || returnValue
|
||||
return returnValue
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
const refreshJobs = () => {
|
||||
return xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => j[job.id] = job)
|
||||
this.jobs = j
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
refreshJobs().then(refreshSchedules)
|
||||
getLogs()
|
||||
}
|
||||
|
||||
refresh()
|
||||
const interval = $interval(() => {
|
||||
refresh()
|
||||
}, 5e3)
|
||||
$scope.$on('$destroy', () => {
|
||||
$interval.cancel(interval)
|
||||
})
|
||||
|
||||
this.enable = id => {
|
||||
this.working[id] = true
|
||||
return xo.scheduler.enable(id)
|
||||
.finally(() => { this.working[id] = false })
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
this.disable = id => {
|
||||
this.working[id] = true
|
||||
return xo.scheduler.disable(id)
|
||||
.finally(() => { this.working[id] = false })
|
||||
.then(refreshSchedules)
|
||||
}
|
||||
|
||||
this.displayScheduleJobName = schedule => this.jobs[schedule.job] && this.jobs[schedule.job].name
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
this.working = {}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
83
app/modules/task-scheduler/overview/view.jade
Normal file
83
app/modules/task-scheduler/overview/view.jade
Normal file
@@ -0,0 +1,83 @@
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-eye
|
||||
| Job Scheduling Overview
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedules
|
||||
.panel-body
|
||||
//- The 2 tables below are here for a "full-width" effect of the content vs the menu (cf sheduler/view.jade)
|
||||
table.table(ng-if = '!ctrl.schedules')
|
||||
tr
|
||||
td.text-center: i.xo-icon-loading
|
||||
table.table(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
td.text-center No scheduled jobs
|
||||
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
|
||||
tr
|
||||
th Name
|
||||
th Job
|
||||
th.hidden-xs Scheduling
|
||||
th State
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
|
||||
td: a(ui-sref = 'taskscheduler.schedule({id: schedule.id})') {{ schedule.name || schedule.id }}
|
||||
td
|
||||
a(ui-sref = 'taskscheduler.job({id: schedule.job})') {{ ctrl.displayScheduleJobName(schedule) || schedule.job }}
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td
|
||||
span.label.label-success.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === true') enabled
|
||||
span.label.label-default.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
|
||||
span.label.label-warning.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') unknown
|
||||
fieldset.pull-right(ng-disabled = 'ctrl.working[schedule.id]')
|
||||
button.btn(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)'): i.fa.fa-toggle-off
|
||||
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)'): i.fa.fa-toggle-on
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-file-text
|
||||
| Logs
|
||||
.panel-body
|
||||
table.table.table-hover(ng-if = 'ctrl.logs')
|
||||
thead
|
||||
tr
|
||||
th Job ID
|
||||
th Start
|
||||
th End
|
||||
th Duration
|
||||
th Status
|
||||
tbody(ng-repeat = 'log in ctrl.logs | map | filter:ctrl.logSearch | orderBy:"-time" | slice:(ctrl.logPageSize * (ctrl.currentLogPage - 1)):(ctrl.logPageSize * ctrl.currentLogPage) track by log.logKey')
|
||||
tr
|
||||
td
|
||||
button.btn.btn-sm(type = 'button', tooltip = 'See calls', ng-click = 'seeCalls = !seeCalls', ng-class = '{"btn-default": !log.hasErrors, "btn-danger": log.hasErrors}'): i.fa(ng-class = '{"fa-caret-down": !seeCalls, "fa-caret-up": seeCalls}')
|
||||
| {{ log.jobId }}
|
||||
td {{ log.start | date:'medium' }}
|
||||
td {{ log.end | date:'medium' }}
|
||||
td {{ log.duration | duration}}
|
||||
td
|
||||
span(ng-if = 'log.status === "Terminated"')
|
||||
span.label(ng-class = '{"label-success": (!log.error && !log.hasErrors), "label-danger": (log.error || log.hasErrors)}') {{ log.status }}
|
||||
span.label(ng-if = 'log.status !== "Terminated"', ng-class = '{"label-warning": log.status === "In progress", "label-default": !log.status}') {{ log.status || "unknown" }}
|
||||
p.text-danger(ng-if = 'log.error') {{ log.error }}
|
||||
tr.bg-info(collapse = '!seeCalls')
|
||||
td(colspan = '5')
|
||||
ul.list-group
|
||||
li.list-group-item(ng-repeat = 'call in log.calls | map | orderBy:"-time" track by call.callKey')
|
||||
strong.text-info {{ call.method }}: 
|
||||
span(ng-repeat = '(key, param) in call.params')
|
||||
| {{ key }}:
|
||||
strong {{ param }} 
|
||||
span(ng-if = 'call.returnedValue')
|
||||
|  
|
||||
i.text-primary.fa.fa-arrow-right
|
||||
|  {{ call.returnedValue }}
|
||||
span.text-danger(ng-if = 'call.error')
|
||||
|  
|
||||
i.fa.fa-times
|
||||
|  {{ call.error }}
|
||||
.form-inline
|
||||
.input-group
|
||||
.input-group-addon: i.fa.fa-search
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.logSearch', placeholder = 'Search logs...')
|
||||
.center(ng-if = '(ctrl.logs | map | filter:ctrl.logSearch | count) > ctrl.logPageSize || currentLogPage > 1')
|
||||
pagination.pagination-sm(boundary-links = 'true', total-items = 'ctrl.logs | map | filter:ctrl.logSearch | count', ng-model = 'ctrl.currentLogPage', items-per-page = 'ctrl.logPageSize', max-size = '10', previous-text = '<', next-text = '>', first-text = '<<', last-text = '>>')
|
||||
103
app/modules/task-scheduler/schedule/index.js
Normal file
103
app/modules/task-scheduler/schedule/index.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import angular from 'angular'
|
||||
import Bluebird from 'bluebird'
|
||||
import find from 'lodash.find'
|
||||
import forEach from 'lodash.foreach'
|
||||
import prettyCron from 'prettycron'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
Bluebird.longStackTraces()
|
||||
|
||||
import view from './view'
|
||||
|
||||
// ====================================================================
|
||||
|
||||
const JOB_KEY = 'genericTask'
|
||||
|
||||
export default angular.module('xoWebApp.taskscheduler.schedule', [
|
||||
uiRouter,
|
||||
uiBootstrap
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('taskscheduler.schedule', {
|
||||
url: '/schedule/:id',
|
||||
controller: 'ScheduleCtrl as ctrl',
|
||||
template: view
|
||||
})
|
||||
})
|
||||
|
||||
.controller('ScheduleCtrl', function (xo, xoApi, notify, $stateParams) {
|
||||
this.scheduleApi = {}
|
||||
this.formData = {}
|
||||
this.ready = false
|
||||
let comesForEditing = $stateParams.id
|
||||
|
||||
this.reset = () => {
|
||||
this.formData.editedScheduleId = undefined
|
||||
this.formData.scheduleName = undefined
|
||||
this.formData.selectedJob = undefined
|
||||
this.formData.enabled = false
|
||||
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
|
||||
}
|
||||
|
||||
this.reset()
|
||||
|
||||
const refreshJobs = () => xo.job.getAll().then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => {
|
||||
if (job.key === JOB_KEY) {
|
||||
j[job.id] = job
|
||||
}
|
||||
})
|
||||
this.jobs = j
|
||||
})
|
||||
|
||||
const refreshSchedules = () => xo.schedule.getAll().then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => {
|
||||
if (this.jobs && this.jobs[schedule.job] && (this.jobs[schedule.job].key === JOB_KEY)) {
|
||||
s[schedule.id] = schedule
|
||||
}
|
||||
})
|
||||
this.schedules = s
|
||||
})
|
||||
|
||||
const refresh = () => refreshJobs().then(refreshSchedules)
|
||||
const getReady = () => refresh().then(() => this.ready = true)
|
||||
getReady().then(() => {
|
||||
if (comesForEditing) {
|
||||
this.edit(comesForEditing)
|
||||
comesForEditing = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const saveNew = (name, job, cron, enabled) => xo.schedule.create(job.id, cron, enabled, name)
|
||||
const save = (id, name, job, cron) => xo.schedule.set(id, job.id, cron, undefined, name)
|
||||
|
||||
this.save = (id, name, job, cron, enabled) => {
|
||||
const saved = (id !== undefined) ? save(id, name, job, cron) : saveNew(name, job, cron, enabled)
|
||||
return saved
|
||||
.then(() => this.reset())
|
||||
.finally(refresh)
|
||||
}
|
||||
|
||||
this.edit = id => {
|
||||
this.reset()
|
||||
const schedule = this.schedules[id]
|
||||
if (schedule) {
|
||||
this.formData.editedScheduleId = schedule.id
|
||||
this.formData.scheduleName = schedule.name
|
||||
this.formData.selectedJob = find(this.jobs, job => job.id = schedule.job)
|
||||
this.scheduleApi.setCron(schedule.cron)
|
||||
}
|
||||
}
|
||||
|
||||
this.delete = (id) => xo.schedule.delete(id).then(refresh).then(() => {
|
||||
if (id === this.formData.editedScheduleId) {
|
||||
this.reset()
|
||||
}
|
||||
})
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
75
app/modules/task-scheduler/schedule/view.jade
Normal file
75
app/modules/task-scheduler/schedule/view.jade
Normal file
@@ -0,0 +1,75 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-clock-o
|
||||
| Job scheduler
|
||||
form#scheduleform(ng-submit = 'ctrl.save(ctrl.formData.editedScheduleId, ctrl.formData.scheduleName, ctrl.formData.selectedJob, ctrl.formData.cronPattern, ctrl.formData.enabled)')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs
|
||||
| Job to schedule
|
||||
.panel-body
|
||||
.alert.alert-warning(ng-if = 'ctrl.formData.editedScheduleId') Editing Schedule ID: {{ ctrl.formData.editedScheduleId }}
|
||||
fieldset.form-horizontal(ng-disabled = '!ctrl.ready')
|
||||
.form-group
|
||||
label.col-sm-2.control-label Schedule Name
|
||||
.col-sm-10
|
||||
input.form-control(form = 'scheduleform', type = 'text', ng-model = 'ctrl.formData.scheduleName', required, placeholder = 'An explicit name for your schedule')
|
||||
.form-group
|
||||
label.col-sm-2.control-label Job
|
||||
.col-sm-10
|
||||
select.form-control(form = 'scheduleform', ng-model = 'ctrl.formData.selectedJob', ng-options = '(job.name + " (" + job.id + ")") for job in (ctrl.jobs | map | orderBy:"name")', required)
|
||||
option(value = '') -- Choose a job --
|
||||
p.help-block(ng-if = 'ctrl.formData.selectedJob') {{ ctrl.selectedJob }}
|
||||
.form-group(ng-if = '!ctrl.formData.editedScheduleId')
|
||||
label.control-label.col-md-2(for = 'enabled')
|
||||
input#enabled(form = 'scheduleform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
|
||||
.help-block.col-md-10 Enable immediatly after creation
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-clock-o
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
|
||||
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
fieldset.center(ng-disabled = '!ctrl.ready')
|
||||
button.btn.btn-lg.btn-primary(form = 'scheduleform', type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.reset()')
|
||||
| Reset
|
||||
.grid-sm
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul
|
||||
| Schedules
|
||||
.panel-body
|
||||
.text-center(ng-if = '!(ctrl.schedules | count)') No schedules found
|
||||
table.table(ng-if = 'ctrl.schedules | count')
|
||||
tr
|
||||
th Name
|
||||
th Job
|
||||
th.hidden-xs Schedule
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules | map | orderBy:"name" track by schedule.id')
|
||||
td
|
||||
| {{ schedule.name }} 
|
||||
span.text-muted.hidden-xs ({{schedule.id}}) 
|
||||
br.visible-xs-block
|
||||
span.label.label-success(ng-if = 'schedule.enabled') enabled
|
||||
td {{ ctrl.jobs[schedule.job].name }} ({{ ctrl.jobs[schedule.job].method }})
|
||||
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
|
||||
td
|
||||
span.pull-right
|
||||
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule.id)'): i.fa.fa-pencil
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule.id)'): i.fa.fa-trash
|
||||
17
app/modules/task-scheduler/view.jade
Normal file
17
app/modules/task-scheduler/view.jade
Normal file
@@ -0,0 +1,17 @@
|
||||
.menu-grid
|
||||
.side-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.overview', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.fa-eye.fa-menu
|
||||
span.menu-entry Overview
|
||||
li
|
||||
a(ui-sref = '.job')
|
||||
i.fa.fa-fw.fa-cogs.fa-menu
|
||||
span.menu-entry Jobs
|
||||
li
|
||||
a(ui-sref = '.schedule')
|
||||
i.fa.fa-fw.fa-clock-o.fa-menu
|
||||
span.menu-entry Scheduler
|
||||
|
||||
.side-content(ui-view = '')
|
||||
@@ -310,16 +310,16 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
i.xo-icon-working(ng-if="isVMWorking(VM)", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
//- VM name.
|
||||
td.vm-name.col-xs-8.col-sm-2.col-md-2
|
||||
td.vm-name.col-xs-8.col-sm-3.col-md-3
|
||||
p.vm {{VM.name_label}}
|
||||
//- Quick actions.
|
||||
td.vm-quick-buttons.col-md-2.hidden-xs
|
||||
td.vm-quick-buttons.col-md-1.col-sm-1.hidden-xs
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="confirmAction('stopVM', VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Shutdown VM", xo-click="confirmAction('stopVM', VM.id)")
|
||||
i.fa.fa-stop
|
||||
a(tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
a(ng-if="VM.power_state == ('Halted')", tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="confirmAction('rebootVM', VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Reboot VM", xo-click="confirmAction('rebootVM', VM.id)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console
|
||||
@@ -364,20 +364,20 @@ div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
i.xo-icon-working(ng-if="isVMWorking(VM)", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
|
||||
i(class="xo-icon-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
|
||||
//- VM name.
|
||||
td.vm-name.col-xs-8.col-sm-2.col-md-2
|
||||
td.vm-name.col-xs-8.col-sm-3.col-md-3
|
||||
p.vm {{VM.name_label}}
|
||||
//- Quick actions.
|
||||
td.vm-quick-buttons.col-md-2.hidden-xs
|
||||
td.vm-quick-buttons.col-md-1.hidden-xs
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="stopVM(VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Shutdown VM", xo-click="stopVM(VM.id)")
|
||||
i.fa.fa-stop
|
||||
a(ng-if="VM.power_state == 'Suspended'", tooltip="Resume VM", xo-click="resumeVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(ng-if="VM.power_state != 'Suspended'", tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="rebootVM(VM.id)")
|
||||
a(ng-if="VM.power_state == ('Running' || 'Paused')", tooltip="Reboot VM", xo-click="rebootVM(VM.id)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console")
|
||||
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console
|
||||
//- Description.
|
||||
td.vm-description.col-md-4.hidden-xs
|
||||
|
||||
@@ -234,7 +234,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.savingBootOrder = true
|
||||
paramString = ''
|
||||
forEach(bootParams, (boot) -> boot.v && paramString += boot.e)
|
||||
return xoApi.call 'vm.bootOrder', {vm: id, order: paramString}
|
||||
return xoApi.call 'vm.setBootOrder', {vm: id, order: paramString}
|
||||
.finally () ->
|
||||
$scope.savingBootOrder = false
|
||||
$scope.bootReordering = false
|
||||
@@ -302,6 +302,19 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
message: 'Start VM'
|
||||
}
|
||||
|
||||
$scope.recoveryStartVM = (id) ->
|
||||
oldBootParams = vm.boot.order
|
||||
xoApi.call('vm.setBootOrder', {vm: id, order: 'dn'}).then(
|
||||
-> xo.vm.start id
|
||||
).then(
|
||||
->
|
||||
notify.info {
|
||||
title: 'VM starting...'
|
||||
message: 'Start VM'
|
||||
}
|
||||
return xoApi.call 'vm.setBootOrder', {vm: id, order: oldBootParams}
|
||||
)
|
||||
|
||||
$scope.stopVM = (id) ->
|
||||
modal.confirm
|
||||
title: 'VM shutdown'
|
||||
|
||||
@@ -255,7 +255,7 @@
|
||||
span(ng-if="VM.PV_drivers && !VM.PV_drivers_up_to_date") Outdated
|
||||
|
||||
//- Action panel
|
||||
.grid-sm
|
||||
.grid-sm(ng-if = 'canOperate()')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flash
|
||||
@@ -272,6 +272,9 @@
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
|
||||
button.btn(tooltip="Start VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="startVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-play.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted') && VM.virtualizationMode == 'hvm'")
|
||||
button.btn(tooltip="Start VM in recovery mode", tooltip-placement="top", type="button", style="width: 90%", xo-click="recoveryStartVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-forward.fa-2x.fa-fw
|
||||
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
|
||||
button.btn(tooltip="Reboot VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootVM(VM.id)", ng-if = 'canOperate()')
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
@@ -738,13 +741,15 @@
|
||||
.panel-body
|
||||
p.center(ng-if="!VM.snapshots.length") No snapshots
|
||||
table.table.table-hover(ng-if="VM.snapshots.length")
|
||||
th Date
|
||||
th Name
|
||||
th.col-md-4 Date
|
||||
th.col-md-8 Name
|
||||
tr(ng-repeat="snapshot in VM.snapshots | resolve | orderBy:'-snapshot_time' | slice:(5*(currentSnapPage-1)):(5*currentSnapPage) track by snapshot.id")
|
||||
td.oneliner {{snapshot.snapshot_time*1e3 | date:"medium"}}
|
||||
td.oneliner
|
||||
span(editable-text="snapshot.name_label", e-name="name_label", e-form="vmSnap", onbeforesave="saveSnapshot(snapshot.id, $data)")
|
||||
| {{snapshot.name_label}}
|
||||
span(ng-if="snapshot.tags | includes:'quiesce'")
|
||||
i.fa.fa-info-circle(tooltip = "Quiesced snapshot")
|
||||
| {{snapshot.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Export this snapshot", type="button", xo-click="exportVM(snapshot.id)", ng-if = 'canAdmin()')
|
||||
i.fa.fa-upload.fa-lg
|
||||
|
||||
33
app/node_modules/angular-no-vnc/index.js
generated
vendored
33
app/node_modules/angular-no-vnc/index.js
generated
vendored
@@ -77,7 +77,10 @@ export default angular.module('no-vnc', [])
|
||||
rfb = new RFB({
|
||||
encrypt: isSecure,
|
||||
target: canvas,
|
||||
wsProtocols: ['chat']
|
||||
wsProtocols: ['chat'],
|
||||
onClipboard (rfb, text) {
|
||||
setClipboard(text)
|
||||
}
|
||||
})
|
||||
|
||||
rfb._onUpdateState = (rfb, state) => {
|
||||
@@ -101,6 +104,11 @@ export default angular.module('no-vnc', [])
|
||||
}
|
||||
|
||||
this.remoteControl = {
|
||||
pasteToClipboard (text) {
|
||||
if (rfb) {
|
||||
rfb.clipboardPasteFrom(text)
|
||||
}
|
||||
},
|
||||
sendCtrlAltDel () {
|
||||
if (rfb) {
|
||||
rfb.sendCtrlAltDel()
|
||||
@@ -108,8 +116,28 @@ export default angular.module('no-vnc', [])
|
||||
}
|
||||
}
|
||||
|
||||
const setClipboard = (text) => {
|
||||
this.onClipboardChange({ clipboardContent: text })
|
||||
}
|
||||
|
||||
let canvas = $element.find('canvas')[0]
|
||||
|
||||
$scope.unfocus = () => {
|
||||
if (rfb) {
|
||||
rfb.get_keyboard().ungrab()
|
||||
rfb.get_mouse().ungrab()
|
||||
}
|
||||
}
|
||||
$scope.focus = () => {
|
||||
if (rfb) {
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
rfb.get_keyboard().grab()
|
||||
rfb.get_mouse().grab()
|
||||
}
|
||||
}
|
||||
|
||||
$attrs.$observe('url', (url) => {
|
||||
reset(url)
|
||||
})
|
||||
@@ -122,7 +150,8 @@ export default angular.module('no-vnc', [])
|
||||
controller: 'NoVncCtrl as noVnc',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
remoteControl: '='
|
||||
remoteControl: '=',
|
||||
onClipboardChange: '&'
|
||||
},
|
||||
template: view
|
||||
}
|
||||
|
||||
2
app/node_modules/angular-no-vnc/view.jade
generated
vendored
2
app/node_modules/angular-no-vnc/view.jade
generated
vendored
@@ -1,5 +1,7 @@
|
||||
canvas.center-block(
|
||||
height = "{{noVnc.height}}"
|
||||
width = "{{noVnc.width}}"
|
||||
ng-mouseenter = 'focus()'
|
||||
ng-mouseleave = 'unfocus()'
|
||||
)
|
||||
| Sorry, your browser does not support the canvas element.
|
||||
|
||||
314
app/node_modules/scheduler/index.js
generated
vendored
Normal file
314
app/node_modules/scheduler/index.js
generated
vendored
Normal file
@@ -0,0 +1,314 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import forEach from 'lodash.foreach'
|
||||
import indexOf from 'lodash.indexof'
|
||||
import later from 'later'
|
||||
import moment from 'moment'
|
||||
import prettyCron from 'prettycron'
|
||||
import remove from 'lodash.remove'
|
||||
|
||||
later.date.localTime()
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('xoWebApp.scheduler', [])
|
||||
|
||||
.directive('xoScheduler', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: view,
|
||||
controller: 'XoScheduler as ctrl',
|
||||
bindToController: true,
|
||||
scope: {
|
||||
data: '=',
|
||||
api: '='
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
.controller('XoScheduler', function () {
|
||||
this.init = () => {
|
||||
let i, j
|
||||
|
||||
const minutes = []
|
||||
for (i = 0; i < 6; i++) {
|
||||
minutes[i] = []
|
||||
for (j = 0; j < 10; j++) {
|
||||
minutes[i].push(10 * i + j)
|
||||
}
|
||||
}
|
||||
this.minutes = minutes
|
||||
|
||||
const hours = []
|
||||
for (i = 0; i < 3; i++) {
|
||||
hours[i] = []
|
||||
for (j = 0; j < 8; j++) {
|
||||
hours[i].push(8 * i + j)
|
||||
}
|
||||
}
|
||||
this.hours = hours
|
||||
|
||||
const days = []
|
||||
for (i = 0; i < 4; i++) {
|
||||
days[i] = []
|
||||
for (j = 1; j < 8; j++) {
|
||||
days[i].push(7 * i + j)
|
||||
}
|
||||
}
|
||||
days.push([29, 30, 31])
|
||||
this.days = days
|
||||
|
||||
this.months = [
|
||||
[
|
||||
{v: 1, l: 'Jan'},
|
||||
{v: 2, l: 'Feb'},
|
||||
{v: 3, l: 'Mar'},
|
||||
{v: 4, l: 'Apr'},
|
||||
{v: 5, l: 'May'},
|
||||
{v: 6, l: 'Jun'}
|
||||
],
|
||||
[
|
||||
{v: 7, l: 'Jul'},
|
||||
{v: 8, l: 'Aug'},
|
||||
{v: 9, l: 'Sep'},
|
||||
{v: 10, l: 'Oct'},
|
||||
{v: 11, l: 'Nov'},
|
||||
{v: 12, l: 'Dec'}
|
||||
]
|
||||
]
|
||||
|
||||
this.dayWeeks = [
|
||||
{v: 0, l: 'Sun'},
|
||||
{v: 1, l: 'Mon'},
|
||||
{v: 2, l: 'Tue'},
|
||||
{v: 3, l: 'Wed'},
|
||||
{v: 4, l: 'Thu'},
|
||||
{v: 5, l: 'Fri'},
|
||||
{v: 6, l: 'Sat'}
|
||||
]
|
||||
this.resetData()
|
||||
}
|
||||
|
||||
this.selectMinute = function (minute) {
|
||||
if (this.isSelectedMinute(minute)) {
|
||||
remove(this.data.minSelect, v => String(v) === String(minute))
|
||||
} else {
|
||||
this.data.minSelect.push(minute)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMinute = function (minute) {
|
||||
return indexOf(this.data.minSelect, minute) > -1 || indexOf(this.data.minSelect, String(minute)) > -1
|
||||
}
|
||||
|
||||
this.selectHour = function (hour) {
|
||||
if (this.isSelectedHour(hour)) {
|
||||
remove(this.data.hourSelect, v => String(v) === String(hour))
|
||||
} else {
|
||||
this.data.hourSelect.push(hour)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedHour = function (hour) {
|
||||
return indexOf(this.data.hourSelect, hour) > -1 || indexOf(this.data.hourSelect, String(hour)) > -1
|
||||
}
|
||||
|
||||
this.selectDay = function (day) {
|
||||
if (this.isSelectedDay(day)) {
|
||||
remove(this.data.daySelect, v => String(v) === String(day))
|
||||
} else {
|
||||
this.data.daySelect.push(day)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDay = function (day) {
|
||||
return indexOf(this.data.daySelect, day) > -1 || indexOf(this.data.daySelect, String(day)) > -1
|
||||
}
|
||||
|
||||
this.selectMonth = function (month) {
|
||||
if (this.isSelectedMonth(month)) {
|
||||
remove(this.data.monthSelect, v => String(v) === String(month))
|
||||
} else {
|
||||
this.data.monthSelect.push(month)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMonth = function (month) {
|
||||
return indexOf(this.data.monthSelect, month) > -1 || indexOf(this.data.monthSelect, String(month)) > -1
|
||||
}
|
||||
|
||||
this.selectDayWeek = function (dayWeek) {
|
||||
if (this.isSelectedDayWeek(dayWeek)) {
|
||||
remove(this.data.dayWeekSelect, v => String(v) === String(dayWeek))
|
||||
} else {
|
||||
this.data.dayWeekSelect.push(dayWeek)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDayWeek = function (dayWeek) {
|
||||
return indexOf(this.data.dayWeekSelect, dayWeek) > -1 || indexOf(this.data.dayWeekSelect, String(dayWeek)) > -1
|
||||
}
|
||||
|
||||
this.noMinutePlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the minSelect array with lodash.remove
|
||||
return this.data.min === 'select' && this.data.minSelect.length === 1 && String(this.data.minSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.minSelect = [0]
|
||||
this.data.min = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.noHourPlan = function (set = false) {
|
||||
if (!set) {
|
||||
// The last part (after &&) of this expression is reliable because we maintain the hourSelect array with lodash.remove
|
||||
return this.data.hour === 'select' && this.data.hourSelect.length === 1 && String(this.data.hourSelect[0]) === '0'
|
||||
} else {
|
||||
this.data.hourSelect = [0]
|
||||
this.data.hour = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.resetData = () => {
|
||||
this.data.minRange = 5
|
||||
this.data.hourRange = 2
|
||||
this.data.minSelect = [0]
|
||||
this.data.hourSelect = []
|
||||
this.data.daySelect = []
|
||||
this.data.monthSelect = []
|
||||
this.data.dayWeekSelect = []
|
||||
this.data.min = 'select'
|
||||
this.data.hour = 'all'
|
||||
this.data.day = 'all'
|
||||
this.data.month = 'all'
|
||||
this.data.dayWeek = 'all'
|
||||
this.data.cronPattern = '* * * * *'
|
||||
this.data.summary = []
|
||||
this.data.previewLimit = 0
|
||||
|
||||
this.update()
|
||||
}
|
||||
|
||||
this.update = () => {
|
||||
const d = this.data
|
||||
const i = (d.min === 'all' && '*') ||
|
||||
(d.min === 'range' && ('*/' + d.minRange)) ||
|
||||
(d.min === 'select' && d.minSelect.join(',')) ||
|
||||
'*'
|
||||
const h = (d.hour === 'all' && '*') ||
|
||||
(d.hour === 'range' && ('*/' + d.hourRange)) ||
|
||||
(d.hour === 'select' && d.hourSelect.join(',')) ||
|
||||
'*'
|
||||
const dm = (d.day === 'all' && '*') ||
|
||||
(d.day === 'select' && d.daySelect.join(',')) ||
|
||||
'*'
|
||||
const m = (d.month === 'all' && '*') ||
|
||||
(d.month === 'select' && d.monthSelect.join(',')) ||
|
||||
'*'
|
||||
const dw = (d.dayWeek === 'all' && '*') ||
|
||||
(d.dayWeek === 'select' && d.dayWeekSelect.join(',')) ||
|
||||
'*'
|
||||
this.data.cronPattern = i + ' ' + h + ' ' + dm + ' ' + m + ' ' + dw
|
||||
|
||||
const tabState = {
|
||||
min: {
|
||||
all: d.min === 'all',
|
||||
range: d.min === 'range',
|
||||
select: d.min === 'select'
|
||||
},
|
||||
hour: {
|
||||
all: d.hour === 'all',
|
||||
range: d.hour === 'range',
|
||||
select: d.hour === 'select'
|
||||
},
|
||||
day: {
|
||||
all: d.day === 'all',
|
||||
range: d.day === 'range',
|
||||
select: d.day === 'select'
|
||||
},
|
||||
month: {
|
||||
all: d.month === 'all',
|
||||
select: d.month === 'select'
|
||||
},
|
||||
dayWeek: {
|
||||
all: d.dayWeek === 'all',
|
||||
select: d.dayWeek === 'select'
|
||||
}
|
||||
}
|
||||
this.tabs = tabState
|
||||
this.summarize()
|
||||
}
|
||||
|
||||
this.summarize = () => {
|
||||
const schedule = later.parse.cron(this.data.cronPattern)
|
||||
const occurences = later.schedule(schedule).next(25)
|
||||
this.data.summary = []
|
||||
forEach(occurences, occurence => {
|
||||
this.data.summary.push(moment(occurence).format('LLLL'))
|
||||
})
|
||||
}
|
||||
|
||||
const cronToData = (data, cron) => {
|
||||
const d = Object.create(null)
|
||||
const cronItems = cron.split(' ')
|
||||
|
||||
if (cronItems[0] === '*') {
|
||||
d.min = 'all'
|
||||
} else if (cronItems[0].indexOf('/') !== -1) {
|
||||
d.min = 'range'
|
||||
const [, range] = cronItems[0].split('/')
|
||||
d.minRange = range
|
||||
} else {
|
||||
d.min = 'select'
|
||||
d.minSelect = cronItems[0].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[1] === '*') {
|
||||
d.hour = 'all'
|
||||
} else if (cronItems[1].indexOf('/') !== -1) {
|
||||
d.hour = 'range'
|
||||
const [, range] = cronItems[1].split('/')
|
||||
d.hourRange = range
|
||||
} else {
|
||||
d.hour = 'select'
|
||||
d.hourSelect = cronItems[1].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[2] === '*') {
|
||||
d.day = 'all'
|
||||
} else {
|
||||
d.day = 'select'
|
||||
d.daySelect = cronItems[2].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[3] === '*') {
|
||||
d.month = 'all'
|
||||
} else {
|
||||
d.month = 'select'
|
||||
d.monthSelect = cronItems[3].split(',')
|
||||
}
|
||||
|
||||
if (cronItems[4] === '*') {
|
||||
d.dayWeek = 'all'
|
||||
} else {
|
||||
d.dayWeek = 'select'
|
||||
d.dayWeekSelect = cronItems[4].split(',')
|
||||
}
|
||||
|
||||
assign(data, d)
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
this.api.setCron = cron => {
|
||||
cronToData(this.data, cron)
|
||||
this.update()
|
||||
}
|
||||
this.api.resetData = this.resetData.bind(this)
|
||||
|
||||
this.init()
|
||||
})
|
||||
|
||||
.name
|
||||
9
app/node_modules/scheduler/package.json
generated
vendored
Normal file
9
app/node_modules/scheduler/package.json
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify",
|
||||
"browserify-plain-jade"
|
||||
]
|
||||
}
|
||||
}
|
||||
104
app/node_modules/scheduler/view.jade
generated
vendored
Normal file
104
app/node_modules/scheduler/view.jade
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
accordion(ng-if = 'ctrl.data', close-others= 'false', ng-click = 'ctrl.update()')
|
||||
accordion-group
|
||||
accordion-heading Month
|
||||
tabset
|
||||
tab(select = 'ctrl.data.month = "all"', active = 'ctrl.tabs.month.all')
|
||||
tab-heading every month
|
||||
tab(select = 'ctrl.data.month = "select"', active = 'ctrl.tabs.month.select')
|
||||
tab-heading each selected month
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.months')
|
||||
td(ng-click = 'ctrl.selectMonth(month.v)', ng-class = '{"bg-success": ctrl.isSelectedMonth(month.v)}',ng-repeat = 'month in line') {{ month.l }}
|
||||
accordion-group
|
||||
accordion-heading Day of the month
|
||||
tabset
|
||||
tab(select = 'ctrl.data.day = "all"', active = 'ctrl.tabs.day.all')
|
||||
tab-heading every day
|
||||
tab(select = 'ctrl.data.day = "select"', active = 'ctrl.tabs.day.select')
|
||||
tab-heading each selected day
|
||||
br
|
||||
p.text-warning
|
||||
i.fa.fa-warning
|
||||
| This selection can restrict or be restricted by "Day of week" selections below. Use the summary preview to ensure your choice.
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.days')
|
||||
td(ng-click = 'ctrl.selectDay(day)', ng-class = '{"bg-success": ctrl.isSelectedDay(day)}',ng-repeat = 'day in line') {{ day }}
|
||||
accordion-group
|
||||
accordion-heading Day of week
|
||||
tabset
|
||||
tab(select = 'ctrl.data.dayWeek = "all"', active = 'ctrl.tabs.dayWeek.all')
|
||||
tab-heading every day of week
|
||||
tab(select = 'ctrl.data.dayWeek = "select"', active = 'ctrl.tabs.dayWeek.select')
|
||||
tab-heading each selected day of week
|
||||
br
|
||||
p.text-warning
|
||||
i.fa.fa-warning
|
||||
| This selection can restrict or be restricted by "Day of the month" selections up ahead. Use the summary preview to ensure your choice.
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr
|
||||
td(ng-click = 'ctrl.selectDayWeek(dayWeek.v)', ng-class = '{"bg-success": ctrl.isSelectedDayWeek(dayWeek.v)}',ng-repeat = 'dayWeek in ctrl.dayWeeks') {{ dayWeek.l }}
|
||||
accordion-group
|
||||
accordion-heading Hour
|
||||
button.btn.btn-primary(ng-if = '!ctrl.noHourPlan()', type = 'button', ng-click = 'ctrl.noHourPlan(true)') Plan nothing on a hourly grain
|
||||
button.btn.btn-primary.disabled(ng-if = 'ctrl.noHourPlan()', type = 'button')
|
||||
i.fa.fa-info-circle
|
||||
| Nothing planned on a hourly grain
|
||||
br
|
||||
br
|
||||
tabset
|
||||
tab(select = 'ctrl.data.hour = "all"', active = 'ctrl.tabs.hour.all')
|
||||
tab-heading every hour
|
||||
tab(select = 'ctrl.data.hour = "range"', active = 'ctrl.tabs.hour.range')
|
||||
tab-heading every N hour
|
||||
br
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.data.hourRange }}
|
||||
.col-sm-10
|
||||
input.form-control(type = 'range', min = '2', max = '23', step = '1', ng-model = 'ctrl.data.hourRange', ng-change = 'ctrl.update()')
|
||||
tab(select = 'ctrl.data.hour = "select"', active = 'ctrl.tabs.hour.select')
|
||||
tab-heading each selected hour
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.hours')
|
||||
td(ng-click = 'ctrl.selectHour(hour)', ng-class = '{"bg-success": ctrl.isSelectedHour(hour)}',ng-repeat = 'hour in line') {{ hour }}
|
||||
accordion-group
|
||||
accordion-heading Minute
|
||||
button.btn.btn-primary(ng-if = '!ctrl.noMinutePlan()', type = 'button', ng-click = 'ctrl.noMinutePlan(true)') Plan nothing on a minute grain
|
||||
button.btn.btn-primary.disabled(ng-if = 'ctrl.noMinutePlan()', type = 'button')
|
||||
i.fa.fa-info-circle
|
||||
| Nothing planned on a minute grain
|
||||
br
|
||||
br
|
||||
tabset
|
||||
tab(select = 'ctrl.data.min = "all"', active = 'ctrl.tabs.min.all')
|
||||
tab-heading every minute
|
||||
tab(select = 'ctrl.data.min = "range"', active = 'ctrl.tabs.min.range')
|
||||
tab-heading every N minutes
|
||||
br
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.data.minRange }}
|
||||
.col-sm-10
|
||||
input.form-control(type = 'range', min = '2', max = '59', step = '1', ng-model = 'ctrl.data.minRange', ng-change = 'ctrl.update()')
|
||||
tab(select = 'ctrl.data.min = "select"', active = 'ctrl.tabs.min.select')
|
||||
tab-heading each selected minute
|
||||
br
|
||||
table.table.table-bordered
|
||||
tr(ng-repeat = 'line in ctrl.minutes')
|
||||
td(ng-click = 'ctrl.selectMinute(min)', ng-class = '{"bg-success": ctrl.isSelectedMinute(min)}',ng-repeat = 'min in line') {{ min }}
|
||||
input.form-control.hidden(type ='text', readonly, ng-model = 'ctrl.data.cronPattern')
|
||||
.text-center(ng-if = '!ctrl.data'): i.xo-icon-loading
|
||||
div(ng-if = 'ctrl.data')
|
||||
p
|
||||
strong Scheduled to run:
|
||||
| {{ ctrl.prettyCron(ctrl.data.cronPattern) }}
|
||||
.form-inline.container-fluid
|
||||
.form-group
|
||||
label Preview:
|
||||
input.form-control(type = 'range', min = '0', max = '{{ ctrl.data.summary.length - 3 }}', step = '1', ng-model = 'ctrl.data.previewLimit')
|
||||
br
|
||||
ul
|
||||
li(ng-repeat = 'occurence in ctrl.data.summary | limitTo: +ctrl.data.previewLimit+3') {{ occurence }}
|
||||
li ...
|
||||
2
app/node_modules/xo-filters/index.js
generated
vendored
2
app/node_modules/xo-filters/index.js
generated
vendored
@@ -1,5 +1,6 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import includes from 'lodash.includes'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import map from 'lodash.map'
|
||||
import slice from 'lodash.slice'
|
||||
@@ -105,6 +106,7 @@ export default angular.module('xoWebApp.filters', [
|
||||
|
||||
.filter('isEmpty', () => isEmpty)
|
||||
.filter('isNotEmpty', () => (collection) => !isEmpty(collection))
|
||||
.filter('includes', () => includes)
|
||||
|
||||
.filter('duration', () => (n, unit = 'ms') => (n > 0 && moment.duration(n, unit).humanize() || ''))
|
||||
|
||||
|
||||
25
app/node_modules/xo/index.js
generated
vendored
25
app/node_modules/xo/index.js
generated
vendored
@@ -129,12 +129,15 @@ export default angular.module('xo', [
|
||||
listMissingPatches: action('Check available patches', 'host.listMissingPatches', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
installPatch: action('Install a patch from a patch id', 'host.installPatch', {
|
||||
installPatch: action('Install a patch from a patch id', 'host.installPatch', {
|
||||
argsMapper: (host, patch) => ({host, patch})
|
||||
}),
|
||||
installAllPatches: action('Install all the missing patches on a host', 'host.installAllPatches', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
emergencyShutdownHost: action('Suspend all VMs running on host and shutdown host', 'host.emergencyShutdownHost', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
refreshStats: action('Get Stats', 'host.stats', {
|
||||
notification: false,
|
||||
argsMapper: (host, granularity) => ({host, granularity})
|
||||
@@ -228,6 +231,12 @@ export default angular.module('xo', [
|
||||
}),
|
||||
restart: action('Restart a Docker Container', 'docker.restart', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
register: action('Register the VM for the Docker plugin', 'docker.register', {
|
||||
argsMapper: (vm) => ({ vm })
|
||||
}),
|
||||
deregister: action('Deregister the VM for the Docker plugin', 'docker.deregister', {
|
||||
argsMapper: (vm) => ({ vm })
|
||||
})
|
||||
},
|
||||
|
||||
@@ -281,6 +290,12 @@ export default angular.module('xo', [
|
||||
notification: false,
|
||||
argsMapper: (id, granularity) => ({id, granularity})
|
||||
}),
|
||||
getCloudInitConfig: action('Get Cloud Init Template', 'vm.getCloudInitConfig', {
|
||||
argsMapper: (template) => ({ template })
|
||||
}),
|
||||
createCloudInitConfigDrive: action('Create Cloud Config Drive', 'vm.createCloudInitConfigDrive', {
|
||||
argsMapper: (vm, sr, config) => ({ vm, sr, config })
|
||||
}),
|
||||
// TODO: create/set/pause
|
||||
connectPci: action('Connect PCI device', 'vm.attachPci', {
|
||||
argsMapper: (vm, pciId) => ({vm, pciId})
|
||||
@@ -319,20 +334,24 @@ export default angular.module('xo', [
|
||||
}),
|
||||
delete: action('Delete a job', 'job.delete', {
|
||||
argsMapper: (id) => ({id})
|
||||
}),
|
||||
runSequence: action('Run a sequence of jobs', 'job.runSequence', {
|
||||
argsMapper: (idSequence) => ({idSequence})
|
||||
})
|
||||
},
|
||||
|
||||
schedule: {
|
||||
getAll: action('Get all schedules', 'schedule.getAll'),
|
||||
create: action('Create a schedule', 'schedule.create', {
|
||||
argsMapper: (jobId, cron, enabled) => ({jobId, cron, enabled})
|
||||
argsMapper: (jobId, cron, enabled, name) => ({jobId, cron, enabled, name})
|
||||
}),
|
||||
set: action('Modify a schedule', 'schedule.set', {
|
||||
argsMapper: (id, jobId = undefined, cron = undefined, enabled = undefined) => {
|
||||
argsMapper: (id, jobId = undefined, cron = undefined, enabled = undefined, name = undefined) => {
|
||||
const args = {id}
|
||||
jobId !== undefined && (args.jobId = jobId)
|
||||
cron !== undefined && (args.cron = cron)
|
||||
enabled !== undefined && (args.enabled = enabled)
|
||||
name !== undefined && (args.name = name)
|
||||
return args
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xo-web",
|
||||
"version": "4.9.0",
|
||||
"version": "4.10.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -27,6 +27,7 @@
|
||||
"browserify": "^12.0.1",
|
||||
"browserify-plain-jade": "^0.2.2",
|
||||
"bundle-collapser": "^1.1.4",
|
||||
"clipboard": "^1.5.5",
|
||||
"coffeeify": "^1.0.0",
|
||||
"d3": "^3.5.5",
|
||||
"event-stream": "^3.3.0",
|
||||
@@ -62,6 +63,7 @@
|
||||
"lodash.isempty": "^3.0.3",
|
||||
"lodash.keys": "^3.1.2",
|
||||
"lodash.map": "^3.1.2",
|
||||
"lodash.mapvalues": "^3.0.1",
|
||||
"lodash.omit": "^3.1.0",
|
||||
"lodash.pluck": "^3.1.0",
|
||||
"lodash.pull": "^3.0.1",
|
||||
|
||||
Reference in New Issue
Block a user