Initial work on React/Redux.

This commit is contained in:
Julien Fontanet 2015-12-03 18:22:13 +01:00
parent 0c173fde53
commit 9abd9d20ec
163 changed files with 409 additions and 18652 deletions

View File

@ -2,13 +2,9 @@
"comments": false, "comments": false,
"compact": true, "compact": true,
"optional": [ "optional": [
// Experimental features.
// "minification.constantFolding",
// "minification.deadCodeElimination",
"es7.asyncFunctions", "es7.asyncFunctions",
"es7.decorators", "es7.decorators",
"es7.exportExtensions",
"es7.functionBind" "es7.functionBind"
//"runtime"
] ]
} }

11
.gitignore vendored
View File

@ -1,10 +1,9 @@
/.nyc_output/
/bower_components/ /bower_components/
/dist/ /dist/
/node_modules/*
!/node_modules/*.js
/node_modules/*.js/
jsconfig.json
.idea
npm-debug.log npm-debug.log
npm-debug.log.*
!node_modules/*
node_modules/*/

5
.mocha.js Normal file
View File

@ -0,0 +1,5 @@
Error.stackTraceLimit = 100
try { require('trace') } catch (_) {}
try { require('clarify') } catch (_) {}
try { require('source-map-support/register') } catch (_) {}

1
.mocha.opts Normal file
View File

@ -0,0 +1 @@
--require ./.mocha.js

View File

@ -1,5 +1,6 @@
/examples/ /examples/
example.js example.js
example.js.map
*.example.js *.example.js
*.example.js.map *.example.js.map

View File

@ -1,7 +1,9 @@
language: node_js language: node_js
node_js: node_js:
- 'stable' - 'stable'
- '4'
- '0.12' - '0.12'
- '0.10'
# Use containers. # Use containers.
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/ # http://docs.travis-ci.com/user/workers/container-based-infrastructure/

View File

@ -1,189 +0,0 @@
// Must be loaded before angular.
import 'angular-file-upload'
import angular from 'angular'
import uiBootstrap from'angular-ui-bootstrap'
import uiIndeterminate from'angular-ui-indeterminate'
import uiRouter from'angular-ui-router'
import uiSelect from'angular-ui-select'
import naturalSort from 'angular-natural-sort'
import xeditable from 'angular-xeditable'
import xoDirectives from 'xo-directives'
import xoFilters from 'xo-filters'
import xoServices from 'xo-services'
import aboutState from './modules/about'
import backupState from './modules/backup'
import consoleState from './modules/console'
import dashboardState from './modules/dashboard'
import deleteVmsState from './modules/delete-vms'
import genericModalState from './modules/generic-modal'
import hostState from './modules/host'
import listState from './modules/list'
import navbarState from './modules/navbar'
import newSrState from './modules/new-sr'
import newVmState from './modules/new-vm'
import poolState from './modules/pool'
import settingsState from './modules/settings'
import srState from './modules/sr'
import treeState from './modules/tree'
import updater from './modules/updater'
import vmState from './modules/vm'
import '../dist/bower_components/angular-chart.js/dist/angular-chart.js'
// ===================================================================
export default angular.module('xoWebApp', [
uiBootstrap,
uiIndeterminate,
uiRouter,
uiSelect,
naturalSort,
xeditable,
xoDirectives,
xoFilters,
xoServices,
aboutState,
backupState,
consoleState,
dashboardState,
deleteVmsState,
genericModalState,
hostState,
listState,
navbarState,
newSrState,
newVmState,
poolState,
settingsState,
srState,
treeState,
updater,
vmState,
'chart.js'
])
// Prevent Angular.js from mangling exception stack (interfere with
// source maps).
.factory('$exceptionHandler', () => function (exception) {
throw exception
})
.config(function (
$compileProvider,
$stateProvider,
$urlRouterProvider,
$tooltipProvider,
uiSelectConfig
) {
// Disable debug data to improve performance.
//
// In case of a bug, simply use `angular.reloadWithDebugInfo()` in
// the console.
//
// See https://docs.angularjs.org/guide/production
$compileProvider.debugInfoEnabled(false)
// Redirect to default state.
$stateProvider.state('index', {
url: '/',
controller: function ($state, xoApi) {
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
$state.go(isAdmin ? 'tree' : 'list')
}
})
// Redirects unmatched URLs to `/`.
$urlRouterProvider.otherwise('/')
// Changes the default settings for the tooltips.
$tooltipProvider.options({
appendToBody: true,
placement: 'bottom'
})
uiSelectConfig.theme = 'bootstrap'
uiSelectConfig.resetSearchInput = true
})
.run(function (
$anchorScroll,
$rootScope,
$state,
editableOptions,
editableThemes,
notify,
updater,
xoApi
) {
let requestedStateName, requestedStateParams
$rootScope.$watch(() => xoApi.user, (user, previous) => {
// The user just signed in.
if (user && !previous) {
if (requestedStateName) {
$state.go(requestedStateName, requestedStateParams)
requestedStateName = requestedStateParams = null
} else {
$state.go('index')
}
}
})
$rootScope.$on('$stateChangeStart', function (event, state, stateParams, fromState) {
const { user } = xoApi
if (!user) {
event.preventDefault()
requestedStateName = state.name
requestedStateParams = stateParams
return
}
if (user.permission === 'admin') {
return
}
function forbidState () {
event.preventDefault()
notify.error({
title: 'Restricted area',
message: 'You do not have the permission to view this page'
})
if (fromState.url === '^') {
$state.go('index')
}
}
// Some pages requires the admin permission.
if (state.data && state.data.requireAdmin) {
forbidState()
return
}
const { id } = stateParams
if (id && !xoApi.canInteract(id, 'view')) {
forbidState()
return
}
})
// Work around UI Router bug (https://github.com/angular-ui/ui-router/issues/1509)
$rootScope.$on('$stateChangeSuccess', function () {
$anchorScroll()
})
editableThemes.bs3.inputClass = 'input-sm'
editableThemes.bs3.buttonsClass = 'btn-sm'
editableOptions.theme = 'bs3'
})
.name

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,62 +0,0 @@
//- HTML 5 Doctype
doctype html
//- The “no-js” class will be automatically removed if JavaScript is
//- available.
html.no-js(lang="en", dir="ltr")
head
meta(charset="utf-8")
//- This file is a part of Xen Orchestra Web.
//-
//- Xen Orchestra Web is free software: you can redistribute it and/or
//- modify it under the terms of the GNU Affero General Public License
//- as published by the Free Software Foundation, either version 3 of
//- the License, or (at your option) any later version.
//-
//- Xen Orchestra Web is distributed in the hope that it will be
//- useful, but WITHOUT ANY WARRANTY; without even the implied warranty
//- of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//- Affero General Public License for more details.
//-
//- You should have received a copy of the GNU Affero General Public License
//- along with Xen Orchestra Web. If not, see
//- <http://www.gnu.org/licenses/>.
//-
//- @author Olivier Lambert <olivier.lambert@vates.fr>
//- @license http://www.gnu.org/licenses/agpl-3.0-standalone.html GNU AGPLv3
//-
//- @package Xen Orchestra Web
//- Makes sure IE is using the last engine available.
meta(http-equiv="X-UA-Compatible", content="IE=edge,chrome=1")
//- Replaces the “no-js” class by the “js” class if JavaScript is
//- available.
script.
!function(d){d.className=d.className.replace(/\\bno-js\b/,'js')}(document.documentElement)
//- (To confirm.) For smartphones and tablets: sets the page
//- width to the device width and prevents the page from being
//- zoomed in when going to landscape mode.
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title Xen Orchestra
meta(name="description", content="Web interface for XenServer/XAPI Hosts")
meta(name="author", content="Vates SAS")
//- Place favicon.ico and apple-touch-icon.png in the root directory
link(rel="stylesheet", href="styles/main.css")
body(
ng-app = 'xoWebApp'
)
toaster-container
//- Navigation bar.
navbar
//- Main content (managed by the router).
.view-main(ui-view = "")
script(src="bower_components/Chart.js/Chart.min.js")
script(src="app.js")

View File

@ -1,22 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import pkg from '../../../package'
// ===================================================================
export default angular.module('xoWebApp.about', [
uiRouter
])
.config(function ($stateProvider) {
$stateProvider.state('about', {
url: '/about',
controller: 'AboutCtrl',
template: require('./view')
})
})
.controller('AboutCtrl', function ($scope) {
$scope.pkg = pkg
})
// A module exports its name.
.name

View File

@ -1,50 +0,0 @@
//- TODO: lots of stuff.
.grid-sm
.panel.panel-default
p.page-title About Xen Orchestra
p.text-center ({{pkg.name}} {{pkg.version}})
.grid-sm
//- Vates
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-lightbulb-o
| Vates
.panel-body
p.text-center
| We are the team behind Xen Orchestra, we are Vates! We create Open Source products and we offer commercial support for Xen and Xen Orchestra. Want to know more about us? Go to our website!
p.text-center
img(src="images/arrow.png")
br
p.text-center
a.btn.btn-success(href="https://vates.fr")
i.fa.fa-hand-o-right
| Our website
//- Open Source
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-thumbs-up
| Open Source
.panel-body
p.text-center
| This project is Open Source (AGPL), everyone is welcome aboard! You want a specific feature in XO? Report a bug? Go to our project website, read the FAQ and get involved in the project!
p.text-center
img(src="images/opensource.png")
br
p.text-center
a.btn.btn-info(href="https://xen-orchestra.com")
i.fa.fa-flask
| Project website
//- Pro support
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-truck
| Pro Support Delivered
.panel-body
p.text-center
| Our XO Appliance can be delivered with professional support: stay relaxed, we got your back! You can also have assitance for deploying or upgrade your virtualized infrastructure through our deep understanding of Xen.
p.text-center
img(src="images/support.png")
p.text-center
a.btn.btn-primary(href="https://vates.fr/services.html")
i.fa.fa-envelope
| Get services

View File

@ -1,257 +0,0 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import map from 'lodash.map'
import prettyCron from 'prettycron'
import size from 'lodash.size'
import trim from 'lodash.trim'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import view from './view'
// ====================================================================
export default angular.module('backup.backup', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.backup', {
url: '/backup/:id',
controller: 'BackupCtrl as ctrl',
template: view
})
})
.controller('BackupCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'rollingBackup'
this.ready = false
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshRemotes = () => {
const selectRemoteId = this.formData.remote && this.formData.remote.id
return xo.remote.getAll()
.then(remotes => {
const r = {}
forEach(remotes, remote => {
r[remote.id] = remote
})
this.remotes = r
if (selectRemoteId) {
this.formData.remote = this.remotes[selectRemoteId]
}
})
}
const refreshSchedules = () => {
return xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
}
const refreshJobs = () => {
return xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => {
j[job.id] = job
})
this.jobs = j
})
}
const refresh = () => refreshRemotes().then(refreshJobs).then(refreshSchedules)
this.getReady = () => refresh().then(() => this.ready = true)
this.getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => $interval.cancel(interval))
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const depth = job.paramsVector.items[0].values[0].depth
const cronPattern = schedule.cron
const remoteId = job.paramsVector.items[0].values[0].remoteId
const onlyMetadata = job.paramsVector.items[0].values[0].onlyMetadata || false
let compress = job.paramsVector.items[0].values[0].compress
if (compress === undefined) {
compress = true // Default value
}
this.resetData()
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.formData.remote = this.remotes[remoteId]
this.formData.disableCompression = !compress
this.formData.onlyMetadata = onlyMetadata
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to backup'
})
return
}
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression) : save(id, vms, remoteId, tag, depth, cron, onlyMetadata, disableCompression)
return _save
.then(() => {
notify.info({
title: 'Backup',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, remoteId, tag, depth, cron, onlyMetadata, disableCompression) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
remoteId,
tag,
depth,
onlyMetadata,
compress: !disableCompression
})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression) => {
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
remoteId,
tag,
depth,
onlyMetadata,
compress: !disableCompression
})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingBackup',
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values
}]
}
}
return xo.job.create(job)
.then(jobId => xo.schedule.create(jobId, cron, enabled))
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.path = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.formData.remote = undefined
this.formData.onlyMetadata = false
this.formData.disableCompression = false
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.size = size
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@ -1,144 +0,0 @@
.panel.panel-default
p.page-title
i.fa.fa-download(style="color: #e25440;")
| Backup
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData.onlyMetadata, ctrl.formData.disableCompression)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm
| VMs to backup
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Backup
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Backup ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'backupform', ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select(form = 'backupform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to backup')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(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(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}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'backupform', ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
.form-group
label.control-label.col-md-2(for = 'remote') Remote
.col-md-10
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes', ng-model = 'ctrl.formData.remote' required)
option(value = ''): em -- Choose a file system remote point --
.form-group
.col-md-10.col-md-offset-2
a(ui-sref = 'backup.remote')
i.fa.fa-pencil
| &nbsp; Manage your remote stores
.form-group
label.control-label.col-md-2(for = 'onlyMetadata')
input#onlyMetadata(form = 'backupform', ng-model = 'ctrl.formData.onlyMetadata', type = 'checkbox')
.help-block.col-md-10 Only MetaData (no disks export)
.form-group
label.control-label.col-md-2(for = 'onlyMetadata')
input#disableCompression(form = 'backupform', ng-model = 'ctrl.formData.disableCompression', type = 'checkbox')
.help-block.col-md-10 Disable compression
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'backupform', 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 = 'backupform', type = 'submit')
i.fa.fa-clock-o
| &nbsp;
i.fa.fa-arrow-right
| &nbsp;
i.fa.fa-database
| &nbsp;Save&nbsp;
| &nbsp;
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
| &nbsp;Reset&nbsp;
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-ul
| Schedules
.panel-body
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.size(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.size(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to backup
th.hidden-xs Remote
th.hidden-xs Depth
th.hidden-xs Scheduling
th.hidden-xs Only MetaData.hidden-xs
th.hidden-xs Compression DISABLED
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs&nbsp;
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
br
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
span(ng-if = '(item.id | resolve).$container') &nbsp;({{ ((item.id | resolve).$container | resolve).name_label }})
td.hidden-xs
strong: a(ui-sref = 'scheduler.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].remoteId].name }}
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.hidden-xs.text-center
i.fa.fa-check(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values[0].onlyMetadata')
td.hidden-xs.text-center
i.fa.fa-check(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values[0].compress === false')
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@ -1,213 +0,0 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import later from 'later'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import view from './view'
// ====================================================================
export default angular.module('backup.disasterrecovery', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.disasterrecovery', {
url: '/disasterrecovery/:id',
controller: 'DisasterRecoveryCtrl as ctrl',
template: view
})
})
.controller('DisasterRecoveryCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'disasterRecovery'
this.ready = false
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshSchedules = () => xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
const refreshJobs = () => xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => {
j[job.id] = job
})
this.jobs = j
})
const refresh = () => refreshJobs().then(refreshSchedules)
const getReady = () => refresh().then(() => this.ready = true)
getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => $interval.cancel(interval))
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const selectedPool = xoApi.get(job.paramsVector.items[0].values[0].pool)
const depth = job.paramsVector.items[0].values[0].depth
const cronPattern = schedule.cron
this.resetData()
// const formData = this.formData
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.selectedPool = selectedPool
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, pool, depth, cron, enabled) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to copy'
})
return
}
const _save = (id === undefined) ? saveNew(vms, tag, pool, depth, cron, enabled) : save(id, vms, tag, pool, depth, cron)
return _save
.then(() => {
notify.info({
title: 'Disaster Recovery',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, tag, pool, depth, cron) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({id: vm.id, tag, pool: pool.id, depth})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, tag, pool, depth, cron, enabled) => {
const values = []
forEach(vms, vm => {
values.push({id: vm.id, tag, pool: pool.id, depth})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingDrCopy',
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values
}]
}
}
return xo.job.create(job)
.then(jobId => xo.schedule.create(jobId, cron, enabled))
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.inTargetPool = vm => vm.$poolId === (this.formData.selectedPool && this.formData.selectedPool.id)
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.selectedPool = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.collectionLength = col => Object.keys(col).length
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@ -1,143 +0,0 @@
.panel.panel-default
p.page-title
i.fa.fa-medkit(style="color: #e25440;")
| Disaster Recovery
form#drform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.selectedPool, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm(style='color: #e25440;')
| VMs to copy
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Disaster Recovery
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Disaster Recovery ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
.input-group
span.input-group-addon DR_
input#tag.form-control(form = 'drform', ng-model = 'ctrl.formData.tag', placeholder = 'VM copy tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select#vmlist(form = 'drform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to copy')
span(ng-class = '{"bg-danger": ctrl.inTargetPool($item)}')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(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(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}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'drform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'drform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group(ng-if = '(ctrl.formData.selectedVms | filter:ctrl.inTargetPool).length')
.col-md-offset-2.col-md-10
.alert.alert-warning
i.fa.fa-exclamation-triangle
| &nbsp;At the moment, the selected VMs displayed in red are in the copy target pool.
.form-group
label.control-label.col-md-2(for = 'pool') To Pool
.col-md-10
ui-select#pool(form = 'drform', ng-model = 'ctrl.formData.selectedPool', required)
ui-select-match(placeholder = 'Choose destination pool')
i(class="xo-icon-pool")
| {{$select.selected.name_label}}
span(ng-if="$select.selected.$container")
| ({{($select.selected.$container | resolve).name_label}})
ui-select-choices(repeat = 'pool in ctrl.objects | selectHighLevel | filter:{type: "pool"} | filter:$select.search | orderBy:["$container", "name_label"] track by pool.id')
div
i(class="xo-icon-pool")
| {{pool.name_label}}
span(ng-if="pool.$container")
| ({{(pool.$container | resolve).name_label || ((pool.$container | resolve).master | resolve).name_label}})
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'drform', ng-model = 'ctrl.formData.depth', placeholder = 'How many VM copies to rollover', type = 'number', min = '1', required)
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'drform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-8 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 = 'drform', type = 'submit')
i.fa.fa-clock-o
| &nbsp;
i.fa.fa-arrow-right
| &nbsp;
i.fa.fa-database
| &nbsp;Save&nbsp;
| &nbsp;
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
| &nbsp;Reset&nbsp;
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-ul
| Schedules
.panel-body
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to Copy
th.hidden-xs To Pool
th.hidden-xs Depth
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td
span.label.label-default DR_
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs&nbsp;
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
span(ng-if = '(item.id | resolve).$container') &nbsp;({{ ((item.id | resolve).$container | resolve).name_label }})
td.hidden-xs {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].pool | resolve).name_label }}
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@ -1,350 +0,0 @@
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 uiRouter from 'angular-ui-router'
later.date.localTime()
import backup from './backup'
import disasterRecovery from './disaster-recovery'
import management from './management'
import mount from './remote'
import restore from './restore'
import rollingSnapshot from './rolling-snapshot'
import view from './view'
import scheduler from './scheduler'
export default angular.module('backup', [
uiRouter,
backup,
disasterRecovery,
management,
mount,
restore,
rollingSnapshot
])
.config(function ($stateProvider) {
$stateProvider.state('backup', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/backup'
})
// Redirect to default sub-state.
$stateProvider.state('backup.index', {
url: '',
controller: function ($state) {
$state.go('backup.management')
}
})
})
.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

View File

@ -1,163 +0,0 @@
import angular from 'angular'
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'
// ====================================================================
export default angular.module('backup.management', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.management', {
url: '/management',
controller: 'ManagementCtrl as ctrl',
template: view
})
})
.controller('ManagementCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const mapJobKeyToState = {
'rollingSnapshot': 'rollingsnapshot',
'rollingBackup': 'backup',
'disasterRecovery': 'disasterrecovery'
}
const mapJobKeyToJobDisplay = {
'rollingSnapshot': 'Rolling Snapshot',
'rollingBackup': 'Backup',
'disasterRecovery': 'Disaster Recovery'
}
this.currentLogPage = 1
this.logPageSize = 10
const refreshSchedules = () => {
xo.schedule.getAll()
.then(schedules => this.schedules = schedules)
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') {
viewLogs[logKey] = {
logKey,
jobId: data.jobId,
key: data.key,
userId: data.userId,
start: time,
calls: {},
time
}
} else {
const runJobId = data.runJobId
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] = {
callKey: logKey,
params: resolveParams(data.params),
time
}
} else if (data.event === 'jobCall.end') {
const call = viewLogs[runJobId].calls[data.runCallId]
if (data.error) {
call.error = data.error
viewLogs[runJobId].hasErrors = true
} else {
call.returnedValue = data.returnedValue
}
}
}
})
forEach(viewLogs, log => {
if (log.end === undefined) {
log.status = 'In progress'
}
})
this.logs = viewLogs
})
}
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]
}
}
}
return params
}
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.resolveJobKey = schedule => mapJobKeyToState[this.jobs[schedule.job].key]
this.displayJobKey = schedule => mapJobKeyToJobDisplay[this.jobs[schedule.job].key]
this.displayLogKey = log => mapJobKeyToJobDisplay[log.key]
this.collectionLength = col => Object.keys(col).length
this.working = {}
})
// A module exports its name.
.name

View File

@ -1,87 +0,0 @@
.panel.panel-default
p.page-title
i.fa.fa-eye(style="color: #e25440;")
| Backup 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 ID
th Job
th 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
span.text-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true')
| enabled&nbsp;
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
.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 Job
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
| {{ log.jobId }}&nbsp;
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}')
td {{ ctrl.displayLogKey(log) }}
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') &nbsp;{{ log.error }}
tr.bg-info(collapse = '!seeCalls')
td(colspan = '6')
ul.list-group
li.list-group-item(ng-repeat = 'call in log.calls | map | orderBy:"-time" track by call.callKey')
span(ng-repeat = '(key, param) in call.params')
strong {{ key }}:
| &nbsp;{{ param }}&nbsp;
span(ng-if = 'call.returnedValue')
| &nbsp;
i.text-primary.fa.fa-arrow-right
| &nbsp;{{ call.returnedValue }}
span.text-danger(ng-if = 'call.error')
| &nbsp;
i.fa.fa-times
| &nbsp;{{ 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 = '>>')

View File

@ -1,67 +0,0 @@
import angular from 'angular'
import filter from 'lodash.filter'
import map from 'lodash.map'
import trim from 'lodash.trim'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import size from 'lodash.size'
import view from './view'
// ====================================================================
export default angular.module('backup.remote', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.remote', {
url: '/remote',
controller: 'RemoteCtrl as ctrl',
template: view
})
})
.controller('RemoteCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
this.ready = false
const refresh = () => {
return xo.remote.getAll()
.then(remotes => this.backUpRemotes = remotes)
}
this.getReady = () => {
return refresh()
.then(() => this.ready = true)
}
this.getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.sanitizePath = (...paths) => filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
this.prepareUrl = (type, host, path) => {
let url = type + ':/'
if (type === 'nfs') {
url += '/' + host + ':'
}
url += '/' + this.sanitizePath(path)
return url
}
const reset = () => {
this.path = this.host = this.name = undefined
this.remoteType = 'file'
}
this.add = (name, url) => xo.remote.create(name, url).then(reset).then(refresh)
this.remove = id => xo.remote.delete(id).then(refresh)
this.enable = id => { console.log('GO !!!'); xo.remote.set(id, undefined, undefined, true).then(refresh) }
this.disable = id => xo.remote.set(id, undefined, undefined, false).then(refresh)
this.size = size
reset()
})
// A module exports its name.
.name

View File

@ -1,84 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-plug(style="color: #e25440;")
| Remotes stores for backup
.grid-sm
.panel.panel-default
.panel-body
//- {{ ctrl.backUpRemotes }} {{ ctrl.size(ctrl.backUpRemotes) }}
.text-center(ng-if = '!ctrl.size(ctrl.backUpRemotes)') No remotes
table.table.table-hover(ng-if = 'ctrl.size(ctrl.backUpRemotes)')
tbody(ng-if = '(ctrl.backUpRemotes | filter:{type:"local"}).length')
tr
th.text-info Local
th Name
th Path
th State
th Error
th
tr(ng-repeat = 'remote in ctrl.backUpRemotes | filter:{type:"local"} | orderBy:["name"] track by remote.id')
td
td {{ remote.name }}
td {{ remote.path }}
td
span(ng-if = 'remote.enabled')
span.text-success
| Accessible&nbsp;
i.fa.fa-check
//- button.btn.btn-warning.pull-right(type = 'button', ng-click = 'ctrl.disable(remote.id)'): i.fa.fa-chain-broken
span(ng-if = '!remote.enabled')
span.text-muted Unaccessible&nbsp;
button.btn.btn-primary.pull-right(type = 'button', ng-click = 'ctrl.enable(remote.id)'): i.fa.fa-link
td: span.text-muted {{ remote.error }}
td: button.btn.btn-danger.pull-right(type = 'button', ng-click = 'ctrl.remove(remote.id)'): i.fa.fa-trash
tbody(ng-if = '(ctrl.backUpRemotes | filter:{type:"nfs"}).length')
tr
th.text-info NFS
th Name
th Device
th State
th Error
th
tr(ng-repeat = 'remote in ctrl.backUpRemotes | filter:{type:"nfs"} | orderBy:["name"] track by remote.id')
td
td {{ remote.name }}
td {{ remote.host }}:{{ remote.share }}
td
span(ng-if = 'remote.enabled')
span.text-success
| Mounted&nbsp;
i.fa.fa-check
button.btn.btn-warning.pull-right(type = 'button', ng-click = 'ctrl.disable(remote.id)'): i.fa.fa-chain-broken
span(ng-if = '!remote.enabled')
span.text-muted Unmounted&nbsp;
button.btn.btn-primary.pull-right(type = 'button', ng-click = 'ctrl.enable(remote.id)'): i.fa.fa-link
td: span.text-muted {{ remote.error }}
td: button.btn.btn-danger.pull-right(type = 'button', ng-click = 'ctrl.remove(remote.id)'): i.fa.fa-trash
form(ng-submit = 'ctrl.add(ctrl.name, ctrl.prepareUrl(ctrl.remoteType, ctrl.host, ctrl.path))')
fieldset
legend New File System Remote
.form-inline
.form-group
label.sr-only Type
select.form-control(ng-model = 'ctrl.remoteType')
option(value = 'file') Local
option(value = 'nfs') NFS
| &nbsp;
.form-group
label.sr-only Name
input.form-control(type = 'text', ng-model = 'ctrl.name', placeholder = 'Name', required)
| &nbsp;
.form-group(ng-if = 'ctrl.remoteType === "nfs"')
label.sr-only Host
input.form-control(type = 'text', ng-model = 'ctrl.host', placeholder = 'host', required)
strong &nbsp;:&nbsp;
.input-group
span.input-group-addon /
label.sr-only Path&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.path', placeholder = 'path', required)
| &nbsp;
.form-group
button.btn.btn-primary.pull-right(type = 'submit', ng-disabled = '!ctrl.ready')
| Save&nbsp;
i.fa.fa-floppy-o

View File

@ -1,67 +0,0 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import size from 'lodash.size'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import view from './view'
// ====================================================================
export default angular.module('backup.restore', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.restore', {
url: '/restore',
controller: 'RestoreCtrl as ctrl',
template: view
})
})
.controller('RestoreCtrl', function ($scope, $interval, xo, xoApi, notify, $upload) {
this.loaded = {}
this.hosts = xoApi.getView('hosts')
const refresh = () => {
return xo.remote.getAll()
.then(remotes => {
forEach(this.backUpRemotes, remote => {
if (remote.files) {
const freshRemote = find(remotes, {id: remote.id})
freshRemote && (freshRemote.files = remote.files)
}
})
this.backUpRemotes = remotes
})
}
refresh()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.list = id => {
return xo.remote.list(id)
.then(files => {
const remote = find(this.backUpRemotes, {id})
remote && (remote.files = files)
this.loaded[remote.id] = true
})
}
this.importVm = (id, file, host) => {
notify.info({
title: 'VM import started',
message: 'Starting the VM import'
})
return xo.remote.importVm(id, file, host)
}
this.size = size
})
// A module exports its name.
.name

View File

@ -1,37 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-upload(style="color: #e25440;")
| Backup Restore
.grid-sm
.panel.panel-default
.panel-body
.text-center(ng-if = '!ctrl.size(ctrl.backUpRemotes)') No remotes
.panel.panel-default(ng-repeat = 'remote in ctrl.backUpRemotes | orderBy:["name"] track by remote.id')
.panel-body(ng-if = '!remote.enabled || remote.error', ng-class = '{"bg-danger": remote.error, "bg-muted": !remote.error}')
a(ui-sref = 'backup.remote') {{ remote.name }}
span(ng-if = 'remote.error') &nbsp;(on error)
span(ng-if = '!remote.error') &nbsp;(disabled)
.panel-body(ng-if = 'remote.enabled')
.row
.col-sm-3
p
| {{ remote.name }}&nbsp;
button.btn.btn-default.pull-right(type = 'button', ng-click = 'ctrl.list(remote.id)'): i.fa(ng-class = '{"fa-eye": !ctrl.loaded[remote.id], "fa-refresh": ctrl.loaded[remote.id]}')
br
br
.col-sm-9
div(ng-if = 'ctrl.loaded[remote.id] && !ctrl.size(remote.files)') No backups available
div(ng-if = 'ctrl.size(remote.files)')
div(ng-repeat = 'file in remote.files')
| {{ file }}
span.pull-right.dropdown(dropdown)
button.btn.btn-default(type = 'button', dropdown-toggle)
| Import&nbsp;
span.caret
ul.dropdown-menu(role="menu")
li(ng-repeat = 'h in ctrl.hosts.all | orderBy:natural("name_label") track by h.id')
a(xo-click = "ctrl.importVm(remote.id, file, h.id)")
i.xo-icon-host.fa-fw
| To {{h.name_label}}
hr

View File

@ -1,231 +0,0 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import later from 'later'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import view from './view'
// ====================================================================
export default angular.module('backup.rollingSnapshot', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.rollingsnapshot', {
url: '/rollingsnapshot/:id',
controller: 'RollingSnapshotCtrl as ctrl',
template: view
})
})
.controller('RollingSnapshotCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'rollingSnapshot'
this.ready = false
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshSchedules = () => {
return xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
}
const refreshJobs = () => {
return xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => j[job.id] = job)
this.jobs = j
})
}
const refresh = () => {
return refreshJobs().then(refreshSchedules)
}
this.getReady = () => refresh().then(() => this.ready = true)
this.getReady()
const interval = $interval(() => {
refresh()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const depth = job.paramsVector.items[0].values[0].depth
const cronPattern = schedule.cron
this.resetData()
// const formData = this.formData
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, depth, cron, enabled) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to snapshot'
})
return
}
const _save = (id === undefined) ? saveNew(vms, tag, depth, cron, enabled) : save(id, vms, tag, depth, cron)
return _save
.then(() => {
notify.info({
title: 'Rolling snapshot',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(() => {
refresh()
})
}
const save = (id, vms, tag, depth, cron) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
tag,
depth
})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, tag, depth, cron, enabled) => {
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
tag,
depth
})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingSnapshot',
paramsVector: {
type: 'crossProduct',
items: [
{
type: 'set',
values
}
]
}
}
return xo.job.create(job)
.then(jobId => {
return xo.schedule.create(jobId, cron, enabled)
})
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.collectionLength = col => Object.keys(col).length
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@ -1,117 +0,0 @@
.panel.panel-default
p.page-title
i.xo-icon-snapshot(style="color: #e25440;")
| Rolling snapshots
form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm(style='color: #e25440;')
| VMs to snapshot
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Rolling Snapshot
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Rolling Snapshot ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'snapform', ng-model = 'ctrl.formData.tag', placeholder = 'Rolling snapshot tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select(form = 'snapform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to snapshot')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(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(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}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'snapform', ng-model = 'ctrl.formData.depth', placeholder = 'How many snapshots to rollover', type = 'number', min = '1', required)
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'snapform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-8 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 = 'snapform', type = 'submit')
i.fa.fa-clock-o
| &nbsp;
i.fa.fa-arrow-right
| &nbsp;
i.fa.fa-database
| &nbsp;Save&nbsp;
| &nbsp;
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
| &nbsp;Reset&nbsp;
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-ul
| Schedules
.panel-body
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to snapshot
th.hidden-xs Depth
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs&nbsp;
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
br
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
span(ng-if = '(item.id | resolve).$container') &nbsp;({{ ((item.id | resolve).$container | resolve).name_label }})
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@ -1,104 +0,0 @@
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
| &nbsp;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
| &nbsp;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
| &nbsp;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
| &nbsp;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:&nbsp;
| {{ ctrl.prettyCron(ctrl.data.cronPattern) }}
.form-inline.container-fluid
.form-group
label Preview:&nbsp;
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 ...

View File

@ -1,29 +0,0 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.management', ui-sref-active = 'active')
i.fa.fa-fw.fa-eye.fa-menu
span.menu-entry Overview
li
a(ui-sref = '.rollingsnapshot')
i.xo-icon-snapshot.fa-fw.fa-menu
span.menu-entry Rolling snapshots
li
a(ui-sref = '.remote')
i.fa.fa-fw.fa-plug.fa-menu
span.menu-entry Remote stores
li
a(ui-sref = '.backup')
i.fa.fa-fw.fa-download.fa-menu
span.menu-entry Backup
li
a(ui-sref = '.restore')
i.fa.fa-fw.fa-upload.fa-menu
span.menu-entry Restore
li
a(ui-sref = '.disasterrecovery')
i.fa.fa-fw.fa-medkit.fa-menu
span.menu-entry Disaster Recovery
.side-content(ui-view = '')

View File

@ -1,93 +0,0 @@
angular = require 'angular'
forEach = require('lodash.foreach')
includes = require('lodash.includes')
isoDevice = require('iso-device')
#=====================================================================
module.exports = angular.module 'xoWebApp.console', [
require 'angular-ui-router'
require 'angular-no-vnc'
isoDevice
]
.config ($stateProvider) ->
$stateProvider.state 'consoles_view',
url: '/consoles/:id'
controller: 'ConsoleCtrl'
template: require './view'
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo, xoHideUnauthorizedFilter, modal) ->
{id} = $stateParams
{get} = xoApi
pool = null
host = null
do (
srsByContainer = xoApi.getIndex('srsByContainer')
poolSrs = null
hostSrs = null
) ->
updateSrs = () =>
srs = []
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
$scope.SRs = xoHideUnauthorizedFilter(srs)
$scope.$watchCollection(
() => pool and srsByContainer[pool.id],
(srs) =>
poolSrs = srs
updateSrs()
)
$scope.$watchCollection(
() => host and srsByContainer[host.id],
(srs) =>
hostSrs = srs
updateSrs()
)
$scope.$watch(
-> xoApi.get id
(VM) ->
$scope.consoleUrl = null
unless xoApi.user
$scope.VDIs = []
return
$scope.VM = VM
return unless (
VM? and
VM.power_state is 'Running' and
not includes(VM.current_operations, 'clean_reboot')
)
pool = get VM.$poolId
return unless pool
$scope.consoleUrl = "./api/consoles/#{id}"
host = get VM.$container # host because the VM is running.
)
$scope.startVM = xo.vm.start
$scope.stopVM = (id) ->
modal.confirm
title: 'VM shutdown'
message: 'Are you sure you want to shutdown this VM ?'
.then ->
xo.vm.stop id
$scope.rebootVM = (id) ->
modal.confirm
title: 'VM reboot'
message: 'Are you sure you want to reboot this VM ?'
.then ->
xo.vm.restart id
$scope.eject = ->
xo.vm.ejectCd id
$scope.insert = (disc_id) ->
xo.vm.insertCd id, disc_id, true
# A module exports its name.
.name

View File

@ -1,54 +0,0 @@
.container: .panel.panel-default
//- Title
p.page-title
span.fa-stack
i.fa.fa-square-o.fa-stack-2x
i.xo-icon-console.fa-stack-1x(class = 'xo-color-{{VM.power_state | lowercase}}')
| &nbsp;
a(
class = 'xo-color-{{VM.power_state | lowercase}}'
ui-sref = 'VMs_view({id: VM.id})'
) {{VM.name_label}}
.list-group
//- 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(
ng-click = 'vncRemote.sendCtrlAltDel()'
)
i.fa.fa-keyboard-o
| &nbsp;
| Ctrl+Alt+Del
//- Action panel
.col-sm-3
.btn-group
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Stop VM"
type = "button"
xo-click = "stopVM(VM.id)"
)
i.fa.fa-stop.fa-fw
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Halted')"
tooltip = "Start VM"
type = "button"
xo-click = "startVM(VM.id)"
)
i.fa.fa-play.fa-fw
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Reboot VM"
type = "button"
xo-click = "rebootVM(VM.id)"
)
i.fa.fa-refresh.fa-fw
//- Console
.list-group-item
no-vnc(
url = '{{consoleUrl}}'
remote-control = 'vncRemote'
)

View File

@ -1,369 +0,0 @@
'use strict'
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import debounce from 'lodash.debounce'
import filter from 'lodash.filter'
import foreach from 'lodash.foreach'
import xoApi from 'xo-api'
import xoCircleD3 from 'xo-circle-d3'
import xoParallelD3 from 'xo-parallel-d3'
import xoSunburstD3 from 'xo-sunburst-d3'
import view from './view'
export default angular.module('dashboard.dataviz', [
uiRouter,
uiSelect,
xoApi,
xoCircleD3,
xoParallelD3,
xoSunburstD3
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.dataviz', {
controller: 'Dataviz as ctrl',
data: {
requireAdmin: true
},
url: '/dataviz/:chart',
template: view
})
})
.filter('type', () => {
return function (objects, type) {
if (!type) {
return objects
}
return filter(objects, object => object.type === type)
}
})
.controller('Dataviz', function ($scope, $state) {
$scope.selectedChart = ''
$scope.availablecharts = {
sunburst: {
name: 'Sunburst charts',
imgs: ['images/sunburst.png', 'images/sunburst2.png'],
url: '/dataviz/sunburst'
},
circle: {
name: 'Circles charts',
imgs: ['images/circle1.png', 'images/circle2.png'],
url: '/dataviz/circle'
},
parcoords: {
name: 'VM properties',
imgs: ['images/parcoords.png'],
url: '/dataviz/parcoords'
}
}
$scope.$on('$stateChangeSuccess', function updatePage () {
$scope.selectedChart = $state.params.chart
})
})
.controller('DatavizParcoords', function DatavizParcoords (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
let hostsByPool, vmsByContainer, data
data = []
hostsByPool = xoApi.getIndex('hostsByPool')
vmsByContainer = xoApi.getIndex('vmsByContainer')
/* parallel charts */
function populateChartsData () {
foreach(xoApi.getView('pools').all, function (pool, pool_id) {
foreach(hostsByPool[pool_id], function (host, host_id) {
console.log(host_id)
foreach(vmsByContainer[host_id], function (vm, vm_id) {
let nbvdi, vdisize
nbvdi = 0
vdisize = 0
foreach(vm.$VBDs, function (vbd_id) {
let vbd
vbd = xoApi.get(vbd_id)
if (!vbd.is_cd_drive && vbd.attached) {
nbvdi++
vdisize += xoApi.get(vbd.VDI).size
}
})
data.push({
name: vm.name_label,
id: vm_id,
vcpus: vm.CPUs.number,
vifs: vm.VIFs.length,
ram: vm.memory.size / (1024 * 1024 * 1024)/* memory size in GB */,
nbvdi: nbvdi,
vdisize: vdisize / (1024 * 1024 * 1024)/* disk size in Gb */
})
})
})
})
$scope.charts = {
data: data,
labels: {
vcpus: 'vCPUs number',
ram: 'RAM quantity',
vifs: 'VIF number',
nbvdi: 'VDI number',
vdisize: 'Total space'
}
}
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
xoApi.onUpdate(function () {
debouncedPopulate()
})
})
.controller('DatavizStorageHierarchical', function DatavizStorageHierarchical (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
$scope.charts = {
selected: {},
data: {
name: 'storage',
children: []
},
click: function (d) {
if (d.virtual) {
return
}
switch (d.type) {
case 'pool':
$state.go('pools_view', {
id: d.id
})
break
case 'host':
$state.go('hosts_view', {
id: d.id
})
break
case 'srs':
$state.go('SRs_view', {
id: d.id
})
break
}
}
}
function populateChartsData () {
function populatestorage (root, container_id) {
let srs = filter(xoApi.getIndex('srsByContainer')[container_id], (one_srs) => one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev')
foreach(srs, function (one_srs) {
let srs_used_size = 0
const srs_storage = {
name: one_srs.name_label,
id: one_srs.id,
children: [],
size: one_srs.size,
textSize: bytesToSizeFilter(one_srs.size),
type: 'srs'
}
root.size += one_srs.size
foreach(one_srs.VDIs, function (vdi_id) {
let vdi = xoApi.get(vdi_id)
if (vdi && vdi.name_label.indexOf('.iso') === -1) {
let vdi_storage = {
name: vdi.name_label,
id: vdi_id,
size: vdi.size,
textSize: bytesToSizeFilter(vdi.size),
type: 'vdi'
}
srs_used_size += vdi.size
srs_storage.children.push(vdi_storage)
}
})
if (one_srs.size > srs_used_size) {// some unallocated space
srs_storage.children.push({
color: 'white',
name: 'Free',
id: 'free' + one_srs.id,
size: one_srs.size - srs_used_size,
textSize: bytesToSizeFilter(one_srs.size - srs_used_size),
type: 'vdi',
virtual: true
})
}
root.children.push(srs_storage)
})
root.textSize = bytesToSizeFilter(root.size)
}
let storage_children,
pools,
hostsByPool,
pool_shared_storage
storage_children = []
pools = xoApi.getView('pools')
hostsByPool = xoApi.getIndex('hostsByPool')
foreach(pools.all, function (pool, pool_id) {
let pool_storage, hosts
pool_storage = {
name: pool.name_label || 'no pool',
id: pool_id,
children: [],
size: 0,
color: pool.name_label ? null : 'white',
type: 'pool',
virtual: !pool.name_label
}
pool_shared_storage = {
name: 'Shared',
id: 'Shared' + pool_id,
children: [],
size: 0,
type: 'host',
virtual: true
}
populatestorage(pool_shared_storage, pool_id)
pool_storage.children.push(pool_shared_storage)
pool_storage.size += pool_shared_storage.size
// by hosts
hosts = hostsByPool[pool_id]
foreach(hosts, function (host, host_id) {
// there's also SR attached top
let host_storage = {
name: host.name_label,
id: host.id,
children: [],
size: 0,
type: 'host'
}
populatestorage(host_storage, host_id)
pool_storage.size += host_storage.size
pool_storage.children.push(host_storage)
})
pool_storage.textSize = bytesToSizeFilter(pool_storage.size)
storage_children.push(pool_storage)
})
$scope.charts.data.children = storage_children
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
xoApi.onUpdate(function () {
debouncedPopulate()
})
})
.controller('DatavizRamHierarchical', function DatavizRamHierarchical (xoApi, $scope, $timeout, $state, bytesToSizeFilter) {
$scope.charts = {
selected: {},
data: {
name: 'ram',
children: []
},
click: function (d) {
if (d.virtual) {
return
}
switch (d.type) {
case 'pool':
$state.go('pools_view', {id: d.id})
break
case 'host':
$state.go('hosts_view', {id: d.id})
break
case 'vm':
$state.go('VMs_view', {id: d.id})
break
}
}
}
function populateChartsData () {
let ram_children,
pools,
vmsByContainer,
hostsByPool
ram_children = []
pools = xoApi.getView('pools')
vmsByContainer = xoApi.getIndex('vmsByContainer')
hostsByPool = xoApi.getIndex('hostsByPool')
foreach(pools.all, function (pool, pool_id) {
let pool_ram, hosts
// by hosts
pool_ram = {
name: pool.name_label || 'no pool',
id: pool_id,
children: [],
size: 0,
color: pool.name_label ? null : 'white',
type: 'pool',
virtual: !pool.name_label
}
hosts = hostsByPool[pool_id]
foreach(hosts, function (host, host_id) {
// there's also SR attached top
let vm_ram_size = 0
let host_ram = {
name: host.name_label,
id: host_id,
children: [],
size: host.memory.size,
type: 'host'
}
let VMs = vmsByContainer[host_id]
foreach(VMs, function (VM, vm_id) {
let vm_ram = {
name: VM.name_label,
id: vm_id,
size: VM.memory.size,
textSize: bytesToSizeFilter(VM.memory.size),
type: 'vm'
}
if (vm_ram.size) {
vm_ram_size += vm_ram.size
host_ram.children.push(vm_ram)
}
})
if (host_ram.size !== vm_ram_size) {
host_ram.children.push({
color: 'white',
name: 'Free',
id: 'free' + host.id,
size: host.memory.size - vm_ram_size,
textSize: bytesToSizeFilter(host.memory.size - vm_ram_size),
type: 'vm',
virtual: true
})
}
host_ram.textSize = bytesToSizeFilter(host_ram.size)
pool_ram.size += host_ram.size
pool_ram.children.push(host_ram)
})
if (pool_ram.children.length) {
pool_ram.textSize = bytesToSizeFilter(pool_ram.size)
ram_children.push(pool_ram)
}
})
$scope.charts.data.children = ram_children
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
xoApi.onUpdate(function () {
debouncedPopulate()
})
})
// A module exports its name.
.name

View File

@ -1,85 +0,0 @@
.grid-sm(ng-if="!selectedChart")
.panel.panel-default
p.page-title
i.fa.fa-pie-chart
| Dataviz
.panel-body.text-center
.chart-selector(
ng-repeat="(id,chart) in availablecharts"
ui-sref="dashboard.dataviz({chart:id})")
div {{chart.name }}
img.img-thumbnail(
ng-repeat="img in chart.imgs"
ng-src="{{img}}"
)
.grid-sm(ng-if="selectedChart =='sunburst'")
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory usage
.panel-body.text-center(
ng-controller="DatavizRamHierarchical as ram"
style="position:relative"
)
sunburst-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
.panel-body.text-center(
ng-controller="DatavizStorageHierarchical as storage"
style="position:relative"
)
sunburst-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-sm(ng-if="selectedChart =='circle'")
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory usage
.panel-body.text-center(
ng-controller="DatavizRamHierarchical as ram"
style="position:relative"
)
circle-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
.panel-body.text-center(
ng-controller="DatavizStorageHierarchical as storage"
style="position:relative"
)
circle-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-sm(ng-if="selectedChart == 'parcoords'")
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| VMs properties
.panel-body.text-center(
ng-controller="DatavizParcoords as parcoords"
)
parallel-chart(
click="charts.click(d)"
chart-labels="charts.labels"
chart-data="charts.data"
)

View File

@ -1,363 +0,0 @@
import angular from 'angular'
import Bluebird from 'bluebird'
import uiRouter from 'angular-ui-router'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import sortBy from 'lodash.sortby'
import xoApi from 'xo-api'
import xoHorizon from'xo-horizon'
import xoServices from 'xo-services'
import xoWeekHeatmap from'xo-week-heatmap'
import view from './view'
export default angular.module('dashboard.health', [
uiRouter,
xoApi,
xoHorizon,
xoServices,
xoWeekHeatmap
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.health', {
controller: 'Health as bigController',
data: {
requireAdmin: true
},
url: '/health',
template: view
})
})
.filter('type', () => {
return function (objects, type) {
if (!type) {
return objects
}
return filter(objects, object => object.type === type)
}
})
.controller('Health', function () {})
.controller('HealthHeatmap', function (xoApi, xo, xoAggregate, notify, bytesToSizeFilter) {
this.charts = {
heatmap: null
}
this.objects = xoApi.all
this.prepareTypeFilter = function (selection) {
const object = selection[0]
this.typeFilter = object && object.type || undefined
}
this.selectAll = function (type) {
this.selected = filter(this.objects, object =>
(object.type === type && object.power_state === 'Running'))
this.typeFilter = type
}
this.prepareMetrics = function (objects) {
this.chosen = objects && objects.slice()
this.metrics = undefined
this.selectedMetric = undefined
if (this.chosen && this.chosen.length) {
this.loadingMetrics = true
const statPromises = []
forEach(this.chosen, object => {
const apiType = (object.type === 'host' && 'host') || (object.type === 'VM' && 'vm') || undefined
if (!apiType) {
notify.error({
title: 'Unhandled object ' + (objects.name_label || ''),
message: 'There is no stats available for this type of objects'
})
object._ignored = true
} else {
delete object._ignored
statPromises.push(
xo[apiType].refreshStats(object.id, 'hours') // hours granularity (7 * 24 hours)
.then(result => {
if (result.stats === undefined) {
object._ignored = true
throw new Error('No stats')
}
return {object, result}
})
.catch(error => {
error.object = object
object._ignored = true
throw error
})
)
}
})
Bluebird.settle(statPromises)
.then(stats => {
const averageMetrics = {}
let averageObjectLayers = {}
let averageCPULayers = 0
forEach(stats, statePromiseInspection => { // One object...
if (statePromiseInspection.isRejected()) {
notify.warning({
title: 'Error fetching stats',
message: 'Metrics do not include ' + statePromiseInspection.reason().object.name_label
})
} else if (statePromiseInspection.isFulfilled()) {
const {object, result} = statePromiseInspection.value()
// Make date array
result.stats.date = []
let timestamp = result.endTimestamp
for (let i = result.stats.memory.length - 1; i >= 0; i--) {
result.stats.date.unshift(timestamp)
timestamp -= 3600
}
const averageCPU = averageMetrics['All CPUs'] && averageMetrics['All CPUs'].values || []
forEach(result.stats.cpus, (values, metricKey) => { // Every CPU metric of this object
metricKey = 'CPU ' + metricKey
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
averageCPULayers++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
if (averageCPU[key] === undefined) { // first overall value
averageCPU.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous overall value
averageCPU[key].value = (averageCPU[key].value * (averageCPULayers - 1) + value) / averageCPULayers
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues
}
})
averageMetrics['All CPUs'] = {
key: 'All CPUs',
values: averageCPU
}
forEach(result.stats.vifs, (vif, vifType) => {
const rw = (vifType === 'rx') ? 'out' : 'in'
forEach(vif, (values, metricKey) => {
metricKey = 'Network ' + metricKey + ' ' + rw
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
})
})
forEach(result.stats.pifs, (pif, pifType) => {
const rw = (pifType === 'rx') ? 'out' : 'in'
forEach(pif, (values, metricKey) => {
metricKey = 'NIC ' + metricKey + ' ' + rw
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
})
})
forEach(result.stats.xvds, (xvd, xvdType) => {
const rw = (xvdType === 'r') ? 'read' : 'write'
forEach(xvd, (values, metricKey) => {
metricKey = 'Disk ' + metricKey + ' ' + rw
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
})
})
if (result.stats.load) {
const metricKey = 'Load average'
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(result.stats.load, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues
}
}
if (result.stats.memoryUsed) {
const metricKey = 'RAM Used'
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(result.stats.memoryUsed, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value * (object.type === 'host' ? 1024 : 1),
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
}
}
})
this.metrics = sortBy(averageMetrics, (_, key) => key)
this.loadingMetrics = false
})
}
}
})
.controller('HealthHorizons', function ($scope, xoApi, xoAggregate, xo, $timeout) {
let ctrl, stats
ctrl = this
ctrl.synchronizescale = true
ctrl.objects = xoApi.all
ctrl.chosen = []
this.prepareTypeFilter = function (selection) {
const object = selection[0]
ctrl.typeFilter = object && object.type || undefined
}
this.selectAll = function (type) {
ctrl.selected = filter(ctrl.objects, object =>
(object.type === type && object.power_state === 'Running'))
ctrl.typeFilter = type
}
this.prepareMetrics = function (objects) {
ctrl.chosen = objects
ctrl.selectedMetric = null
ctrl.loadingMetrics = true
xoAggregate
.refreshStats(ctrl.chosen, 'hours')
.then(function (result) {
stats = result
ctrl.metrics = stats.keys
ctrl.stats = {}
// $timeout(refreshStats, 1000)
ctrl.loadingMetrics = false
})
.catch(function (e) {
console.log(' ERROR ', e)
})
}
this.toggleSynchronizeScale = function () {
ctrl.synchronizescale = !ctrl.synchronizescale
if (ctrl.selectedMetric) {
ctrl.prepareStat()
}
}
this.prepareStat = function () {
let min, max
max = 0
min = 0
ctrl.stats = {}
// compute a global extent => the chart will have the same scale
if (ctrl.synchronizescale) {
forEach(stats.details, function (stat, object_id) {
forEach(stat[ctrl.selectedMetric], function (val) {
if (!isNaN(val.value)) {
max = Math.max(val.value || 0, max)
}
})
})
ctrl.extents = [min, max]
} else {
ctrl.extents = null
}
forEach(stats.details, function (stat, object_id) {
const label = find(ctrl.chosen, {id: object_id})
ctrl.stats[label.name_label] = stat[ctrl.selectedMetric]
})
}
})
.name

View File

@ -1,155 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-heartbeat
| Health
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-fire
| Weekly Heatmap
.panel-body(ng-controller='HealthHeatmap as heatmap')
| {{heatmap.toto}}
form
.grid-sm
.grid-cell.grid--gutters
.container-fluid
.form-group
ui-select.form-control(ng-model = 'heatmap.selected', ng-change = 'heatmap.prepareTypeFilter(heatmap.selected)', multiple, close-on-select = 'false')
ui-select-match(placeholder = 'Choose an object')
i(class = 'xo-icon-{{ $item.type | lowercase }}')
| {{ $item.name_label }}
ui-select-choices(repeat = 'object in heatmap.objects | underStat | type:heatmap.typeFilter | filter:{ power_state: "Running" } | filter:$select.search | orderBy:["type", "name_label"] track by object.id')
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 }})
//- br
.btn-group(role = 'group')
button.btn.btn-default(ng-click = 'heatmap.selected = []', tooltip = 'Clear selection')
i.fa.fa-times
button.btn.btn-default(ng-click = 'heatmap.selectAll("VM")', tooltip = 'Choose all VMs')
i.xo-icon-vm
button.btn.btn-default(ng-click = 'heatmap.selectAll("host")', tooltip = 'Choose all hosts')
i.xo-icon-host
button.btn.btn-success(ng-click = 'heatmap.prepareMetrics(heatmap.selected)', tooltip = 'Load metrics')
i.fa.fa-check
| &nbsp;Select
.grid-cell.grid--gutters
.container-fluid
span(ng-if = 'heatmap.loadingMetrics')
| Loading metrics ...&nbsp;
i.fa.fa-circle-o-notch.fa-spin
.form-group(ng-if = 'heatmap.metrics')
ui-select(ng-model = 'heatmap.selectedMetric')
ui-select-match(placeholder = 'Choose a metric') {{ $select.selected.key }}
ui-select-choices(repeat = 'metric in heatmap.metrics | filter:$select.search | orderBy:["key"]') {{ metric.key }}
br
p.text-center(ng-if = 'heatmap.chosen.length')
span(ng-repeat = 'object in heatmap.chosen', ng-class = '{"text-danger": object._ignored}')
i(class = 'xo-icon-{{ object.type | lowercase }}')
| &nbsp;
span(ng-if = '!object._ignored') {{ object.name_label }}
del(ng-if = 'object._ignored') {{ object.name_label }}
| &nbsp;&ensp;
weekheatmap(ng-if = 'heatmap.selectedMetric', chart-data='heatmap.selectedMetric')
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-fire
| Weekly Charts
.panel-body(ng-controller="HealthHorizons as horizons")
form
.grid-sm
.grid-cell.grid--gutters
.container-fluid
.form-group
ui-select.form-control(
ng-model = 'horizons.selected',
ng-change = 'horizons.prepareTypeFilter(horizons.selected)',
multiple,
close-on-select = 'false'
)
ui-select-match(placeholder = 'Choose an object')
i(class = 'xo-icon-{{ $item.type | lowercase }}')
| {{ $item.name_label }}
ui-select-choices(repeat = 'object in horizons.objects | underStat | type:horizons.typeFilter | filter:{ power_state: "Running" } | filter:$select.search | orderBy:["type", "name_label"] track by object.id')
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 }})
//- br
.btn-group(role = 'group')
button.btn.btn-default(ng-click = 'horizons.selected = []', tooltip = 'Clear selection')
i.fa.fa-times
button.btn.btn-default(ng-click = 'horizons.selectAll("VM")', tooltip = 'Choose all VMs')
i.xo-icon-vm
button.btn.btn-default(ng-click = 'horizons.selectAll("host")', tooltip = 'Choose all hosts')
i.xo-icon-host
button.btn.btn-success(ng-click = 'horizons.prepareMetrics(horizons.selected)', tooltip = 'Load metrics')
i.fa.fa-check
| &nbsp;Select
.grid-cell.grid--gutters
.container-fluid
span(ng-if = 'horizons.loadingMetrics')
| Loading metrics ...&nbsp;
i.fa.fa-circle-o-notch.fa-spin
.form-group(ng-if = 'horizons.metrics && !horizons.loadingMetrics')
ui-select(ng-model = 'horizons.selectedMetric',ng-change='horizons.prepareStat()')
ui-select-match(placeholder = 'Choose a metric') {{ $select.selected }}
ui-select-choices(repeat = 'metric in horizons.metrics | filter:$select.search | orderBy:["key"]') {{ metric }}
br
button.btn.btn-primary.pull-right(
tooltip="Desynchronize Scale",
ng-click="horizons.toggleSynchronizeScale()"
ng-if='horizons.synchronizescale && horizons.selectedMetric'
)
i.fa.fa-balance-scale
button.btn.btn-default.pull-right(
tooltip="Synchronize Scale",
ng-click="horizons.toggleSynchronizeScale()"
ng-if='!horizons.synchronizescale && horizons.selectedMetric'
)
i.fa.fa-balance-scale
br
p.text-center(ng-if = 'horizons.chosen.length')
span(ng-repeat = 'object in horizons.chosen', ng-class = '{"text-danger": object._ignored}')
i(class = 'xo-icon-{{ object.type | lowercase }}')
| &nbsp;
span(ng-if = '!object._ignored') {{ object.name_label }}
del(ng-if = 'object._ignored') {{ object.name_label }}
| &nbsp;&ensp;
div(
ng-repeat='(label,stat) in horizons.stats'
ng-if='!horizons.loadingMetrics'
style='position:relative'
)
horizon(
ng-if='$first'
chart-data='stat'
show-axis='true'
axis-orientation='top'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)
horizon(
ng-if='$middle'
chart-data='stat'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)
horizon(
ng-if='$last && !$first'
chart-data='stat'
show-axis='true'
axis-orientation='bottom'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)

View File

@ -1,41 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import dataviz from './dataviz'
import filter from 'lodash.filter'
import health from './health'
import overview from './overview'
import view from './view'
export default angular.module('dashboard', [
uiRouter,
dataviz,
health,
overview
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/dashboard'
})
// Redirect to default sub-state.
$stateProvider.state('dashboard.index', {
url: '',
controller: function ($state) {
$state.go('dashboard.overview')
}
})
})
.filter('underStat', () => {
let isUnderStat = object => object.type === 'host' || object.type === 'VM'
return objects => filter(objects, isUnderStat)
})
.name

View File

@ -1,137 +0,0 @@
'use strict'
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import clone from 'lodash.clonedeep'
import debounce from 'lodash.debounce'
import foreach from 'lodash.foreach'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('dashboard.overview', [
uiRouter,
uiSelect,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.overview', {
controller: 'Overview as ctrl',
data: {
requireAdmin: true
},
url: '/overview',
template: view
})
})
.controller('Overview', function ($scope, $window, xoApi, xo, $timeout, bytesToSizeFilter) {
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
angular.extend($scope, {
pools: {
nb: 0
},
hosts: {
nb: 0
},
vms: {
nb: 0,
running: 0,
halted: 0,
action: 0
},
ram: [0, 0],
cpu: [0, 0],
srs: []
})
function populateChartsData () {
let pools,
vmsByContainer,
hostsByPool,
nb_hosts,
nb_pools,
vms,
srsByContainer,
srs
nb_pools = 0
nb_hosts = 0
vms = {
nb: 0,
states: [0, 0, 0, 0]
}
const runningStateToIndex = {
Running: 0,
Halted: 1,
Suspended: 2,
Action: 3
}
nb_pools = 0
srs = []
// update vdi, set them to the right host
pools = xoApi.getView('pools')
srsByContainer = xoApi.getIndex('srsByContainer')
vmsByContainer = xoApi.getIndex('vmsByContainer')
hostsByPool = xoApi.getIndex('hostsByPool')
foreach(pools.all, function (pool, pool_id) {
nb_pools++
let pool_srs = srsByContainer[pool_id]
foreach(pool_srs, (one_srs) => {
if (one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev') {
one_srs = clone(one_srs)
one_srs.ratio = one_srs.size ? one_srs.physical_usage / one_srs.size : 0
one_srs.pool_label = pool.name_label
srs.push(one_srs)
}
})
let VMs = vmsByContainer[pool_id]
foreach(VMs, function (VM) {
// non running VM
vms.states[runningStateToIndex[VM['power_state']]]++
vms.nb++
})
let hosts = hostsByPool[pool_id]
foreach(hosts, function (host, host_id) {
let hosts_srs = srsByContainer[host_id]
foreach(hosts_srs, (one_srs) => {
if (one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev') {
one_srs = clone(one_srs)
one_srs.ratio = one_srs.size ? one_srs.physical_usage / one_srs.size : 0
one_srs.host_label = host.name_label
one_srs.pool_label = pool.name_label
srs.push(one_srs)
}
})
nb_hosts++
let VMs = vmsByContainer[host_id]
foreach(VMs, function (VM) {
vms.states[runningStateToIndex[VM['power_state']]]++
vms.nb++
})
})
})
$scope.hosts.nb = nb_hosts
$scope.vms = vms
$scope.pools.nb = nb_pools
$scope.srs = srs
$scope.ram = [xoApi.stats.$memory.usage, xoApi.stats.$memory.size - xoApi.stats.$memory.usage]
$scope.cpu = [[xoApi.stats.$vCPUs], [xoApi.stats.$CPUs]]
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
xoApi.onUpdate(function () {
debouncedPopulate()
})
})
.name

View File

@ -1,103 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-dashboard
| Dashboard
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cloud
| Pools
.panel-body.text-center
p.big-stat {{pools.nb}}
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-server
| Hosts
.panel-body.text-center
p.big-stat {{hosts.nb}}
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-desktop
| VMs
.panel-body.text-center
p.big-stat {{vms.nb}}
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Global RAM usage
.panel-body.text-center
canvas(
id="doughnut"
class="chart chart-doughnut"
data="ram"
labels="['Used', 'Free']"
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}'
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-dashboard
| vCPUs/CPUs
.panel-body.text-center
canvas(
id="bar"
class="chart chart-bar"
data="cpu"
labels="['']"
series="['vCPUs','CPUs']"
options="{scaleShowGridLines: false, barDatasetSpacing : 10, showScale: false, responsive: false}"
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-question-circle
| VMs power state
.panel-body.text-center
canvas(
id="pie"
class="chart chart-pie"
data="vms.states"
labels="['Running', 'Halted', 'Suspended', 'Action']"
colours="['#5cb85c', '#d9534f', '#5bc0de', '#f0ad4e']"
options="{responsive: false}"
)
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-database
| Storage
.panel-body
table.table.table-hover
tr
th Name
th pool
th host
th Format
th Size
th Physical/Allocated usage
//- TODO: display PBD status for each SR of this host (connected or not)
//- Shared SR
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | orderBy:'-ratio' track by SR.id")
td.oneliner
| {{SR.name_label}}
td.oneliner
| {{SR.pool_label}}
td.oneliner
| {{SR.host_label}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.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}}")

View File

@ -1,16 +0,0 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.overview', ui-sref-active = 'active')
i.fa.fa-fw.fa-dashboard.fa-menu
span.menu-entry Overview
li
a(ui-sref = '.dataviz({chart:""})')
i.fa.fa-fw.fa-pie-chart.fa-menu
span.menu-entry Dataviz
li
a(ui-sref = '.health')
i.fa.fa-fw.fa-heartbeat.fa-menu
span.menu-entry Health
.side-content(ui-view = '')

View File

@ -1,63 +0,0 @@
// TODO: should be integrated xo.deleteVms()
import angular from 'angular'
import forEach from 'lodash.foreach'
import uiBootstrap from 'angular-ui-bootstrap'
import xoServices from 'xo-services'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.deleteVms', [
uiBootstrap,
xoServices
])
.controller('DeleteVmsCtrl', function (
$scope,
$modalInstance,
xoApi,
VMsIds
) {
$scope.$watchCollection(() => xoApi.get(VMsIds), function (VMs) {
$scope.VMs = VMs
})
// Do disks have to be deleted for a given VM.
let disks = $scope.disks = {}
forEach(VMsIds, id => {
disks[id] = true
})
$scope.delete = function () {
let value = []
forEach(VMsIds, id => {
value.push([id, disks[id]])
})
$modalInstance.close(value)
}
})
.service('deleteVmsModal', function ($modal, xo) {
return function (ids) {
return $modal.open({
controller: 'DeleteVmsCtrl',
template: view,
resolve: {
VMsIds: () => ids
}
}).result.then(function (toDelete) {
let promises = []
forEach(toDelete, ([id, deleteDisks]) => {
promises.push(xo.vm.delete(id, deleteDisks))
})
return promises
})
}
})
// A module exports its name.
.name

View File

@ -1,27 +0,0 @@
form(ng-submit="delete()")
.modal-header
h3 VMs deletion
.modal-body
p
| You are going to delete the following VMs, this is a
strong dangerous action
| !
table.table
tr
th.col-sm-3 Name
th.col-sm-6 Description
th.col-sm-3 Delete disks?
tbody
tr(ng-repeat="VM in VMs | orderBy:natural('name_label') track by VM.id")
td {{VM.name_label}}
td {{VM.name_description}}
td
input(type="checkbox", ng-model="disks[VM.id]")
p
i.fa.fa-exclamation-triangle
| &nbsp;All snapshots will be deleted too
.modal-footer
button.btn.btn-primary(type="submit")
| Delete
button.btn.btn-warning(type="button", ng-click="$dismiss()")
| Cancel

View File

@ -1,39 +0,0 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
// ===================================================================
export default angular.module('xoWebApp.genericModal', [
uiBootstrap
])
.controller('GenericModalCtrl', function ($scope, $modalInstance, options) {
$scope.title = options.title
$scope.message = options.message
$scope.yesButtonLabel = options.yesButtonLabel || 'Ok'
$scope.noButtonLabel = options.noButtonLabel
})
.service('modal', function ($modal) {
return {
confirm: function (opts) {
const modal = $modal.open({
controller: 'GenericModalCtrl',
template: require('./view'),
resolve: {
options: function () {
return {
title: opts.title,
message: opts.message,
noButtonLabel: 'Cancel'
}
}
}
})
return modal.result
}
}
})
// A module exports its name.
.name

View File

@ -1,11 +0,0 @@
.modal-header
h3
i.fa.fa-exclamation-triangle.text-danger
| {{title}}
.modal-body
| {{message}}
.modal-footer
button.btn.btn-primary(type="button", ng-click="$close()")
| {{yesButtonLabel}}
button.btn.btn-warning(ng-if="noButtonLabel", type="button", ng-click="$dismiss()")
| {{noButtonLabel}}

View File

@ -1,414 +0,0 @@
angular = require 'angular'
forEach = require 'lodash.foreach'
intersection = require 'lodash.intersection'
map = require 'lodash.map'
omit = require 'lodash.omit'
sum = require 'lodash.sum'
throttle = require 'lodash.throttle'
find = require 'lodash.find'
#=====================================================================
module.exports = angular.module 'xoWebApp.host', [
require 'angular-file-upload'
require 'angular-ui-router'
require 'tag'
]
.config ($stateProvider) ->
$stateProvider.state 'hosts_view',
url: '/hosts/:id'
controller: 'HostCtrl'
template: require './view'
.controller 'HostCtrl', (
$scope, $stateParams, $http
$upload
$window
$timeout
dateFilter
xoApi, xo, modal, notify, bytesToSizeFilter
) ->
do (
hostId = $stateParams.id
controllers = xoApi.getIndex('vmControllersByContainer')
poolPatches = xoApi.getIndex('poolPatchesByPool')
srs = xoApi.getIndex('srsByContainer')
tasks = xoApi.getIndex('runningTasksByHost')
vms = xoApi.getIndex('vmsByContainer')
) ->
Object.defineProperties($scope, {
controller: {
get: () => controllers[hostId]
},
poolPatches: {
get: () => $scope.host && poolPatches[$scope.host.$poolId]
},
sharedSrs: {
get: () => $scope.host && srs[$scope.host.$poolId]
},
srs: {
get: () => srs[hostId]
},
tasks: {
get: () => tasks[hostId]
},
vms: {
get: () => vms[hostId]
}
})
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
host = null
$scope.currentPatchPage = 1
$scope.currentLogPage = 1
$scope.currentPCIPage = 1
$scope.currentGPUPage = 1
$scope.refreshStatControl = refreshStatControl = {
baseStatInterval: 5000
baseTimeOut: 10000
period: null
running: false
attempt: 0
start: () ->
return if this.running
this.stop()
this.running = true
this._reset()
$scope.$on('$destroy', () => this.stop())
return this._trig(Date.now())
_trig: (t1) ->
if this.running
timeoutSecurity = $timeout(
() => this.stop(),
this.baseTimeOut
)
return $scope.refreshStats($scope.host.id)
.then () => this._reset()
.catch (err) =>
if !this.running || this.attempt >= 2 || $scope.host.power_state isnt 'Running' || $scope.isVMWorking($scope.host)
return this.stop()
else
this.attempt++
.finally () =>
$timeout.cancel(timeoutSecurity)
if this.running
t2 = Date.now()
return this.period = $timeout(
() => this._trig(t2),
Math.max(this.baseStatInterval - (t2 - t1), 0)
)
_reset: () ->
this.attempt = 0
stop: () ->
if this.period
$timeout.cancel(this.period)
this.running = false
return
}
$scope.$watch(
-> xoApi.get $stateParams.id
(host) ->
$scope.host = host
return unless host?
pool = $scope.pool = xoApi.get host.$poolId
SRsToPBDs = $scope.SRsToPBDs = Object.create null
for PBD in host.$PBDs
PBD = xoApi.get PBD
# If this PBD is unknown, just skips it.
continue unless PBD
SRsToPBDs[PBD.SR] = PBD
$scope.listMissingPatches($scope.host.id)
if host.power_state is 'Running'
refreshStatControl.start()
else
refreshStatControl.stop()
)
$scope.$watch('vms', (vms) =>
$scope.vCPUs = sum(vms, (vm) => vm.CPUs.number)
)
$scope.cancelTask = (id) ->
modal.confirm({
title: 'Cancel task'
message: 'Are you sure you want to cancel this task?'
}).then ->
xo.task.cancel id
$scope.destroyTask = (id) ->
modal.confirm({
title: 'Destroy task'
message: 'Are you sure you want to destroy this task?'
}).then ->
xo.task.destroy id
$scope.disconnectPBD = xo.pbd.disconnect
$scope.removePBD = xo.pbd.delete
$scope.new_sr = xo.pool.new_sr
$scope.pool_addHost = (id) ->
xo.host.attach id
$scope.pools = xoApi.getView('pools')
$scope.hostsByPool = xoApi.getIndex('hostsByPool')
$scope.pool_moveHost = (target) ->
modal.confirm({
title: 'Move host to another pool'
message: 'Are you sure you want to move this host?'
}).then ->
xo.pool.mergeInto({ source: $scope.pool.id, target: target.id })
$scope.pool_removeHost = (id) ->
modal.confirm({
title: 'Remove host from pool'
message: 'Are you sure you want to detach this host from its pool? It will be automatically rebooted AND LOCAL STORAGE WILL BE ERASED.'
}).then ->
xo.host.detach id
$scope.rebootHost = (id) ->
modal.confirm({
title: 'Reboot host'
message: 'Are you sure you want to reboot this host? It will be disabled then rebooted'
}).then ->
xo.host.restart id
$scope.enableHost = (id) ->
xo.host.enable id
notify.info {
title: 'Host action'
message: 'Host is enabled'
}
$scope.disableHost = (id) ->
modal.confirm({
title: 'Disable host'
message: 'Are you sure you want to disable this host? In disabled state, no new VMs can be started and currently active VMs on the host continue to execute.'
}).then ->
xo.host.disable id
.then ->
notify.info {
title: 'Host action'
message: 'Host is disabled'
}
$scope.restartToolStack = (id) ->
modal.confirm({
title: 'Restart XAPI'
message: 'Are you sure you want to restart the XAPI toolstack?'
}).then ->
xo.host.restartToolStack id
$scope.shutdownHost = (id) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to shutdown this host?'
}).then ->
xo.host.stop id
$scope.saveHost = ($data) ->
{host} = $scope
{name_label, name_description, enabled} = $data
$data = {
id: host.id
}
if name_label isnt host.name_label
$data.name_label = name_label
if name_description isnt host.name_description
$data.name_description = name_description
if enabled isnt host.enabled
if host.enabled
$scope.disableHost($data.id)
else
$scope.enableHost($data.id)
# enabled is not set via the "set" method, so we remove it before send it
delete $data.enabled
xoApi.call 'host.set', $data
$scope.deleteAllLog = ->
modal.confirm({
title: 'Log deletion'
message: 'Are you sure you want to delete all the logs?'
}).then ->
forEach $scope.host.messages, (log) ->
console.log "Remove log #{log.id}"
xo.log.delete log.id
return
$scope.deleteLog = (id) ->
console.log "Remove log #{id}"
xo.log.delete id
$scope.connectPBD = (id) ->
console.log "Connect PBD #{id}"
xoApi.call 'pbd.connect', {id: id}
$scope.disconnectPBD = (id) ->
console.log "Disconnect PBD #{id}"
xoApi.call 'pbd.disconnect', {id: id}
$scope.removePBD = (id) ->
console.log "Remove PBD #{id}"
xoApi.call 'pbd.delete', {id: id}
$scope.connectPIF = (id) ->
console.log "Connect PIF #{id}"
xoApi.call 'pif.connect', {id: id}
$scope.disconnectPIF = (id) ->
console.log "Disconnect PIF #{id}"
xoApi.call 'pif.disconnect', {id: id}
$scope.removePIF = (id) ->
console.log "Remove PIF #{id}"
xoApi.call 'pif.delete', {id: id}
$scope.importVm = ($files, id) ->
file = $files[0]
notify.info {
title: 'VM import started'
message: "Starting the VM import"
}
xo.vm.import id
.then ({ $sendTo: url }) ->
return $upload.http {
method: 'POST'
url
data: file
}
.then (result) ->
throw result.status if result.status isnt 200
notify.info
title: 'VM import'
message: 'Success'
$scope.createNetwork = (name, description, pif, mtu, vlan) ->
$scope.createNetworkWaiting = true # disables form fields
notify.info {
title: 'Network creation...'
message: 'Creating the network'
}
params = {
host: $scope.host.id
name,
}
if mtu then params.mtu = mtu
if pif then params.pif = pif
if vlan then params.vlan = vlan
if description then params.description = description
xoApi.call 'host.createNetwork', params
.then ->
$scope.creatingNetwork = false
$scope.createNetworkWaiting = false
$scope.isPoolPatch = (patch) ->
return false if $scope.poolPatches is undefined
return $scope.poolPatches.hasOwnProperty(patch.uuid)
$scope.isPoolPatchApplied = (patch) ->
return true if patch.applied
hostPatch = intersection(patch.$host_patches, $scope.host.patches)
return false if not hostPatch.length
hostPatch = xoApi.get(hostPatch[0])
return hostPatch.applied
$scope.listMissingPatches = (id) ->
return xo.host.listMissingPatches id
.then (result) ->
$scope.updates = omit(result,map($scope.poolPatches,'id'))
$scope.installPatch = (id, patchUid) ->
console.log("Install patch "+patchUid+" on "+id)
notify.info {
title: 'Patch host'
message: "Patching the host, please wait..."
}
xo.host.installPatch id, patchUid
$scope.installAllPatches = (id) ->
modal.confirm({
title: 'Install all the missing patches'
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 patches on host ' + id)
xo.host.installAllPatches id
$scope.refreshStats = (id) ->
return xo.host.refreshStats id
.then (result) ->
result.stats.cpuSeries = []
if result.stats.cpus.length >= 12
nValues = result.stats.cpus[0].length
nCpus = result.stats.cpus.length
cpuAVG = (0 for [1..nValues])
forEach result.stats.cpus, (cpu) ->
forEach cpu, (stat, index) ->
cpuAVG[index] += stat
return
return
forEach cpuAVG, (cpu, index) ->
cpuAVG[index] /= nCpus
return
result.stats.cpus = [cpuAVG]
result.stats.cpuSeries.push 'CPU AVG'
else
forEach result.stats.cpus, (v,k) ->
result.stats.cpuSeries.push 'CPU ' + k
return
result.stats.pifSeries = []
pifsArray = []
forEach result.stats.pifs.rx, (v,k) ->
return unless v
result.stats.pifSeries.push '#' + k + ' in'
result.stats.pifSeries.push '#' + k + ' out'
pifsArray.push (v || [])
pifsArray.push (result.stats.pifs.tx[k] || [])
return
result.stats.pifs = pifsArray
forEach result.stats.memoryUsed, (v, k) ->
result.stats.memoryUsed[k] = v*1024
forEach result.stats.memory, (v, k) ->
result.stats.memory[k] = v*1024
result.stats.date = []
timestamp = result.endTimestamp
for i in [result.stats.memory.length-1..0] by -1
result.stats.date.unshift new Date(timestamp*1000).toLocaleTimeString()
timestamp -= 5
$scope.stats = result.stats
$scope.statView = {
cpuOnly: false,
ramOnly: false,
netOnly: false,
loadOnly: false
}
# A module exports its name.
.name

View File

@ -1,539 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
| {{host.name_label}}
small(ng-if="pool.name_label")
| (
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
| )
p.center {{host.bios_strings["system-manufacturer"]}} {{host.bios_strings["system-product-name"]}}
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()")
i.fa.fa-edit.fa-fw
.panel-body
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="host.name_label", e-name="name_label", e-form="hostSettings")
| {{host.name_label}}
dt Description
dd
span(editable-text="host.name_description", e-name="name_description", e-form="hostSettings")
| {{host.name_description}}
dt Enabled
dd
span(editable-select="host.enabled", e-ng-options="ap.v as ap.t for ap in [{v: true, t:'Yes'}, {v: false, t:'No'}]", e-name="enabled", e-form="hostSettings")
| {{host.enabled ? 'Yes' : 'No'}}
dt Tags
dd
xo-tag(ng-if = 'host', object = 'host')
dt CPUs
dd {{host.CPUs["cpu_count"]}}x {{host.CPUs["modelname"]}}
dt Hostname
dd
| {{host.hostname}}
dt UUID
dd {{host.UUID}}
dt iQN
dd {{host.iSCSI_name}}
dt(ng-if="refreshStatControl.running && stats") vCPUs/CPUs:
dd(ng-if="refreshStatControl.running && stats") {{vCPUs}}/{{host.CPUs['cpu_count']}}
dt(ng-if="refreshStatControl.running && stats") Running VMs:
dd(ng-if="refreshStatControl.running && stats") {{vms | count}}
dt(ng-if="refreshStatControl.running && stats") RAM (used/free):
dd(ng-if="refreshStatControl.running && stats") {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}}
.btn-form(ng-show="hostSettings.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="hostSettings.$waiting", ng-click="hostSettings.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="hostSettings.$waiting")
i.fa.fa-save
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats
| Stats
.panel-body(ng-if="refreshStatControl.running && stats")
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
p.stat-name
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-full(
id="bigCpu"
data="stats.cpus"
labels="stats.date"
series="stats.cpuSeries"
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= Math.round(10*value)/10 %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= Math.round(10*value)/10 %>", pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.ramOnly", ng-click="statView.ramOnly = false")
p.stat-name
i.xo-icon-memory
//- i.fa.fa-bar-chart
//- i.fa.fa-tasks
//- i.fa.fa-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-full(
id="bigRam"
data="[stats.memoryUsed,stats.memory]"
labels="stats.date"
series="['Used RAM', 'Total RAM']"
colours="['#ff0000', '#ffbbbb']"
legend="true"
options=' {responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.netOnly", ng-click="statView.netOnly = false")
p.stat-name
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-full(
id="bigNet"
data="stats.pifs"
labels="stats.date"
series="stats.pifSeries"
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
legend="true"
options=' {responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.loadOnly", ng-click="statView.loadOnly = false")
p.stat-name
i.fa.fa-cogs
| &nbsp; Load Average
canvas.chart.chart-line.chart-stat-full(
id="bigLoad"
data="[stats.load]"
labels="stats.date"
series="['Load']"
colours="['#960094']"
legend="true"
options=' {responsive: true, maintainAspectRatio: false, multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="!statView.netOnly && !statView.loadOnly && !statView.cpuOnly && !statView.ramOnly")
.row
.col-md-6(ng-click="statView.cpuOnly=true")
p.stat-name
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-preview(
id="smallCpu"
data="stats.cpus"
labels="stats.date"
series="stats.cpuSeries"
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
options='{responsive: true, maintainAspectRatio: false, showTooltips: false, pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1}'
)
.col-md-6(ng-click="statView.ramOnly=true")
p.stat-name
i.xo-icon-memory
//- i.fa.fa-bar-chart
//- i.fa.fa-tasks
//- i.fa.fa-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-preview(
id="smallRam"
data="[stats.memoryUsed,stats.memory]"
labels="stats.date"
series="['Used RAM', 'Total RAM']"
colours="['#ff0000', '#ffbbbb']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.row
.col-md-6(ng-click="statView.netOnly=true")
p.stat-name
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-preview(
id="smallNet"
data="stats.pifs"
labels="stats.date"
series="stats.pifSeries"
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.col-md-6(ng-click="statView.loadOnly=true")
p.stat-name
i.fa.fa-cogs
| &nbsp; Load Average
canvas.chart.chart-line.chart-stat-preview(
id="smallDisk"
data="[stats.load]"
labels="stats.date"
series="['Load']"
colours="['#960094']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.panel-body(ng-if="!refreshStatControl.running || !stats")
.row
.col-sm-4
p.stat-name CPU usage:
p.center.mid-stat {{vCPUs}}/{{host.CPUs['cpu_count']}}
.col-sm-4
p.stat-name RAM used:
p.center.mid-stat {{host.memory.usage | bytesToSize}}
.col-sm-4
p.stat-name Running VMs:
p.center.mid-stat {{vms | count}}
p.center(ng-if="refreshStatControl.running")
i.xo-icon-loading
| &nbsp; Fetching stats...
//- Action panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body.text-center
.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})")
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})")
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)")
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)")
i.fa.fa-power-off.fa-2x.fa-fw
.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)")
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)")
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)")
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)")
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(
dropdown-toggle
tooltip="Move host to another pool"
tooltip-placement="top"
type="button"
style="width: 90%"
)
i.fa.fa-cloud-download.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu")
li(ng-repeat="p in pools.all | map | orderBy:natural('name_label') track by p.id" ng-if="p!=pool")
a(xo-click="pool_moveHost(p)")
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)")
i.fa.fa-cloud-download.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
tooltip="Import VM"
tooltip-placement="top"
type="button"
style="width: 90%"
ng-file-select = 'importVm($files, host.id)'
)
i.fa.fa-upload.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
tooltip="Host console"
tooltip-placement="top"
type="button"
style="width: 90%"
xo-sref="consoles_view({id: controller.id})"
)
i.xo-icon-console.fa-2x.fa-fw
//- TODO: Memory panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory
.panel-body.text-center
.progress
.progress-bar-host(role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | percentage}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | percentage}}")
small {{host.name_label}}
.progress-bar.progress-bar-vm(ng-repeat="VM in vms | map | orderBy:natural('name_label') track by VM.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | percentage}}", xo-sref="VMs_view({id: VM.id})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | percentage}}")
small {{VM.name_label}}
ul.list-inline.text-center
li Total: {{host.memory.size | bytesToSize}}
li Currently used: {{host.memory.usage | bytesToSize}}
li Available: {{host.memory.size-host.memory.usage | bytesToSize}}
//- SR panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
.panel-body
table.table.table-hover
tr
th Name
th Format
th Size
th Physical/Allocated usage
th Type
th Status
//- TODO: display PBD status for each SR of this host (connected or not)
//- Shared SR
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in sharedSrs | map | orderBy:natural('name_label') track by SR.id")
td.oneliner
| {{SR.name_label}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.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}}")
td
span.label.label-primary Shared
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)")
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)")
i.fa.fa-link.fa-lg
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
i.fa.fa-ban.fa-lg
//- Local SR
//- TODO: migrate to SRs and not PBDs when implemented in xo-server spec
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id")
td
| {{SR.name_label}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.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}}")
td
span.label.label-info Local
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)")
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)")
i.fa.fa-link.fa-lg
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
i.fa.fa-ban.fa-lg
//- Networks/Interfaces panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| Interfaces
.panel-body
table.table.table-hover
th.col-md-1 Device
th.col-md-1 VLAN
th.col-md-1 Address
th.col-md-2 MAC
th.col-md-1 MTU
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}}
span.label.label-primary(ng-if="PIF.management") XAPI
td
span(ng-if="PIF.vlan > -1")
| {{PIF.vlan}}
span(ng-if="PIF.vlan == -1")
| -
td.oneliner {{PIF.IP}} ({{PIF.mode}})
td.oneliner {{PIF.MAC}}
td {{PIF.MTU}}
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)")
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)")
i.fa.fa-link.fa-lg
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)")
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")
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 }}')
.form-group
label(for = 'newNetworkPIF') Interface&nbsp;
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in host.$PIFs')
option(value = '', disabled) None
| &nbsp;
.form-group
label.control-label(for = 'newNetworkName') Name&nbsp;
input#newNetworkName.form-control(type = 'text', ng-model = 'newNetworkName', required)
| &nbsp;
.form-group
label.control-label(for = 'newNetworkDescription') Description&nbsp;
input#newNetworkDescription.form-control(type = 'text', ng-model = 'newNetworkDescription', placeholder= 'Network created with Xen Orchestra')
| &nbsp;
.form-group
label.control-label(for = 'newNetworkVlan') VLAN&nbsp;
input#newNetworkVlan.form-control(type = 'text', ng-model = 'newNetworkVlan', placeholder = 'Defaut: no VLAN')
| &nbsp;
.form-group
label(for = 'newNetworkMTU') MTU&nbsp;
input#newNetworkMTU.form-control(type = 'text', ng-model = 'newNetworkMTU', placeholder = 'Default: 1500')
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-plus-square
| Create
span(ng-if = 'createNetworkWaiting')
| &nbsp;
i.xo-icon-loading-sm
br
//- CPU and Logs panels
.grid-sm
//- Task panel
.panel.panel-default
.panel-heading.panel-title(ng-if="tasks | isNotEmpty")
i.fa.fa-spinner.fa-pulse
| Pending tasks
.panel-heading.panel-title(ng-if="tasks | isEmpty")
i.fa.fa-spinner
| Pending tasks
.panel-body
p.center(ng-if="tasks | isEmpty") No recent tasks
table.table.table-hover(ng-if="tasks | isNotEmpty")
th Date
th Progress
th Name
//- TODO: working reverse order, from recent to oldest
tr(ng-repeat="task in tasks | map | orderBy:'created':true track by task.id")
td.oneliner {{task.created * 1e3 | date:'medium'}}
td
.progress-condensed
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(role="progressbar", aria-valuemin="0", aria-valuenow="{{task.progress*100}}", aria-valuemax="100", style="width: {{task.progress*100}}%", tooltip="Progress: {{task.progress*100 | number:1}}%")
| {{task.progress*100 | number:1}}%
td.oneliner
| {{task.name_label}}
span.pull-right.btn-group.quick-buttons
a(xo-click="cancelTask(task.id)")
i.fa.fa-times.fa-lg(tooltip="Cancel this task")
a(xo-click="destroyTask(task.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this task")
//- Logs panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="host.messages | isNotEmpty", 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
table.table.table-hover(ng-if="host.messages | isNotEmpty")
th Date
th Name
tr(ng-repeat="message in host.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
span.pull-right.btn-group.quick-buttons
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')
pagination(boundary-links="true", total-items="host.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid-sm
//- Patches panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-file-code-o
| 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")
i.fa.fa-download
.panel-body
table.table.table-hover(ng-if="poolPatches || updates")
th.col-sm-2 Name
th.col-sm-5 Description
th.col-sm-3 Applied/Released date
th.col-sm-1 Size
th.col-sm-1 Status
tr(
ng-repeat="patch in updates"
ng-if="!isPoolPatch(patch)"
)
td.oneliner {{patch.name}}
td.oneliner
a(href="{{patch.documentationUrl}}", target="_blank") {{patch.description}}
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.label.label-danger Missing
tr(ng-repeat="patch in poolPatches | map | slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
td.oneliner {{patch.name}}
td.oneliner {{patch.description}}
//- TODO: use a proper function for patch date, like poolPatchToHostPatch
td.oneliner {{((patch.$host_patches[0]) | resolve).time*1e3 | date:"medium"}}
td {{patch.size | bytesToSize}}
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
.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=">>")
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-plug
| PCI Devices
.panel-body
p.center(ng-if="!host.$PCIs") No PCI devices available
table.table.table-hover(ng-if="host.$PCIs")
th PCI Info
th Device Name
tr(ng-repeat="pci in host.$PCIs | resolve | orderBy:'pci_id' | slice:(5*(currentPCIPage-1)):(5*currentPCIPage) track by pci.id")
td.oneliner {{pci.pci_id}} ({{pci.class_name}})
td.oneliner {{pci.device_name}}
.center(ng-if = '(host.$PCIs | resolve).length > 5')
pagination(boundary-links="true", total-items="(host.$PCIs | resolve).length", ng-model="$parent.currentPCIPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-desktop
| GPUs
.panel-body
p.center(ng-if="host.$PGPUs.length === 0") No GPUs available
table.table.table-hover(ng-if="host.$PGPUs.length !== 0")
th Device
tr(ng-repeat="pgpu in host.$PGPUs | resolve | orderBy:'device' | slice:(5*(currentGPUPage-1)):(5*currentGPUPage) track by pgpu.id")
td.oneliner {{pgpu.device}}
.center(ng-if = '(host.$PGPUs | resolve).length > 5')
pagination(boundary-links="true", total-items="(host.$PGPUs | resolve).length", ng-model="$parent.currentGPUPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")

View File

@ -1,35 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import xoTag from 'tag'
import xoApi from 'xo-api'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.list', [
uiRouter,
xoApi,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('list', {
url: '/list',
controller: 'ListCtrl as list',
template: view
})
})
.controller('ListCtrl', function (xoApi) {
this.hosts = xoApi.getView('host')
this.pools = xoApi.getView('pool')
this.SRs = xoApi.getView('SR')
this.VMs = xoApi.getView('VM')
this.hostsByPool = xoApi.getIndex('hostsByPool')
this.runningHostsByPool = xoApi.getIndex('runningHostsByPool')
this.vmsByContainer = xoApi.getIndex('vmsByContainer')
})
// A module exports its name.
.name

View File

@ -1,152 +0,0 @@
//- TODO: print a message when no entries.
//- If it's a (named) pool.
.grid.flat-object(ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by pool.id", ng-if="pool.name_label", xo-sref="pools_view({id: pool.id})")
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-pool
//- Properties & tags.
.grid-cell
//- Properties.
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{pool.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{pool.name_description}}
.grid-cell.flat-cell(ng-init="default_SR = (pool.default_SR | resolve)")
div(ng-if="default_SR")
| Default SR:
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
div(ng-if="!default_SR")
em No default SR.
.grid-cell.flat-cell(ng-init="master = (pool.master | resolve)")
div(ng-if="master")
| Master:
a(ui-sref="hosts_view({id: master.id})") {{master.name_label}}
div(ng-if="!master")
em Unknown master.
.grid-cell.flat-cell
div(ng-if="pool.HA_enabled")
| HA enabled
div(ng-if="!pool.HA_enabled")
| HA disabled
.grid-cell.flat-cell
| {{list.runningHostsByPool[pool.id] | count}}/{{list.hostsByPool[pool.id] | count}} hosts
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag &nbsp;
xo-tag(object = 'pool')
//- /Tags.
//- /Properties & tags.
//- /Pool.
//- If it's a host.
.grid.flat-object(ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by host.id", xo-sref="hosts_view({id: host.id})")
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
//- Properties & tags.
.grid-cell
//- Properties.
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{host.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{host.name_description}}
.grid-cell.flat-cell
| Address: {{host.address}}
//- .grid-cell.flat-cell
//- | {{host.$vCPUs}} vCPUs used on {{host.CPUs["cpu_count"]}} cores
.grid-cell.flat-cell
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | percentage}}", tooltip="RAM: {{[host.memory.usage, host.memory.size] | percentage}} allocated")
| {{[host.memory.usage, host.memory.size] | percentage}}
.grid-cell.flat-cell
| {{list.vmsByContainer[host.id] | count}} VMs running
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag &nbsp;
xo-tag(object = 'host')
//- /Tags.
//- /Properties & tags.
//- /Host.
//- If it's a VM.
.grid.flat-object(ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})")
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-vm(class="xo-color-{{VM.power_state | lowercase}}")
//- Properties & tags.
.grid-cell
//- Properties.
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{VM.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{VM.name_description}}
.grid-cell.flat-cell
| Address: {{VM.addresses["0/ip"]}}
.grid-cell.flat-cell
| {{VM.CPUs.number}} vCPUs
.grid-cell.flat-cell
| {{VM.memory.size | bytesToSize}} RAM
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)")
div(ng-if="'pool' === container.type")
| Resident on:
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
div(ng-if="'host' === container.type", ng-init="pool = (container.$poolId | resolve)")
| Resident on:
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
small(ng-if="pool.name_label")
| (
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
| )
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag &nbsp;
xo-tag(object = 'VM')
//- /Tags.
//- /Properties & tags.
//- /VM.
//- If it's a SR.
.grid.flat-object(ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by SR.id", xo-sref="SRs_view({id: SR.id})")
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-sr
//- Properties & tags.
.grid-cell
//- Properties.
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{SR.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{SR.name_description}}
.grid-cell.flat-cell
| Usage: {{[SR.usage, SR.size] | percentage}} ({{SR.usage | bytesToSize}}/{{SR.size | bytesToSize}})
.grid-cell.flat-cell
| Type: {{SR.SR_type}}
.grid-cell.flat-cell(ng-init="container = (SR.$container | resolve)")
div(ng-if="'pool' === container.type")
strong
| Shared on:
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
div(ng-if="'host' === container.type")
| Connected to:
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag &nbsp;
xo-tag(object = 'SR')
//- /Tags.
//- /Properties & tags.
//- /SR.

View File

@ -1,51 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import updater from '../updater'
import xoServices from 'xo-services'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.navbar', [
uiRouter,
updater,
xoServices
])
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater) {
this.updater = updater
// TODO: It would make sense to inject xoApi in the scope.
Object.defineProperties(this, {
status: {
get: () => xoApi.status
},
user: {
get: () => xoApi.user
}
})
this.logIn = xoApi.logIn
this.logOut = function () {
xoApi.logOut()
}
// When a searched is entered, we must switch to the list view if
// necessary.
this.ensureListView = function () {
$state.go('list')
}
this.tasks = xoApi.getView('runningTasks')
})
.directive('navbar', function () {
return {
restrict: 'E',
controller: 'NavbarCtrl as navbar',
template: view,
scope: {}
}
})
// A module exports its name.
.name

View File

@ -1,140 +0,0 @@
nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
//- Brand and toggle get grouped for better mobile display
.navbar-header
//- Button used to (un)collapse on mobile display.
button.navbar-toggle(type="button", ng-init="collapsed = true", ng-click="collapsed = !collapsed")
span.sr-only Toggle navigation
span.icon-bar
span.icon-bar
span.icon-bar
//- Brand name.
a.navbar-brand(ui-sref = 'index')
img.navbar-logo(src="images/logo.png")
| Xen Orchestra
//- All navbar items are collapsed on mobile display.
.collapse.navbar-collapse(ng-class="!collapsed && 'in'")
//- Search form of the navbar.
form.navbar-form.navbar-left(role="search", style="width: 250px")
//- Forced width due to issue with `input`s (https://github.com/twbs/bootstrap/issues/9950.
.input-group
input.form-control.inverse(
type = 'text'
placeholder = ''
ng-model = '$root.listFilter'
ng-change = 'navbar.ensureListView()'
)
span.input-group-btn
button.btn.btn-search(
type = 'button'
ng-click = 'navbar.ensureListView()'
)
i.fa.fa-search
//- /Search form.
ul.nav.navbar-nav
li
a(href="https://xen-orchestra.com/#/pricing?pk_campaign=xoa_source", target="_blank", tooltip="Source version without Pro support. Use in production at your own risk.")
i.xo-icon-info.text-danger
span.hidden-sm No Pro Support!
//- Right items of the navbar.
ul.nav.navbar-nav.navbar-right
li.navbar-text(ng-if="'disconnected' === navbar.status")
i.xo-icon-error
| Disconnected from XO-Server
li.navbar-text(ng-if="'connecting' === navbar.status")
i.fa.fa-refresh.fa-spin
| Connecting to XO-Server
//- Running tasks
li.disabled(ng-if="!navbar.tasks.size", tooltip="No running tasks")
a.dropdown-toggle.inverse
i.xo-icon-task
li.dropdown(dropdown, ng-if="navbar.tasks.size")
a.dropdown-toggle.inverse(dropdown-toggle)
i.xo-icon-task
ul.dropdown-menu.inverse
li.task-menu(
ng-repeat="task in navbar.tasks.all | orderBy:natural('name_label') track by task.id"
)
a(
ui-sref="hosts_view({id: task.$host})"
tooltip = "{{task.name_label}}"
)
//- i.fa.fa-spinner.fa-fw
//- | {{task.name_label}}
.progress-condensed
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(
role = "progressbar"
aria-valuemin = "0"
aria-valuenow = "{{task.progress*100}}"
aria-valuemax = "100"
style = "width: {{task.progress*100}}%"
)
| {{task.progress*100 | number:1}}%
//- Main menu.
li.dropdown(dropdown)
a.dropdown-toggle.inverse(dropdown-toggle)
i.fa.fa-th
ul.dropdown-menu.inverse
li(
ui-sref-active = 'active'
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref = 'tree')
i.fa.fa-indent
| Tree view
li(ui-sref-active="active")
a(ui-sref="list")
i.fa.fa-align-justify
| Flat view
li(
ui-sref-active="active"
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
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.divider
li(
ui-sref-active = 'active'
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref="settings.index")
i.fa.fa-cog
| Settings
li.divider
li(ui-sref-active="active")
a(ui-sref="about")
i.fa.fa-info-circle(style="color:#5bc0de")
| About
//- /Main menu.
li
a(ui-sref="settings.update")
i.fa.fa-question-circle.text-warning(ng-if = '!navbar.updater.state', tooltip = 'No update information available')
i.fa.fa-question-circle.text-info(ng-if = 'navbar.updater.state == "connected"', tooltip = 'Update information may be available')
i.fa.fa-check.text-success(ng-if = 'navbar.updater.state == "upToDate"', tooltip = 'Your XOA is up-to-date')
i.fa.fa-bell.text-primary(ng-if = 'navbar.updater.state == "upgradeNeeded"', tooltip = 'You need to update your XOA (new version is available)')
i.fa.fa-bell-slash.text-warning(ng-if = 'navbar.updater.state == "registerNeeded"', tooltip = 'Your XOA is not registered for updates')
i.fa.fa-exclamation-triangle.text-danger(ng-if = 'navbar.updater.state == "error"', tooltip = 'Can\'t fetch update information')
li
a(ng-if = '!navbar.user.provider', ui-sref="{{navbar.user.provider ? 'settings.users' : 'settings.user'}}", tooltip="{{navbar.user.email}}")
i.fa.fa-user
span.hidden-sm {{navbar.user.email}}
li
a(ng-click = 'navbar.logOut()')
i.fa.fa-sign-out
| &nbsp;
| &nbsp;
//- /Right items.
//- /Navbar items.
//- /Navbar.

View File

@ -1,422 +0,0 @@
import angular from 'angular'
import Bluebird from 'bluebird'
import forEach from 'lodash.foreach'
import uiRouter from 'angular-ui-router'
import view from './view'
import _indexOf from 'lodash.indexof'
// ===================================================================
export default angular.module('xoWebApp.newSr', [
uiRouter
])
.config(function ($stateProvider) {
$stateProvider.state('SRs_new', {
url: '/srs/new/:container',
controller: 'NewSrCtrl as newSr',
template: view
})
})
.controller('NewSrCtrl', function ($scope, $state, $stateParams, xo, xoApi, notify, modal, bytesToSizeFilter) {
this.reset = function (data = {}) {
this.data = {}
delete this.lockCreation
this.lock = !(
(data.srType === 'Local') &&
(data.srPath && data.srPath.path)
)
}
this.resetLists = function () {
delete this.data.nfsList
delete this.data.scsiList
delete this.lockCreation
this.lock = true
this.resetErrors()
}
this.resetErrors = function () {
delete this.data.error
}
/*
* Loads NFS paths and iScsi iqn`s
*/
this.populateSettings = function (type, server, auth, user, password) {
this.reset()
this.loading = true
server = this._parseAddress(server)
if (type === 'NFS' || type === 'NFS_ISO') {
xoApi.call('sr.probeNfs', {
host: this.container.id,
server: server.host
})
.then(response => this.data.paths = response)
.catch(error => notify.warning({
title: 'NFS Detection',
message: error.message
}))
.finally(() => this.loading = false)
} else if (type === 'iSCSI') {
let params = {
host: this.container.id
}
if (auth) {
params.chapUser = user
params.chapPassword = password
}
params.target = server.host
if (server.port) {
params.port = server.port
}
xoApi.call('sr.probeIscsiIqns', params)
.then(response => {
if (response.length > 0) {
this.data.iqns = response
} else {
notify.warning({
title: 'iSCSI Detection',
message: 'No IQNs found'
})
}
})
.catch(error => notify.warning({
title: 'iSCSI Detection',
message: error.message
}))
.finally(() => this.loading = false)
} else {
this.loading = false
}
}
/*
* Loads iScsi LUNs
*/
this.populateIScsiIds = function (iqn, auth, user, password) {
delete this.data.iScsiIds
this.loading = true
let params = {
host: this.container.id,
target: iqn.ip,
targetIqn: iqn.iqn
}
if (auth) {
params.chapUser = user
params.chapPassword = password
}
xoApi.call('sr.probeIscsiLuns', params)
.then(response => {
forEach(response, item => {
item.display = 'LUN ' + item.id + ': ' +
item.serial + ' ' + bytesToSizeFilter(item.size) +
' (' + item.vendor + ')'
})
this.data.iScsiIds = response
})
.catch(error => notify.warning({
title: 'LUNs Detection',
message: error.message
}))
.finally(() => this.loading = false)
}
this._parseAddress = function (address) {
let index = address.indexOf(':')
let port = false
let host = address
if (index > -1) {
port = address.substring(index + 1)
host = address.substring(0, index)
}
return {
host,
port
}
}
this._prepareNfsParams = function (data) {
let server = this._parseAddress(data.srServer)
let params = {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
server: server.host,
serverPath: data.srPath.path
}
return params
}
this._prepareScsiParams = function (data) {
let params = {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
target: data.srIqn.ip,
targetIqn: data.srIqn.iqn,
scsiId: data.srIScsiId.scsiId
}
let server = this._parseAddress(data.srServer)
if (server.port) {
params.port = server.port
}
if (data.srAuth) {
params.chapUser = data.srChapUser
params.chapPassword = data.srChapPassword
}
return params
}
this.createSR = function (data) {
this.lock = true
this.creating = true
let operationToPromise
switch (data.srType) {
case 'NFS':
let nfsParams = this._prepareNfsParams(data)
operationToPromise = this._checkNfsExistence(nfsParams)
.then(() => xoApi.call('sr.createNfs', nfsParams))
break
case 'iSCSI':
let scsiParams = this._prepareScsiParams(data)
operationToPromise = this._checkScsiExistence(scsiParams)
.then(() => xoApi.call('sr.createIscsi', scsiParams))
break
case 'lvm':
let device = data.srDevice.device
operationToPromise = xoApi.call('sr.createLvm', {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
device
})
break
case 'NFS_ISO':
case 'Local':
let server = this._parseAddress(data.srServer || '')
let path = (
data.srType === 'NFS_ISO'
? server.host + ':'
: ''
) + data.srPath.path
operationToPromise = xoApi.call('sr.createIso', {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
path
})
break
default:
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'})
break
}
operationToPromise
.then(id => {
$state.go('SRs_view', {id})
})
.catch(error => {
notify.error({
title: 'Storage Creation Error',
message: error.message
})
})
.finally(() => {
this.lock = false
this.creating = false
})
}
this._checkScsiExistence = function (params) {
this.resetLists()
return xoApi.call('sr.probeIscsiExists', params)
.then(response => {
if (response.length > 0) {
this.data.scsiList = response
return modal.confirm({
title: 'Previous LUN Usage',
message: 'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
})
}
return true
})
}
this._checkNfsExistence = function (params) {
this.resetLists()
return xoApi.call('sr.probeNfsExists', params)
.then(response => {
if (response.length > 0) {
this.data.nfsList = response
return modal.confirm({
title: 'Previous Path Usage',
message: 'This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
})
}
return true
})
}
const hostsByPool = xoApi.getIndex('hostsByPool')
const srsByContainer = xoApi.getIndex('srsByContainer')
this._gatherConnectedUuids = function () {
const srIds = []
// Shared SRs.
forEach(srsByContainer[this.container.$poolId], sr => {
srIds.push(sr.id)
})
// Local SRs.
forEach(hostsByPool[this.container.$poolId], host => {
forEach(srsByContainer[host.id], sr => {
srIds.push(sr.id)
})
})
return srIds
}
this._processSRList = function (list) {
let inUse = false
let SRs = this._gatherConnectedUuids()
forEach(list, item => {
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse
})
this.lockCreation = inUse
return list
}
this.loadScsiList = function (data) {
this.resetLists()
this.loading = true
let params = this._prepareScsiParams(data)
xoApi.call('sr.probeIscsiExists', params)
.then(response => {
if (response.length > 0) {
this.data.scsiList = this._processSRList(response)
}
this.lock = !Boolean(data.srIScsiId)
})
.catch(error => {
notify.error({
title: 'iSCSI Error',
message: error.message
})
})
.finally(() => this.loading = false)
}
this.loadNfsList = function (data) {
this.resetLists()
let server = this._parseAddress(data.srServer)
xoApi.call('sr.probeNfsExists', {
host: this.container.id,
server: server.host,
serverPath: data.srPath.path
})
.then(response => {
if (response.length > 0) {
this.data.scsiList = this._processSRList(response)
}
this.lock = !Boolean(data.srPath.path)
})
.catch(error => {
notify.error({
title: 'NFS error',
message: error.message
})
})
}
this.reattachNfs = function (uuid, {name, nameError}, {desc, descError}, iso) {
this._reattach(uuid, 'nfs', {name, nameError}, {desc, descError}, iso)
}
this.reattachIScsi = function (uuid, {name, nameError}, {desc, descError}) {
this._reattach(uuid, 'iscsi', {name, nameError}, {desc, descError})
}
this._reattach = function (uuid, type, {name, nameError}, {desc, descError}, iso = false) {
this.resetErrors()
let method = 'sr.reattach' + (iso ? 'Iso' : '')
if (nameError || descError) {
this.data.error = {
name: nameError,
desc: descError
}
notify.warning({
title: 'Missing parameters',
message: 'Complete the General section information, please'
})
} else {
this.lock = true
this.attaching = true
xoApi.call(method, {
host: this.container.id,
uuid,
nameLabel: name,
nameDescription: desc,
type
})
.then(id => {
$state.go('SRs_view', {id})
})
.catch(error => notify.error({
title: 'reattach',
message: error.message
}))
.finally(() => {
this.lock = false
this.attaching = false
})
}
}
this.reset()
$scope.$watch(() => xoApi.get($stateParams.container), container => {
this.container = container
})
})
// A module exports its name.
.name

View File

@ -1,183 +0,0 @@
.grid
.panel.panel-default
p.page-title
i.xo-icon-sr
| Add SR on&nbsp;
a(ng-if="'pool' === newSr.container.type", ui-sref="pools_view({id: newSr.container.id})")
| {{newSr.container.name_label}}
a(ng-if="'host' === newSr.container.type", ui-sref="hosts_view({id: newSr.container.id})")
| {{newSr.container.name_label}}
form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
.grid
//- Choose SR type panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-info-circle
| General
.panel-body
.form-group
label.col-sm-3.control-label Type
.col-sm-9
select.form-control(ng-change = 'newSr.reset(formData)', ng-model = 'formData.srType', name = 'srType', ng-required = 'true')
option(value="") -- Choose a type of SR --
optgroup(label="VDI SR")
option(value="NFS") NFS
option(value="iSCSI") iSCSI
option(value="lvm") Local LVM
optgroup(label="ISO SR")
option(value="Local") Local
option(value="NFS_ISO") NFS ISO
.form-group(ng-class = '{"has-error": newSr.data.error.name}')
label.col-sm-3.control-label Name
.col-sm-9
input.form-control(type="text", placeholder="", name = 'srName', ng-model = 'formData.srName', ng-required = 'true')
.form-group(ng-class = '{"has-error": newSr.data.error.desc}')
label.col-sm-3.control-label Description
.col-sm-9
input.form-control(type="text", placeholder="SR Created by Xen Orchestra", name = 'srDesc', ng-model = 'formData.srDesc', ng-required = 'true')
//- Choose SR details
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| Settings
.panel-body
.form-group(ng-if = 'formData.srType === "NFS" || formData.srType === "iSCSI" || formData.srType === "NFS_ISO"')
label.col-sm-3.control-label
| Server
span(ng-if = 'formData.srType === "iSCSI"')
| &nbsp;(auth&nbsp;
input(type = 'checkbox', ng-model = 'formData.srAuth')
| &nbsp;)
.col-sm-9
.input-group
input.form-control(type="text", placeholder='address{{ formData.srType === "iSCSI" ? "[:port]" : "" }}', name = 'srServer', ng-model = 'formData.srServer', required)
span.input-group-btn
button.btn.btn-default(type = 'button', ng-click = 'newSr.populateSettings(formData.srType, formData.srServer, formData.srAuth, formData.srChapUser, formData.srChapPassword)')
i.fa.fa-search
//- For Local LVM
.form-group(ng-if = 'formData.srType === "lvm"')
label.col-sm-3.control-label Device
.col-sm-9
input.form-control(
ng-if = 'formData.srType === "lvm"'
type = 'text'
name = 'srDevice'
ng-model = 'formData.srDevice.device'
placeholder = 'Device, e.g /dev/sda...'
ng-change = 'newSr.lock = !formData.srDevice.device'
required
)
.form-group(ng-if = 'newSr.data.paths || formData.srType === "Local"')
label.col-sm-3.control-label Path
.col-sm-9
//- For NFS
select.form-control(
ng-if = 'newSr.data.paths'
name = 'srPath'
ng-change = 'newSr.loadNfsList(formData)'
ng-model = 'formData.srPath'
ng-options = 'item.path for item in newSr.data.paths', required)
option(value = '', disabled) -- Choose path --
//- For Local
input.form-control(
ng-if = 'formData.srType === "Local"'
type = 'text'
name = 'srPath'
ng-model = 'formData.srPath.path'
ng-change = 'newSr.lock = !formData.srPath.path'
required
)
//- For iScsi
.form-group(ng-if = 'formData.srType === "iSCSI"')
.col-sm-9.col-sm-offset-3.form-inline(ng-if = 'formData.srAuth')
label.sr-only(for = 'chapUser') User
input#chapUser.form-control(type = 'text', ng-model = 'formData.srChapUser', placeholder = 'user', ng-required = 'formData.srAuth')
| &ensp;
label.sr-only(for = 'chapUser') Password
input#chapPassword.form-control(type = 'password', ng-model = 'formData.srChapPassword', placeholder = 'password', ng-required = 'formData.srAuth')
.form-group(ng-if = 'newSr.data.iqns')
label.col-sm-3.control-label IQN
.col-sm-9
select.form-control(ng-change = 'newSr.populateIScsiIds(formData.srIqn, formData.srAuth, formData.srChapUser, formData.srChapPassword)', name = 'srIqn', ng-model = 'formData.srIqn', ng-options = '(item.iqn + " (" + item.ip + ")") for item in newSr.data.iqns', required)
option(value = '', disabled) -- Choose IQN --
.form-group(ng-if = 'newSr.data.iScsiIds')
label.col-sm-3.control-label LUN
.col-sm-9
select.form-control(name = 'srIScsiId', ng-change = 'newSr.loadScsiList(formData)', ng-model = 'formData.srIScsiId', ng-options = 'item.display for item in newSr.data.iScsiIds', required)
option(value = '', disabled) -- Choose LUN --
.form-group.text-center(ng-if = 'newSr.loading')
i.xo-icon-loading
.grid(ng-if = 'newSr.data.nfsList && newSr.data.nfsList.length > 0')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-eye
| NFS storage use
.panel-body
table.table.table-condensed
tr
th.text-center Storage UUID
th
tr(ng-repeat = 'nfsSr in newSr.data.nfsList')
td.text-center {{ nfsSr.uuid }}
td.text-center(ng_if = '!nfsSr.used')
button.btn.btn-sm.btn-primary(type = 'button', ng-class = '{disabled: newSr.lock}', ng-click = 'newSr.reattachNfs(nfsSr.uuid, {name: formData.srName, nameError: srForm.srName.$error.required}, {desc: formData.srDesc, descError: srForm.srDesc.$error.required}, "NFS_ISO" === formData.srType)') Reattach
td.text-center(ng_if = 'nfsSr.used', ng-class = '{disabled: newSr.lock}')
button.btn.btn-sm.btn-danger(ui-sref = 'SRs_view({id: nfsSr.uuid})', ng-class = '{disabled: newSr.lock}')
i.fa.fa-eye
| In use
p.text-center(ng-if = 'newSr.attaching')
i.xo-icon-loading
.grid(ng-if = 'newSr.data.scsiList && newSr.data.scsiList.length > 0')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-eye
| iSCSI storage use
.panel-body
table.table.table-condensed
tr
th.text-center Storage UUID
th
tr(ng-repeat = 'scsiSr in newSr.data.scsiList')
td.text-center {{ scsiSr.uuid }}
td.text-center(ng_if = '!scsiSr.used')
button.btn.btn-sm.btn-primary(type = 'button', ng-class = '{disabled: newSr.lock}', ng-click = 'newSr.reattachIScsi(scsiSr.uuid, {name: formData.srName, nameError: srForm.srName.$error.required}, {desc: formData.srDesc, descError: srForm.srDesc.$error.required})') Reattach
td.text-center(ng_if = 'scsiSr.used')
button.btn.btn-sm.btn-danger(ui-sref = 'SRs_view({id: scsiSr.uuid})', ng-class = '{disabled: newSr.lock}')
i.fa.fa-eye
| In use
p.text-center(ng-if = 'newSr.attaching')
i.xo-icon-loading
//- Summary
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flag-checkered
| Summary
.panel-body
.grid
.grid-cell
p.stat-name
| Name:
p.center.big {{formData.srName}}
.grid-cell
p.stat-name
| Type:
p.center.big {{formData.srType}}
.grid-cell
div(ng-if = 'formData.srType === "iSCSI"')
p.stat-name Size
p.center.big {{formData.srIScsiId.size | bytesToSize}}
div(ng-if = 'formData.srType === "NFS"')
p.stat-name Path
p.center.big {{formData.srPath.path}}
p.center
button.btn.btn-lg.btn-primary(type="submit", ng-disabled = 'newSr.lock || newSr.lockCreation')
i.fa.fa-play
| &nbsp;Create SR&nbsp;
i.xo-icon-loading-sm(ng-if = 'newSr.creating')

View File

@ -1,285 +0,0 @@
angular = require 'angular'
cloneDeep = require 'lodash.clonedeep'
filter = require 'lodash.filter'
forEach = require 'lodash.foreach'
#=====================================================================
module.exports = angular.module 'xoWebApp.newVm', [
require 'angular-ui-router'
]
.config ($stateProvider) ->
$stateProvider.state 'VMs_new',
url: '/vms/new/:container'
controller: 'NewVmsCtrl'
template: require './view'
.controller 'NewVmsCtrl', (
$scope, $stateParams, $state
xoApi, xo
bytesToSizeFilter, sizeToBytesFilter
notify
) ->
{get} = xoApi
removeItems = do ->
splice = Array::splice.call.bind Array::splice
(array, index, n) -> splice array, index, n ? 1
merge = do ->
push = Array::push.apply.bind Array::push
(args...) ->
result = []
for arg in args
push result, arg if arg?
result
pool = default_SR = null
host = null
do (
networks = xoApi.getIndex('networksByPool')
srsByContainer = xoApi.getIndex('srsByContainer')
vmTemplatesByContainer = xoApi.getIndex('vmTemplatesByContainer')
poolSrs = null
hostSrs = null
poolTemplates = null
hostTemplates = null
) ->
Object.defineProperties($scope, {
networks: {
get: () => pool && networks[pool.id]
}
})
updateSrs = () =>
srs = []
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
$scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')
updateTemplates = () =>
templates = []
poolTemplates and forEach(poolTemplates, (template) => templates.push(template))
hostTemplates and forEach(hostTemplates, (template) => templates.push(template))
$scope.templates = templates
$scope.$watchCollection(
() => pool and srsByContainer[pool.id],
(srs) =>
poolSrs = srs
updateSrs()
)
$scope.$watchCollection(
() => host and srsByContainer[host.id],
(srs) =>
hostSrs = srs
updateSrs()
)
$scope.$watchCollection(
() => pool and vmTemplatesByContainer[pool.id],
(templates) =>
poolTemplates = templates
updateTemplates()
)
$scope.$watchCollection(
() => host and vmTemplatesByContainer[host.id],
(templates) =>
hostTemplates = templates
updateTemplates()
)
$scope.$watch(
-> get $stateParams.container
(container) ->
$scope.container = container
# If the container was not found, no need to continue.
return unless container?
if container.type is 'host'
host = container
pool = (get container.$poolId) ? {}
else
host = {}
pool = container
default_SR = get pool.default_SR
default_SR = if default_SR then default_SR.id else ''
)
$scope.availableMethods = {}
$scope.CPUs = ''
$scope.pv_args = ''
$scope.installation_cdrom = ''
$scope.installation_method = ''
$scope.installation_network = ''
$scope.memory = ''
$scope.name_description = ''
$scope.name_label = ''
$scope.template = ''
$scope.VDIs = []
$scope.VIFs = []
$scope.isDiskTemplate = false
$scope.addVIF = do ->
id = 0
->
$scope.VIFs.push {
id: id++
network: ''
}
$scope.addVIF()
$scope.removeVIF = (index) -> removeItems $scope.VIFs, index
$scope.moveVDI = (index, direction) ->
{VDIs} = $scope
newIndex = index + direction
[VDIs[index], VDIs[newIndex]] = [VDIs[newIndex], VDIs[index]]
$scope.removeVDI = (index) -> removeItems $scope.VDIs, index
VDI_id = 0
$scope.addVDI = ->
$scope.VDIs.push {
id: VDI_id++
bootable: false
size: ''
SR: default_SR
type: 'system'
}
# When the selected template changes, updates other variables.
$scope.$watch 'template', (template) ->
return unless template
{install_methods} = template.template_info
availableMethods = $scope.availableMethods = Object.create null
for method in install_methods
availableMethods[method] = true
if install_methods.length is 1 # FIXME: does not work with network.
$scope.installation_method = install_methods[0]
else
delete $scope.installation_method
VDIs = $scope.VDIs = cloneDeep template.template_info.disks
# if the template has no config disk
# nor it's Other install media (specific case)
# then do NOT display disk and network panel
if VDIs.length is 0 and template.name_label isnt 'Other install media'
$scope.isDiskTemplate = true
$scope.VIFs.length = 0
else $scope.isDiskTemplate = false
for VDI in VDIs
VDI.id = VDI_id++
VDI.size = bytesToSizeFilter VDI.size
VDI.SR or= default_SR
$scope.createVM = ->
{
CPUs
pv_args
installation_cdrom
installation_method
installation_network
memory
name_description
name_label
template
VDIs
VIFs
} = $scope
# Does not edit the displayed data directly.
VDIs = cloneDeep VDIs
for VDI, index in VDIs
# Removes the dummy identifier used for AngularJS.
delete VDI.id
# Adds the device number based on the index.
VDI.device = "#{index}"
# Transforms the size from human readable format to bytes.
VDI.size = sizeToBytesFilter VDI.size
# TODO: handles invalid values.
# Does not edit the displayed data directly.
VIFs = cloneDeep VIFs
for VIF in VIFs
# Removes the dummy identifier used for AngularJS.
delete VIF.id
# Removes the MAC address if empty.
if 'MAC' of VIF
VIF.MAC = VIF.MAC.trim()
delete VIF.MAC unless VIF.MAC
if installation_method is 'cdrom'
installation = {
method: 'cdrom'
repository: installation_cdrom
}
else if installation_network
matches = /^(http|ftp|nfs)/i.exec installation_network
throw new Error 'invalid network URL' unless matches
installation = {
method: matches[1].toLowerCase()
repository: installation_network
}
else if installation_method is 'pxe'
installation = {
method: 'network'
repository: 'pxe'
}
else
installation = undefined
data = {
installation
pv_args
name_label
template: template.id
VDIs
VIFs
}
# TODO:
# - disable the form during creation
# - indicate the progress of the operation
notify.info {
title: 'VM creation'
message: 'VM creation started'
}
xoApi.call('vm.create', data).then (id) ->
# If nothing to sets, just stops.
return id unless CPUs or name_description or memory
data = {
id
}
data.CPUs = +CPUs if CPUs
if name_description
data.name_description = name_description
if pv_args
data.pv_args = pv_args
if memory
memory = sizeToBytesFilter memory
# FIXME: handles invalid entries.
data.memory = memory
xoApi.call('vm.set', data).then -> id
.then (id) ->
$state.go 'VMs_view', { id }
.catch (error) ->
notify.error {
title: 'VM creation'
message: 'The creation failed'
}
console.log error
# A module exports its name.
.name

View File

@ -1,236 +0,0 @@
.grid
.panel.panel-default
p.page-title
i.xo-icon-vm
| Create VM on&nbsp;
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.id})")
| {{container.name_label}}
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.id})")
| {{container.name_label}}
//- Add server panel
form.form-horizontal(ng-submit="createVM()")
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-info-circle
| VM info
.panel-body
.form-group
label.col-sm-3.control-label Template
.col-sm-9
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="")
.form-group
label.col-sm-3.control-label Name
.col-sm-9
input.form-control(type="text", placeholder="Name of your new VM", required="", ng-model="name_label")
.form-group
label.col-sm-3.control-label Description
.col-sm-9
input.form-control(type="text", placeholder="Optional description of you new VM", ng-model="name_description")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-dashboard
| Performances
.panel-body
.form-group
label.col-sm-3.control-label vCPUs
.col-sm-9
input.form-control(type="text", placeholder="{{template.CPUs.number}}", ng-model="CPUs")
.form-group
label.col-sm-3.control-label RAM
.col-sm-9
input.form-control(type="text", placeholder="{{template.memory.size | bytesToSize}}", ng-model="memory")
.grid(ng-if="isDiskTemplate")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-info-circle
| Template info
.panel-body
p.center This template will create automatically a VM with:
.col-md-6
ul(ng-repeat="VIF in template.VIFs | resolve | orderBy:natural('device') track by VIF.id")
li Interface \#{{VIF.device}} (MTU {{VIF.MTU}}) on {{(VIF.$network | resolve).name_label}}
.col-md-6
ul(ng-repeat = 'VBD in (template.$VBDs | resolve) track by VBD.id')
li Disk {{(VBD.VDI | resolve).name_label}} ({{(VBD.VDI | resolve).size | bytesToSize}}) on {{((VBD.VDI | resolve).$SR | resolve).name_label}}
.grid(ng-if="!isDiskTemplate")
//- Install panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-download
| Install settings
.panel-body
.form-group(ng-show="availableMethods.cdrom")
label.col-sm-3.control-label ISO/DVD
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'cdrom'
required
)
select.form-control.disabled(
ng-disabled="'cdrom' !== installation_method"
ng-model="$parent.installation_cdrom"
required
)
option(value = '') Please select
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.id", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", ng-value="VDI.id")
| {{VDI.name_label}}
.form-group(
ng-show = 'availableMethods.http || availableMethods.ftp || availableMethods.nfs'
)
label.col-sm-3.control-label Network
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'network'
required
)
input.form-control(type="text", ng-disabled="'network' !== installation_method", placeholder="e.g: http://ftp.debian.org/debian", ng-model="$parent.installation_network")
.form-group(ng-show = 'template.virtualizationMode === "hvm"')
label.col-sm-3.control-label PXE
.col-sm-9
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'pxe'
required
)
.form-group(ng-show="template.PV_args")
label.col-sm-3.control-label PV Args
.col-sm-9
input.form-control(type="text", placeholder="{{template.PV_args}}", ng-model="$parent.pv_args")
//- <div class="form-group"> FIXME
//- <label class="col-sm-3 control-label">Home server</label>
//- <div class="col-sm-9">
//- <select class="form-control">
//- <option>Default (auto)</option>
//- </select>
//- </div>
//- </div>
//- Interface panel
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| Interfaces
.panel-body
table.table.table-hover
tr
th MAC
th Network
th.col-md-1 &#160;
//- Buttons
tr(ng-repeat="VIF in VIFs track by VIF.id")
td
input.form-control(type="text", ng-model="VIF.MAC", ng-pattern="/^\s*[0-9a-f]{2}(:[0-9a-f]{2}){5}\s*$/i", placeholder="00:00:00:00:00")
td
select.form-control(
ng-options = 'network.id as network.name_label for network in networks | orderBy:natural("name_label") track by network.id'
ng-model = 'VIF.network'
required
)
option(value = '') Please select
td
.pull-right
button.btn.btn-default(type="button", ng-click="removeVIF($index)", title="Remove this interface")
i.fa.fa-times
.btn-form
p.center
.btn-form
p.center
button.btn.btn-success(type="button", ng-click="addVIF()")
i.fa.fa-plus
| Add interface
//- end of misc and interface panel
//- Disk panel
.grid(ng-if="!isDiskTemplate")
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-disk
| Disks
.panel-body
table.table.table-hover
tr
th.col-md-2 SR
th.col-md-1 Bootable?
th.col-md-2 Size
th.col-md-2 Name
th.col-md-4 Description
th.col-md-1 &#160;
//- Buttons
tr(ng-repeat="VDI in VDIs track by VDI.id")
td
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))")
td.text-center
input(type="checkbox", ng-model="VDI.bootable")
td
input.form-control(type="text", ng-model="VDI.size", required="")
td
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="VDI.name_label")
td
input.form-control(type="text", placeholder="Description of this virtual disk", ng-model="VDI.name_description")
td
.btn-group
button.btn.btn-default(type="button", ng-click="moveVDI($index, -1)", ng-disabled="$first", title="Move this disk up")
i.fa.fa-chevron-up
button.btn.btn-default(type="button", ng-click="moveVDI($index, 1)", ng-disabled="$last", title="Move this disk down")
i.fa.fa-chevron-down
.pull-right
button.btn.btn-default(type="button", ng-click="removeVDI($index)", title="Remove this disk")
i.fa.fa-times
.btn-form
p.center
.btn-form
p.center
button.btn.btn-success(type="button", ng-click="addVDI()")
i.fa.fa-plus
| Add disk
//- Confirmation panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flag-checkered
| Summary
.panel-body
.grid
.grid-cell
p.center.big {{name_label}}
| &nbsp;
span.small(ng-if="template.name_label") ({{template.name_label}})
.grid
.grid-cell
//- p.stat-name vCPUs
p.center.big(tooltip="vCPUs")
| {{CPUs || template.CPUs.number || 0}}x&nbsp;
i.xo-icon-cpu
.grid-cell
//- p.stat-name RAM
p.center.big(tooltip="RAM")
| {{(memory) || (template.memory.size | bytesToSize)}}&nbsp;
i.xo-icon-memory
.grid-cell
//- p.stat-name Disks
p.center.big(tooltip="Disks")
| {{(VDIs.length) || (template.$VBDs.length) || 0}}x&nbsp;
i.xo-icon-disk
.grid-cell
//- p.stat-name Interfaces
p.center.big(tooltip="Network interfaces")
| {{(VIFs.length) || (template.VIFs.length) || 0}}x&nbsp;
i.xo-icon-network
p.center
button.btn.btn-lg.btn-primary(type="submit")
i.fa.fa-play
| Create VM

View File

@ -1,109 +0,0 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import uiRouter from 'angular-ui-router'
import xoTag from 'tag'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.pool', [
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('pools_view', {
url: '/pools/:id',
controller: 'PoolCtrl',
template: view
})
})
.controller('PoolCtrl', function ($scope, $stateParams, xoApi, xo, modal) {
{
const {id} = $stateParams
const hostsByPool = xoApi.getIndex('hostsByPool')
const runningHostsByPool = xoApi.getIndex('runningHostsByPool')
const srsByContainer = xoApi.getIndex('srsByContainer')
Object.defineProperties($scope, {
hosts: {
get: () => hostsByPool[id]
},
runningHosts: {
get: () => runningHostsByPool[id]
},
srs: {
get: () => srsByContainer[id]
}
})
}
$scope.$watch(() => xoApi.get($stateParams.id), function (pool) {
$scope.pool = pool
})
$scope.currentLogPage = 1
$scope.savePool = function ($data) {
let {pool} = $scope
let {name_label, name_description} = $data
$data = {
id: pool.id
}
if (name_label !== pool.name_label) {
$data.name_label = name_label
}
if (name_description !== pool.name_description) {
$data.name_description = name_description
}
xoApi.call('pool.set', $data)
}
$scope.deleteAllLog = function () {
return modal.confirm({
title: 'Log deletion',
message: 'Are you sure you want to delete all the logs?'
}).then(function () {
// TODO: return all promises.
forEach($scope.pool.messages, function (message) {
xo.log.delete(message.id)
console.log('Remove log', message.id)
})
})
}
$scope.deleteLog = function (id) {
console.log('Remove log', id)
return xo.log.delete(id)
}
// $scope.patchPool = ($files, id) ->
// file = $files[0]
// xo.pool.patch id
// .then ({ $sendTo: url }) ->
// return $upload.http {
// method: 'POST'
// url
// data: file
// }
// .progress throttle(
// (event) ->
// percentage = (100 * event.loaded / event.total)|0
// notify.info
// title: 'Upload patch'
// message: "#{percentage}%"
// 6e3
// )
// .then (result) ->
// throw result.status if result.status isnt 200
// notify.info
// title: 'Upload patch'
// message: 'Success'
})
// A module exports its name.
.name

View File

@ -1,151 +0,0 @@
//- TODO: lots of stuff.
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-pool
| {{pool.name_label}}
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="poolSettings.$show()")
i.fa.fa-edit.fa-fw
.panel-body
form(editable-form="", name="poolSettings", onbeforesave="savePool($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="pool.name_label", e-name="name_label", e-form="poolSettings")
| {{pool.name_label}}
dt Description
dd
span(editable-text="pool.name_description", e-name="name_description", e-form="poolSettings")
| {{pool.name_description}}
dt Master
dd(ng-repeat="master in [pool.master] | resolve")
a(ui-sref="hosts_view({id: master.id})")
| {{master.name_label}}
dt Tags
dd
xo-tag(ng-if = 'pool', object = 'pool')
dt(ng-if="pool.default_SR") Default SR
dd(ng-if="pool.default_SR", ng-init="default_SR = (pool.default_SR | resolve)")
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
dt HA
dd
| {{pool.HA_enabled}}
dt UUID
dd {{pool.UUID}}
.btn-form(ng-show="poolSettings.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="poolSettings.$waiting", ng-click="poolSettings.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="poolSettings.$waiting")
i.fa.fa-save
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats
| Stats
.row
.col-xs-6
p.stat-name Hosts:
p.center.big-stat {{hosts | count}}
.col-xs-6
p.stat-name Running:
p.center.big-stat {{runningHosts | count}}
//- Action panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body
.grid-cell.text-center
.grid
.grid-cell.btn-group
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", disabled)
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: pool.id})")
i.xo-icon-vm.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Patch the pool", tooltip-placement="top", type="button", style="width: 90%", ng-file-select = "patchPool($files, pool.id)")
i.fa.fa-file-code-o.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add Host", tooltip-placement="top", type="button", style="width: 90%")
i.xo-icon-host.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Disconnect", tooltip-placement="top", type="button", style="width: 90%; margin-bottom: 0.5em")
i.fa.fa-unlink.fa-2x.fa-fw
//- Hosts panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-host
| Hosts
.panel-body
table.table.table-hover.table-condensed
th Name
th.col-md-4 Description
th.col-md-6 Memory
tr(xo-sref="hosts_view({id: host.id})", ng-repeat="host in hosts | map | orderBy:natural('name_label') track by host.id")
td.oneliner {{host.name_label}}
td.oneliner {{host.name_description}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{host.memory.usage}}", aria-valuemax="{{host.memory.size}}", style="width: {{[host.memory.usage, host.memory.size] | percentage}}")
//- Shared SR panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Shared SR
.panel-body
table.table.table-hover
th Name
th Description
th Type
th Size
th.col-md-4 Physical/Allocated usage
tr(
ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id"
xo-sref="SRs_view({id: SR.id})"
)
td.oneliner {{SR.name_label}}
td.oneliner {{SR.name_description}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.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}}")
//- Logs panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="pool.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
.panel-body
p.center(ng-if="pool.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="pool.messages | isNotEmpty")
th Date
th Name
tr(ng-repeat="message in pool.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
span.pull-right.btn-group.quick-buttons
a(xo-click="deleteLog(message.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
.center(ng-if = '(pool.messages | count) > 5 || currentLogPage > 1')
pagination(boundary-links="true", total-items="pool.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")

View File

@ -1,131 +0,0 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import Bluebird from 'bluebird'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
const HIGH_LEVEL_OBJECTS = {
pool: true,
host: true,
VM: true,
SR: true,
network: true
}
export default angular.module('settings.acls', [
uiBootstrap,
uiRouter,
uiSelect,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.acls', {
controller: 'SettingsAcls as ctrl',
url: '/acls',
resolve: {
users (xo) {
return xo.user.getAll()
},
groups (xo) {
return xo.group.getAll()
},
roles (xo) {
return xo.role.getAll()
}
},
template: view
})
})
.controller('SettingsAcls', function ($scope, users, groups, roles, xoApi, xo, selectHighLevelFilter, filterFilter) {
const refreshAcls = () => {
xo.acl.get().then(acls => {
forEach(acls, acl => acl.newRole = acl.action)
this.acls = acls
})
}
refreshAcls()
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
this.selectedTypes = {}
this.users = users
this.roles = roles
this.groups = groups
{
let usersById = this.usersById = Object.create(null)
for (let user of users) {
usersById[user.id] = user
}
let groupsById = this.groupsById = Object.create(null)
for (let group of groups) {
groupsById[group.id] = group
}
let rolesById = this.rolesById = Object.create(null)
for (let role of roles) {
rolesById[role.id] = role
}
}
this.entities = this.users.concat(this.groups)
this.objects = xoApi.all
this.getUser = (id) => {
for (let user of this.users) {
if (user.id === id) {
return user
}
}
}
this.addAcl = () => {
const promises = []
forEach(this.selectedObjects, object => promises.push(xo.acl.add(this.subject.id, object.id, this.role.id)))
this.subject = this.selectedObjects = this.role = null
Bluebird.all(promises).then(refreshAcls)
}
this.removeAcl = (subject, object, role) => {
xo.acl.remove(subject, object, role).then(refreshAcls)
}
this.editAcl = (subject, object, role, newRole) => {
console.log(subject, object, role, newRole)
xo.acl.remove(subject, object, role)
.then(xo.acl.add(subject, object, newRole))
.then(refreshAcls)
}
this.toggleType = (toggle, type) => {
const selectedObjects = this.selectedObjects && this.selectedObjects.slice() || []
if (toggle) {
const objects = filterFilter(selectHighLevelFilter(this.objects), {type})
forEach(objects, object => { selectedObjects.indexOf(object) === -1 && selectedObjects.push(object) })
this.selectedObjects = selectedObjects
} else {
const keptObjects = []
for (let index in this.selectedObjects) {
const object = this.selectedObjects[index]
if (object.type !== type) {
keptObjects.push(object)
}
}
this.selectedObjects = keptObjects
}
}
})
.filter('selectHighLevel', () => {
let isHighLevel = (object) => HIGH_LEVEL_OBJECTS[object.type]
return (objects) => filter(objects, isHighLevel)
})
.name

View File

@ -1,88 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-key(style="color: #e25440;")
| ACLs
.grid-lg
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-plus-circle
| Create
.panel-body
form(ng-submit = 'ctrl.addAcl()')
.form-group
ui-select(ng-model = 'ctrl.subject')
ui-select-match(placeholder = 'Choose a user or group')
div
span(ng-if = '$select.selected.email')
i.xo-icon-user.fa-fw
| {{$select.selected.email}}
span(ng-if = '$select.selected.name')
i.xo-icon-group.fa-fw
| {{$select.selected.name}}
ui-select-choices(repeat = 'entity in ctrl.entities | 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}}
.form-group
ui-select(ng-model = 'ctrl.selectedObjects', multiple, close-on-select = 'false', required)
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 = 'ctrl.selectedTypes[type]', ng-change = 'ctrl.toggleType(ctrl.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 = 'ctrl.selectedTypes[type]')
.form-group
ui-select(ng-model = 'ctrl.role')
ui-select-match(placeholder = 'Choose a role')
div
i(class = 'xo-icon-{{$select.selected.type | lowercase}}')
| {{$select.selected.name}}
ui-select-choices(repeat = 'role in ctrl.roles | filter:$select.search | orderBy:"name"')
div
i(class = 'xo-icon-{{role.type | lowercase}}')
| {{role.name}}
.text-center
button.btn.btn-success
i.fa.fa-plus
| Create
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-street-view
| Manage
.panel-body
table.table.table-hover
tr
th User
th Object
th Role
th
tr(ng-repeat = 'acl in ctrl.acls | orderBy:["subject", "object"] track by acl.id')
td {{ ctrl.usersById[acl.subject].email || ctrl.groupsById[acl.subject].name }}
td {{(acl.object | resolve).name_label}}
td
select.form-control(ng-options = 'role.id as role.name for role in ctrl.roles | orderBy:"name"', ng-model = 'acl.newRole', ng-change = 'ctrl.editAcl(acl.subject, acl.object, acl.action, acl.newRole)')
td
button.btn.btn-danger(ng-click = 'ctrl.removeAcl(acl.subject, acl.object, acl.action)')
i.fa.fa-trash

View File

@ -1,160 +0,0 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import uiEvent from 'angular-ui-event'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.group', [
uiRouter,
uiSelect,
uiEvent,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.group', {
controller: 'SettingsGroup as ctrl',
url: '/group/:groupId',
resolve: {
acls (xo) {
return xo.acl.get()
},
groups (xo) {
return xo.group.getAll()
},
roles (xo) {
return xo.role.getAll()
},
users (xo) {
return xo.user.getAll()
}
},
template: view
})
})
.controller('SettingsGroup', function ($scope, $state, $stateParams, $interval, acls, groups, roles, users, xoApi, xo) {
this.acls = acls
this.roles = roles
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
{
let rolesById = Object.create(null)
for (let role of roles) {
rolesById[role.id] = role
}
this.rolesById = rolesById
}
this.objects = xoApi.all
this.removals = Object.create(null)
const findGroup = groups => {
this.group = filter(groups, gr => gr.id === $stateParams.groupId).pop()
if (!this.group) {
$state.go('settings.groups')
}
}
findGroup(groups)
const refreshUsers = () => {
xo.user.getAll().then(users => {
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
})
}
const refreshGroups = () => {
if (!this.isModified()) {
xo.group.getAll().then(groups => findGroup(groups))
}
}
const refreshAcls = () => {
xo.acl.get().then(acls => {
this.acls = acls
})
}
const interval = $interval(() => {
refreshUsers()
refreshGroups()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.addUserToGroup = (group, user) => {
if (user !== null) {
group.users.push(user.id)
this.addedUser = null
this.modified = true
}
}
this.saveGroup = (group) => {
const users = []
group.users.forEach(user => {
let remove = this.removals && this.removals[user]
if (!remove) {
users.push(user)
}
})
this.removals = Object.create(null)
xo.group.setUsers(group.id, users)
.then(() => {
group.users = users
this.modified = false
})
}
this.cancelEdition = () => {
this.modified = false
this.removals = Object.create(null)
refreshGroups()
}
this.isModified = () => this.modified || Object.keys(this.removals).length
this.matchesGroup = acl => {
return acl.subject === this.group.id
}
this.removeAcl = (object, role) => {
xo.acl.remove(this.group.id, object, role).then(refreshAcls)
}
})
.filter('notInGroup', function () {
return function (users, group) {
const filtered = []
users.forEach(user => {
if (!group.users || group.users.indexOf(user.id) === -1) {
filtered.push(user)
}
})
return filtered
}
})
.filter('canAccess', () => {
return (objects, group, acls) => {
const accessed = []
const groupAcls = filter(acls, acl => acl.subject === group.id)
groupAcls.forEach(acl => {
const found = find(objects, object => object.id === acl.object)
found && accessed.push(found)
})
return accessed
}
})
.name

View File

@ -1,69 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-group(style="color: #e25440;")
| {{ ctrl.group.name }}&nbsp;
a.btn.btn-default(ui-sref = 'settings.groups')
i.fa.fa-level-up
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-street-view
| Members&nbsp;
span(ng-if = 'ctrl.isModified()') (*)
.panel-body
ul.list-group(ng-if = '!ctrl.group.users.length')
li.list-group-item.disabled: em (empty)
ul.list-group(ng-if = 'ctrl.group.users.length')
li.list-group-item(ng-repeat = 'user in ctrl.group.users')
span(ng-if = '!ctrl.removals[user]') {{ ctrl.userEmails[user] }}&nbsp;
del(ng-if = 'ctrl.removals[user]') {{ ctrl.userEmails[user] }}&nbsp;
span.pull-right
label
input.hidden(type = 'checkbox', ng-model = 'ctrl.removals[user]')
| &nbsp;
i.fa.fa-trash-o(tooltip="Remove user from group", style = 'cursor: pointer')
p
ui-select(ng-if = '(ctrl.users | notInGroup:ctrl.group).length', ng-model = 'ctrl.addedUser', on-select = 'ctrl.addUserToGroup(ctrl.group, ctrl.addedUser)')
ui-select-match(
placeholder = 'Choose a user to add'
) {{$select.selected.email}}
ui-select-choices(
repeat = 'addedUser in ctrl.users | notInGroup:ctrl.group | filter:$select.search'
) {{addedUser.email}}
em.text-muted(ng-if = '!(ctrl.users | notInGroup:ctrl.group).length') No available users to add
button.btn.btn-primary(ng-if = 'ctrl.isModified()', type="button", ng-click = 'ctrl.saveGroup(ctrl.group)')
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-default(ng-if = 'ctrl.isModified()', type="button", ng-click = 'ctrl.cancelEdition()')
i.fa.fa-times
| Cancel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-key
| ACLs&nbsp;
.panel-body
table.table.table-hover
tr
th Object
th Role
th
tr(ng-repeat = 'acl in ctrl.acls | filter:ctrl.matchesGroup track by acl.id')
td {{(acl.object | resolve).name_label}}
td {{ ctrl.rolesById[acl.action].name }}
td
button.btn.btn-danger(ng-click = 'ctrl.removeAcl(acl.object, acl.action)')
i.fa.fa-trash
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-eye
| Accessible objects&nbsp;
.panel-body
p(ng-repeat = 'object in ctrl.objects | selectHighLevel | canAccess:ctrl.group:ctrl.acls | orderBy:["type", "name_label"]')
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}})

View File

@ -1,189 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import uiEvent from 'angular-ui-event'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
import modal from './modal'
export default angular.module('settings.groups', [
uiRouter,
uiSelect,
uiEvent,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.groups', {
controller: 'SettingsGroups as ctrl',
url: '/groups',
resolve: {
users (xo) {
return xo.user.getAll()
},
groups (xo) {
return xo.group.getAll()
}
},
template: view
})
})
.controller('SettingsGroups', function ($scope, $interval, users, groups, xoApi, xo, $modal) {
this.uiCollapse = Object.create(null)
this.addedUsers = []
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
this.groups = groups
const selectedGroups = this.selectedGroups = {}
this.newGroups = []
const refreshUsers = () => {
xo.user.getAll().then(users => {
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
})
}
const refreshGroups = () => {
if (!this._editingGroup && !this.modified) {
return xo.group.getAll().then(groups => this.groups = groups)
} else {
return this.groups
}
}
const interval = $interval(() => {
refreshUsers()
refreshGroups()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.addGroup = () => {
this.newGroups.push({
// Fake (unique) id needed by Angular.JS
id: Math.random()
})
}
if (!this.groups.length) {
this.addGroup()
}
this.deleteGroup = id => {
const modalInstance = $modal.open({
template: modal,
backdrop: false
})
return modalInstance.result
.then(() => {
return xo.group.delete(id)
.then(() => {
return refreshGroups()
})
.then(groups => {
if (!groups.length) {
this.addGroup()
}
})
})
.catch(() => {})
}
this.saveGroups = () => {
const newGroups = this.newGroups
const groups = this.groups
const updateGroups = []
for (let i = 0, len = groups.length; i < len; i++) {
const group = groups[i]
const {id} = group
if (selectedGroups[id]) {
delete selectedGroups[id]
xo.group.delete(id)
} else {
xo.group.set(group)
updateGroups.push(group)
}
}
for (let i = 0, len = newGroups.length; i < len; i++) {
const group = newGroups[i]
const {name} = group
if (!name) {
continue
}
xo.group.create({name})
.then(function (id) {
group.id = id
group.users = []
})
updateGroups.push(group)
}
this.groups = updateGroups
this.newGroups.length = 0
this.modified = false
if (!this.groups.length) {
this.addGroup()
}
}
this.addUserToGroup = (group, index) => {
group.users.push(this.addedUsers[index].id)
delete this.addedUsers[index]
}
this.flagUserRemoval = (group, index, remove) => {
group.removals || (group.removals = {})
group.removals[group.users[index]] = remove
}
this.saveGroup = (group) => {
const users = []
group.users.forEach(user => {
let remove = group.removals && group.removals[user]
if (!remove) {
users.push(user)
}
})
group.removals && delete group.removals
xo.group.setUsers(group.id, users)
.then(() => {
group.users = users
this.uiCollapse[group.id] = false
})
}
this.editingGroup = (editing = undefined) => editing !== undefined && (this._editingGroup = editing) || this._editingGroup
this.cancelModifications = () => {
this.newGroups.length = 0
this.editingGroup(false)
this.modified = false
refreshGroups()
}
})
.filter('notInGroup', function () {
return function (users, group) {
const filtered = []
users.forEach(user => {
if (!group.users || group.users.indexOf(user.id) === -1) {
filtered.push(user)
}
})
return filtered
}
})
.name

View File

@ -1,12 +0,0 @@
.modal-header
button.close(
type = 'button',
ng-click = '$dismiss()'
)
span(aria-hidden = 'true') &times;
h4.modal-title Confirm group suppression
.modal-body
p Are you sure you want to delete this group ? It's user list and associated ACLs will be lost after that.
button.btn.btn-default(type = 'button', ng-click = '$close()') Ok
| &ensp;
button.btn.btn-default(type = 'button', ng-click = '$dismiss()') Cancel

View File

@ -1,49 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-group(style="color: #e25440;")
| Groups
.grid-sm
.panel.panel-default
form(ng-submit="ctrl.saveGroups()", autocomplete="off").panel-body
table.table.table-hover
tr
th.col-md-5 Name
th.col-md-5 Information
th.col-md-2
tr(ng-repeat="group in ctrl.groups | orderBy:natural('id') track by group.id")
td
input.form-control(type="text", ng-model="group.name", ui-event = '{focus: "ctrl.editingGroup(true)", blur: "ctrl.editingGroup(false)"}', ng-change = 'ctrl.modified = true')
td
span(ng-if = '!group.users.length'): em (empty)
span(ng-if = 'group.users.length')
strong {{ group.users.length }} members:&nbsp;
span(ng-repeat = 'user in group.users | limitTo:4')
| {{ ctrl.userEmails[user] }}{{ $last ? (group.users.length > 4 ? ',...' : '') : ', ' }}
| &nbsp;
td
a.btn.btn-primary(ui-sref = 'settings.group({groupId: group.id})')
| Edit&nbsp;
i.fa.fa-pencil
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.deleteGroup(group.id)')
i.fa.fa-trash
tr(ng-repeat="group in ctrl.newGroups")
td
input.form-control(type = "text", ng-model = "group.name", placeholder = "New group name", ng-change = 'ctrl.modified = true')
td
button.btn.btn-btn-default(type = 'button', ng-click = 'ctrl.newGroups.splice($index, 1)')
i.fa.fa-times
td &#160;
p
button.btn.btn-success(type="button", ng-click="ctrl.addGroup()")
i.fa.fa-plus
| &nbsp;
span(ng-if = 'ctrl.modified')
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-default(type="button", ng-click = "ctrl.cancelModifications()")
i.fa.fa-times
| Cancel

View File

@ -1,45 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import acls from './acls'
import group from './group'
import groups from './groups'
import plugins from './plugins'
import servers from './servers'
import update from './update'
import user from './user'
import users from './users'
import view from './view'
export default angular.module('settings', [
uiRouter,
acls,
group,
groups,
plugins,
servers,
update,
user,
users
])
.config(function ($stateProvider) {
$stateProvider.state('settings', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/settings'
})
// Redirect to default sub-state.
$stateProvider.state('settings.index', {
url: '',
controller: function ($state) {
$state.go('settings.servers')
}
})
})
.name

View File

@ -1,219 +0,0 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import marked from 'marked'
import trim from 'lodash.trim'
import uiRouter from 'angular-ui-router'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
import multiStringView from './multi-string-view'
import objectInputView from './object-input-view'
function isRequired (key, schema) {
return find(schema.required, item => item === key) || false
}
function isPassword (key) {
return key.search(/password|secret/i) !== -1
}
function loadDefaults (schema, configuration) {
if (!schema || !configuration) {
return
}
forEach(schema.properties, (item, key) => {
if (item.type === 'boolean' && !(key in configuration)) { // String default values are used as placeholders in view
configuration[key] = item && item.default
}
})
}
function cleanUpConfiguration (schema, configuration) {
if (!schema || !configuration) {
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(configuration, (item, key) => {
item = sanitizeItem(item)
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)
}
})
}
export default angular.module('settings.plugins', [
uiRouter,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.plugins', {
controller: 'SettingsPlugins as ctrl',
url: '/plugins',
data: {
requireAdmin: true
},
resolve: {
},
template: view
})
})
.controller('SettingsPlugins', function (xo, notify) {
this.disabled = {}
const refreshPlugins = () => xo.plugin.get().then(plugins => {
forEach(plugins, plugin => {
plugin._loaded = plugin.loaded
plugin._autoload = plugin.autoload
if (!plugin.configuration) {
plugin.configuration = {}
}
loadDefaults(plugin.configurationSchema, plugin.configuration)
})
this.plugins = plugins
})
refreshPlugins()
const _execPluginMethod = (id, method, ...args) => {
this.disabled[id] = true
return xo.plugin[method](...args)
.finally(() => {
return refreshPlugins()
.then(() => this.disabled[id] = false)
})
}
this.isRequired = isRequired
this.isPassword = isPassword
this.configure = (plugin) => {
cleanUpConfiguration(plugin.configurationSchema, plugin.configuration)
_execPluginMethod(plugin.id, 'configure', plugin.id, plugin.configuration)
.then(() => notify.info({
title: 'Plugin configuration',
message: 'Successfully saved'
}))
}
this.toggleAutoload = (plugin) => {
let method
if (!plugin._autoload && plugin.autoload) {
method = 'disableAutoload'
} else if (plugin._autoload && !plugin.autoload) {
method = 'enableAutoload'
}
if (method) {
_execPluginMethod(plugin.id, method, plugin.id)
}
}
this.toggleLoad = (plugin) => {
let method
if (!plugin._loaded && plugin.loaded && plugin.unloadable !== false) {
method = 'unload'
} else if (plugin._loaded && !plugin.loaded) {
method = 'load'
}
if (method) {
_execPluginMethod(plugin.id, method, plugin.id)
}
}
})
.directive('multiStringInput', () => {
return {
restrict: 'E',
template: multiStringView,
scope: {
model: '='
},
controller: 'MultiString as ctrl',
bindToController: true
}
})
.controller('MultiString', function ($scope, xo, xoApi) {
if (this.model === undefined || this.model === null) {
this.model = []
}
if (!Array.isArray(this.model)) {
throw new Error('multiString directive model must be an array')
}
this.add = (string) => {
string = trim(string)
if (string === '') {
return
}
this.model.push(string)
}
this.remove = (index) => {
this.model.splice(index, 1)
}
})
.directive('objectInput', () => {
return {
restrict: 'E',
template: objectInputView,
scope: {
model: '=',
schema: '=',
required: '='
},
controller: 'ObjectInput as ctrl',
bindToController: true
}
})
.controller('ObjectInput', function ($scope, xo, xoApi) {
const prepareModel = () => {
if (this.model === undefined || this.model === null) {
this.model = {
__use: this.required
}
} else {
if (typeof this.model !== 'object' || Array.isArray(this.model)) {
throw new Error('objectInput directive model must be a plain object')
}
if (!('__use' in this.model)) {
this.model.__use = true
}
}
loadDefaults(this.schema, this.model)
}
prepareModel()
$scope.$watch(() => this.model, prepareModel)
this.isRequired = isRequired
this.isPassword = isPassword
})
.filter('md2html', function ($sce) {
return function (input) {
return $sce.trustAsHtml(marked(input || ''))
}
})
.name

View File

@ -1,10 +0,0 @@
ul(style = 'padding-left: 0;')
li.list-group-item.clearfix(ng-repeat = 'item in ctrl.model track by $index')
| {{item}}
button.btn.btn-default.btn-sm.pull-right(type = 'button', ng-click = 'ctrl.remove($index)')
i.fa.fa-times
form(ng-submit = 'ctrl.add(newItem); newItem = ""')
.input-group
input.form-control.input-sm(type = 'text', ng-model = 'newItem')
span.input-group-btn
button.btn.btn-primary.btn-sm(type = 'submit') Add

View File

@ -1,14 +0,0 @@
.checkbox(ng-if = '!ctrl.required')
label
input(type = 'checkbox', ng-model = 'ctrl.model.__use')
| &nbsp;Configure (optional)
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
span.input-group-addon
| {{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)')
.help-block(ng-bind-html = 'ctrl.schema.properties[key].description | md2html')

View File

@ -1,48 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-plugin(style="color: #e25440;")
| Plugins
.grid-sm
.panel.panel-default
.panel-body
p.text-center(ng-if = '!ctrl.plugins || !crtl.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 }}&nbsp;
.checkbox.small
label
i.fa.fa-2x(ng-class = '{"fa-toggle-on": plugin.loaded, "fa-toggle-off": !plugin.loaded, "text-success": plugin.loaded}')
span(ng-if = 'plugin.loaded && plugin.unloadable === false')
| &nbsp;
i.fa.fa-2x.fa-lock(tooltip = 'This plugin cannot be unloaded without a server restart')
input.hidden(type = 'checkbox', ng-model = 'plugin._loaded', ng-change = 'ctrl.toggleLoad(plugin)', ng-disabled = 'plugin.unloadable === false && plugin.loaded || ctrl.disabled[plugin.id]')
| &nbsp;
.checkbox.small
label
| Auto-load at server start&nbsp;
input(type = 'checkbox', ng-model = 'plugin._autoload', ng-change = 'ctrl.toggleAutoload(plugin)', ng-disabled = 'ctrl.disabled[plugin.id]')
.form-group.pull-right.small
button.btn.btn-default(type = 'button', ng-click = 'isExpanded = !isExpanded'): i.fa(ng-class = '{"fa-plus": !isExpanded, "fa-minus": isExpanded}')
hr
div(collapse = '!isExpanded')
p(ng-if = '!plugin.configurationSchema') This plugin has no specific configuration
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}}
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)')
.col-md-5
span.help-block(ng-bind-html = 'prop.description | md2html')
.form-group
.col-md-offset-2.col-md-10
button.btn.btn-primary(type = 'submit')
| Save configuration&nbsp;
i.fa.fa-floppy-o

View File

@ -1,123 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.servers', [
uiRouter,
uiSelect,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.servers', {
controller: 'SettingsServers as ctrl',
url: '/servers',
resolve: {
servers (xo) {
return xo.server.getAll()
}
},
template: view
})
})
.controller('SettingsServers', function ($scope, $interval, servers, xoApi, xo, notify) {
this.servers = servers
const selected = this.selectedServers = {}
const newServers = this.newServers = []
const refreshServers = () => {
xo.server.getAll().then(servers => {
this.servers = servers
})
}
const interval = $interval(refreshServers, 10e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.connectServer = (id) => {
notify.info({
title: 'Server connect',
message: 'Connecting the server...'
})
xo.server.connect(id).catch(error => {
notify.error({
title: 'Server connection error',
message: error.message
})
})
}
this.disconnectServer = (id) => {
notify.info({
title: 'Server disconnect',
message: 'Disconnecting the server...'
})
xo.server.disconnect(id)
}
this.addServer = () => {
newServers.push({
// Fake (unique) id needed by Angular.JS
id: Math.random(),
status: 'connecting'
})
}
this.addServer()
this.saveServers = () => {
const newServers = this.newServers
const servers = this.servers
const updateServers = []
for (let i = 0, len = servers.length; i < len; i++) {
const server = servers[i]
const {id} = server
if (selected[id]) {
delete selected[id]
xo.server.remove(id)
} else {
if (!server.password) {
delete server.password
}
xo.server.set(server)
delete server.password
updateServers.push(server)
}
}
for (let i = 0, len = newServers.length; i < len; i++) {
const server = newServers[i]
const {host, username, password} = server
if (!host) {
continue
}
xo.server.add({
host,
username,
password,
autoConnect: false
}).then(function (id) {
server.id = id
xo.server.connect(id).catch(error => {
notify.error({
title: 'Server connection error',
message: error.message
})
})
})
delete server.password
updateServers.push(server)
}
this.servers = updateServers
this.newServers.length = 0
this.addServer()
}
})
.name

View File

@ -1,77 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-cloud(style="color: #e25440;")
| Servers
.grid-sm
.panel.panel-default
form(ng-submit="ctrl.saveServers()", autocomplete="off").panel-body
table.table.table-hover
tr
th.col-md-5 Host
th.col-md-2 User
th.col-md-3 Password
th.col-md-1.text.center Actions
th.col-md-1.text-center
i.fa.fa-trash-o.fa-lg(tooltip="Forget server")
tr(ng-repeat="server in ctrl.servers | orderBy:natural('host') track by server.id")
td
.input-group
span.input-group-addon.hidden-xs(ng-if="server.status === 'connected'")
i.xo-icon-success.fa-lg(tooltip="Connected")
span.input-group-addon.hidden-xs(ng-if="server.status === 'disconnected'")
i.xo-icon-failure.fa-lg(tooltip="Disconnected")
span.input-group-addon.hidden-xs(ng-if="server.status === 'connecting'")
i.fa.fa-cog.fa-lg.fa-spin(tooltip="Connecting...")
input.form-control(type="text", ng-model="server.host")
td
input.form-control(type="text", ng-model="server.username")
td
input.form-control(type="password", ng-model="server.password", placeholder="Fill to change the password")
td.text-center
button.btn.btn-default(
ng-if="server.status === 'disconnected'",
type="button",
ng-click="ctrl.connectServer(server.id)",
tooltip="Reconnect this server"
)
i.fa.fa-link
button.btn.btn-danger(
ng-if="server.status === 'connected'",
type="button",
ng-click="ctrl.disconnectServer(server.id)"
tooltip="Disconnect this server"
)
i.fa.fa-unlink
td.text-center
input(type="checkbox", ng-model="ctrl.selectedServers[server.id]")
tr(ng-repeat="server in ctrl.newServers")
td
input.form-control(
type = "text"
ng-model = "server.host"
placeholder = "address[:port]"
)
td
input.form-control(
type = "text"
ng-model = "server.username"
ng-required = "server.host"
placeholder = "user"
)
td
input.form-control(
type="password"
ng-model="server.password"
ng-required = "server.host"
placeholder="password"
)
td &#160;
td &#160;
p.text-center
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-success(type="button", ng-click="ctrl.addServer()")
i.fa.fa-plus

View File

@ -1,99 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import _assign from 'lodash.assign'
import ansiUp from 'ansi_up'
import updater from '../../updater'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import {AuthenticationFailed} from '../../updater'
import view from './view'
export default angular.module('settings.update', [
uiRouter,
updater,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.update', {
controller: 'SettingsUpdate as ctrl',
url: '/update',
onExit: updater => {
updater.removeAllListeners('end')
},
template: view
})
})
.filter('ansitohtml', function ($sce) {
return function (input) {
return $sce.trustAsHtml(ansiUp.ansi_to_html(input))
}
})
.controller('SettingsUpdate', function (xoApi, xo, updater, notify) {
this.updater = updater
this.updater.isRegistered()
.then(() => this.updater.on('end', () => this.updater.isRegistered()))
.catch(err => console.error(err))
this.updater.getConfiguration()
.then(configuration => this.configuration = _assign({}, configuration))
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
this.registerXoa = (email, password, renewRegister) => {
this.regPwd = ''
this.updater.register(email, password, renewRegister)
.tap(() => this.renewRegister = false)
.then(() => this.updater.update())
.catch(AuthenticationFailed, () => {})
.catch(err => console.error(err))
}
this.update = () => {
this.updater.update()
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
}
this.upgrade = () => {
this.updater.upgrade()
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
}
this.configure = (host, port, username, password) => {
const config = {}
if (!this.withAuth) {
username = null
password = null
}
config.proxyHost = host && host.trim() || null
config.proxyPort = port && port.trim() || null
config.proxyUser = username || null
config.proxyPassword = password || null
return this.updater.configure(config)
.then(configuration => this.configuration = _assign({}, configuration))
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
.finally(() => this.update())
}
this.valid = trial => {
return trial && trial.end && Date.now() < trial.end
}
})
.name

View File

@ -1,128 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-refresh(style="color: #e25440;")
| Update
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-globe
| Status
.panel-body
p(ng-if = '!ctrl.updater.state')
a.btn.btn-warning: i.fa.fa-question-circle(ng-if = '!ctrl.updater.state', tooltip = 'No update information available')
| &nbsp;No update information available&nbsp;
a.btn.btn-default(ng-class = '{disabled: ctrl.updater.isConnected}', ng-click = 'ctrl.update()')
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.isConnected}')
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state === "registerNeeded"')
a.btn.btn-warning(ng-if = 'ctrl.updater.state === "registerNeeded"'): i.fa.fa-bell-slash(tooltip = 'Your XOA is not registered for updates')
| &nbsp;Registration needed&nbsp;
button.btn.btn-default(ng-if = 'ctrl.updater.registerState === "registered"', ng-click = 'ctrl.updater.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}'): i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating || ctrl.updater.upgrading}')
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state !== "registerNeeded"')
a.btn.btn-info(ng-if = 'ctrl.updater.state === "connected"'): i.fa.fa-question-circle(tooltip = 'Update information may be available')
a.btn.btn-success(ng-if = 'ctrl.updater.state === "upToDate"'): i.fa.fa-check(tooltip = 'Your XOA is up-to-date')
a.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"'): i.fa.fa-bell(tooltip = 'You need to update your XOA (new version is available)')
a.btn.btn-danger(ng-if = 'ctrl.updater.state === "error"'): i.fa.fa-exclamation-triangle(tooltip = 'Can\'t fetch update information')
| &nbsp;
button#update.btn.btn-info(type = 'button', ng-click = 'ctrl.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
| Check for updates&nbsp;
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating}')
| &nbsp;
button#upgrade.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"', type = 'button', ng-click = 'ctrl.upgrade()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
| Upgrade&nbsp;
i.fa.fa-cog(ng-class = '{"fa-spin": ctrl.updater.upgrading}')
div
p(ng-repeat = 'entry in ctrl.updater._log')
span(ng-class = '{"text-danger": entry.level === "error", "text-muted": entry.level === "info", "text-warning": entry.level === "warning", "text-success": entry.level === "success"}') {{ entry.date }}
| &nbsp;:&nbsp;
span(style = 'word-wrap: break-word;', ng-bind-html = 'entry.message | ansitohtml')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-pencil
| Registration
.panel-body.text-center
.text-warning(ng-if = '!ctrl.updater.state || ctrl.updater.registerState === "unknown"')
| No registration information available.
br
span.big-stat
i.fa.fa-exclamation-triangle.text-warning
div(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "error"')
.text-danger Can't fetch registration information.
br
span.big-stat
i.fa.fa-exclamation-triangle.text-danger
br
.text-danger {{ ctrl.updater.registerError }}
br
button.btn.btn-default(type = 'button', ng-click = 'ctrl.updater.isRegistered()')
i.fa.fa-refresh
| Refresh
form(ng-if = 'ctrl.updater.state && (ctrl.renewRegister || ctrl.updater.registerState === "unregistered")', ng-submit = 'ctrl.registerXoa(ctrl.regEmail, ctrl.regPwd, ctrl.renewRegister)')
p.form-static-control(ng-if = '!ctrl.renewRegister') XOA is not registered.
p.form-static-control(ng-if = 'ctrl.renewRegister')
| Forget previous registration ?&nbsp;
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = false') Cancel
p.small Your xen-orchestra.com email and password
.form-group
.input-group
span.input-group-addon: i.fa.fa-envelope-o.fa-fw
label.sr-only(for = 'regEmail') Email
input#regEmail.form-control(type = 'email', placeholder = 'Email', ng-model = 'ctrl.regEmail', required)
.form-group
.input-group
span.input-group-addon: i.fa.fa-key.fa-fw
label.sr-only(for = 'regPwd') Email
input#regPwd.form-control(type = 'password', placeholder = 'Password', ng-model = 'ctrl.regPwd', required)
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-check
| Register
p.form-static-control.text-danger {{ ctrl.updater.registerError }}
p(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "registered" && !ctrl.renewRegister')
| Your Xen Orchestra appliance is registered to
span.text-success {{ ctrl.updater.token.registrationEmail }}
| .
br
br
i.fa.fa-check-circle.fa-3x.text-success
br
br
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = true') Register to someone else ?
.grid-sm(ng-if = 'ctrl.updater.state && ctrl.configuration')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| Settings
.panel-body
form(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort, ctrl.configuration.proxyUser, ctrl.configuration.proxyPassword)')
h4
i.fa.fa-globe
| Proxy settings
p
| If you need a proxy to access the Internet&ensp;
label
input(type = 'checkbox', ng-model = 'ctrl.withAuth')
| with authentication
fieldset.form-inline
.form-group
//- label.control-label Host:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyHost', placeholder = 'Host (myproxy.example.org)')
| &nbsp;
.form-group
//- label.control-label Port:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyPort', placeholder = 'Port (3128 ?...)')
br
div(ng-hide = '!ctrl.withAuth')
fieldset.form-inline(ng-disabled = '!ctrl.withAuth')
.form-group
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyUser', placeholder = 'User name', required)
| &nbsp;
.form-group
input.form-control(type = 'password', ng-model = 'ctrl.configuration.proxyPassword', placeholder = 'Password')
br
fieldset
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-floppy-o
| Save

View File

@ -1,48 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.user', [
uiRouter,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.user', {
controller: 'SettingsUser as ctrl',
url: '/user',
data: {
requireAdmin: false
},
resolve: {
},
template: view
})
})
.controller('SettingsUser', function (xo, notify) {
this.changePassword = function (oldPassword, newPassword) {
this.working = true
xo.user.changePassword(oldPassword, newPassword)
.then(() => {
this.oldPassword = ''
this.newPassword = ''
this.confirmPassword = ''
notify.info({
title: 'Change password',
message: 'Password has been successfully change'
})
})
.catch(error => notify.error({
title: 'Change password',
message: error.message
}))
.finally(() => this.working = false)
}
})
.name

View File

@ -1,21 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-user(style="color: #e25440;")
| Profile
.grid-sm
.panel.panel-default
.panel-body
.row
.col-sm-6
form(ng-submit = 'ctrl.changePassword(ctrl.oldPassword, ctrl.newPassword)')
fieldset(ng-disabled = 'ctrl.working')
legend Change password
.form-group
input.form-control(type = 'password', ng-model = 'ctrl.oldPassword', placeholder = 'Current password', required)
.form-group
input.form-control(type = 'password', ng-model = 'ctrl.newPassword', placeholder = 'New password', required)
.form-group(ng-class = '{"has-error": ctrl.confirmPassword && ctrl.newPassword && (ctrl.confirmPassword !== ctrl.newPassword)}')
input.form-control(type = 'password', ng-model = 'ctrl.confirmPassword', placeholder = 'Confirm password', required)
.form-group
button.btn.btn-primary(type = 'submit', ng-disabled = '!ctrl.oldPassword || !ctrl.newPassword || ctrl.newPassword !== ctrl.confirmPassword') Save password

View File

@ -1,132 +0,0 @@
import angular from 'angular'
import passwordGenerator from 'password-generator'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import uiEvent from 'angular-ui-event'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.users', [
uiRouter,
uiSelect,
uiEvent,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.users', {
controller: 'SettingsUsers as ctrl',
url: '/users',
resolve: {
users (xo) {
return xo.user.getAll()
}
},
template: view
})
})
.controller('SettingsUsers', function ($scope, $interval, users, xoApi, xo) {
this.users = users
this.permissions = [
{
label: 'User',
value: 'none'
},
{
label: 'Admin',
value: 'admin'
}
]
const selected = this.selectedUsers = {}
this.newUsers = []
const refreshUsers = () => {
if (!this._editingUser) {
xo.user.getAll().then(users => {
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
})
}
}
const interval = $interval(() => {
refreshUsers()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.addUser = () => {
this.newUsers.push({
// Fake (unique) id needed by Angular.JS
id: Math.random(),
permission: 'none'
})
}
this.addUser()
this.saveUsers = () => {
const newUsers = this.newUsers
const users = this.users
const updateUsers = []
for (let i = 0, len = users.length; i < len; i++) {
const user = users[i]
const {id} = user
if (selected[id]) {
delete selected[id]
xo.user.delete(id)
} else {
if (!user.password) {
delete user.password
}
xo.user.set(user)
delete user.password
updateUsers.push(user)
}
}
for (let i = 0, len = newUsers.length; i < len; i++) {
const user = newUsers[i]
const {email, permission, password} = user
if (!email) {
continue
}
xo.user.create({
email,
permission,
password
}).then(function (id) {
user.id = id
})
delete user.password
updateUsers.push(user)
}
this.users = updateUsers
this.newUsers.length = 0
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
this.addUser()
}
this.editingUser = editing => {
this._editingUser = editing
}
this.generatePassword = (user) => {
// Generate password of 8 letters/numbers/underscore
user.password = passwordGenerator(8, false)
}
})
.name

View File

@ -1,111 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-user(style="color: #e25440;")
| Users
.grid-sm
.panel.panel-default
.panel-body
form(ng-submit="ctrl.saveUsers()", autocomplete="off")
table.table.table-hover
tr
th.col-md-4 Email
th.col-md-4 Permissions
th.col-md-3 Password
th.col-md-1.text-center
i.fa.fa-trash-o.fa-lg(tooltip="Remove user")
tr(ng-repeat="user in ctrl.users | orderBy:natural('id') track by user.id")
td
input.form-control(
type="text"
ng-model="user.email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
select.form-control(
ng-options="p.value as p.label for p in ctrl.permissions"
ng-model="user.permission"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
div.input-group
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Generate random password"
ng-click = "ctrl.generatePassword(user); showPassword = true"
)
i.fa.fa-key
input.form-control(
type = "{{ showPassword ? 'text' : 'password' }}"
ng-model = "user.password"
placeholder = "Fill to change the password"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Reveal password"
ng-show = "user.password.length > 0"
ng-mousedown = "showPassword = true"
ng-mouseup = "showPassword = false"
ng-mouseleave = "showPassword = false"
)
i.fa.fa-eye(ng-if = "showPassword")
i.fa.fa-eye-slash(ng-if = "!showPassword")
td.text-center
input(
type="checkbox"
ng-model="ctrl.selectedUsers[user.id]"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
tr(ng-repeat="user in ctrl.newUsers")
td
input.form-control(
type = "text"
ng-model = "user.email"
placeholder = "email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
select.form-control(
ng-options = "p.value as p.label for p in ctrl.permissions"
ng-model = "user.permission"
ng-required = "user.email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
div.input-group
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Generate random password"
ng-click = "ctrl.generatePassword(user); showPassword = true"
)
i.fa.fa-key
input.form-control(
type = "{{ showPassword ? 'text' : 'password' }}"
ng-model = "user.password"
ng-required = "user.email"
placeholder = "password"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Reveal password"
ng-show = "user.password.length > 0"
ng-mousedown = "showPassword = true"
ng-mouseup = "showPassword = false"
ng-mouseleave = "showPassword = false"
)
i.fa.fa-eye(ng-if = "showPassword")
i.fa.fa-eye-slash(ng-if = "!showPassword")
td &#160;
p.text-center
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-success(type="button", ng-click="ctrl.addUser()")
i.fa.fa-plus

View File

@ -1,28 +0,0 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.servers', ui-sref-active = 'active')
i.fa.fa-fw.fa-cloud.fa-menu
span.menu-entry Servers
li
a(ui-sref = '.users')
i.xo-icon-user.fa-fw.fa-menu
span.menu-entry Users
li
a(ui-sref = '.groups')
i.xo-icon-group.fa-fw.fa-menu
span.menu-entry Groups
li
a(ui-sref = '.acls')
i.fa.fa-fw.fa-key.fa-menu
span.menu-entry ACLs
li
a(ui-sref = '.plugins')
i.xo-icon-plugin.fa-fw.fa-menu
span.menu-entry Plugins
li
a(ui-sref = '.update')
i.fa.fa-fw.fa-refresh.fa-menu
span.menu-entry Update
.side-content(ui-view = '')

View File

@ -1,213 +0,0 @@
import angular from 'angular'
import escapeRegExp from 'lodash.escaperegexp'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import isEmpty from 'lodash.isempty'
import trim from 'lodash.trim'
import uiRouter from 'angular-ui-router'
import Bluebird from 'bluebird'
import xoTag from 'tag'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.sr', [
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('SRs_view', {
url: '/srs/:id',
controller: 'SrCtrl',
template: view
})
})
.filter('vdiFilter', (xoApi, filterFilter) => {
return (input, search) => {
search && (search = trim(search).toLowerCase())
return filter(input, vdi => {
let vbd, vm
let vmName = vdi.$VBDs && vdi.$VBDs[0] && (vbd = xoApi.get(vdi.$VBDs[0])) && (vm = xoApi.get(vbd.VM)) && vm.name_label
vmName && (vmName = vmName.toLowerCase())
return !search || (vmName && (vmName.search(escapeRegExp(search)) !== -1) || filterFilter([vdi], search).length)
})
}
})
.controller('SrCtrl', function ($scope, $stateParams, $state, $q, notify, xoApi, xo, modal, $window, bytesToSizeFilter) {
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
$scope.currentLogPage = 1
$scope.currentVDIPage = 1
let {get} = xoApi
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
$scope.SR = SR
})
$scope.saveSR = function ($data) {
let {SR} = $scope
let {name_label, name_description} = $data
$data = {
id: SR.id
}
if (name_label !== SR.name_label) {
$data.name_label = name_label
}
if (name_description !== SR.name_description) {
$data.name_description = name_description
}
return xoApi.call('sr.set', $data)
}
$scope.deleteVDI = function (id) {
console.log('Delete VDI', id)
return modal.confirm({
title: 'VDI deletion',
message: 'Are you sure you want to delete this VDI? This operation is irreversible.'
}).then(function () {
return xo.vdi.delete(id)
})
}
$scope.disconnectVBD = function (id) {
console.log('Disconnect VBD', id)
return modal.confirm({
title: 'VDI disconnection',
message: 'Are you sure you want to disconnect this VDI?'
}).then(function () {
return xoApi.call('vbd.disconnect', {id: id})
})
}
$scope.connectPBD = function (id) {
console.log('Connect PBD', id)
return xoApi.call('pbd.connect', {id: id})
}
$scope.disconnectPBD = function (id) {
console.log('Disconnect PBD', id)
return xoApi.call('pbd.disconnect', {id: id})
}
$scope.reconnectAllHosts = function () {
// TODO: return a Bluebird.all(promises).
for (let id of $scope.SR.$PBDs) {
let pbd = xoApi.get(id)
xoApi.call('pbd.connect', {id: pbd.id})
}
}
$scope.disconnectAllHosts = function () {
return modal.confirm({
title: 'Disconnect hosts',
message: 'Are you sure you want to disconnect all hosts to this SR?'
}).then(function () {
for (let id of $scope.SR.$PBDs) {
let pbd = xoApi.get(id)
xoApi.call('pbd.disconnect', {id: pbd.id})
console.log(pbd.id)
}
})
}
$scope.rescanSr = function (id) {
console.log('Rescan SR', id)
return xoApi.call('sr.scan', {id: id})
}
$scope.removeSR = function (id) {
console.log('Remove SR', id)
return modal.confirm({
title: 'SR deletion',
message: 'Are you sure you want to delete this SR? This operation is irreversible.'
}).then(function () {
return Bluebird.map($scope.SR.$PBDs, pbdId => {
let pbd = xoApi.get(pbdId)
return xoApi.call('pbd.disconnect', { id: pbd.id })
})
}).then(function () {
return xoApi.call('sr.destroy', {id: id})
}).then(function () {
$state.go('index')
notify.info({
title: 'SR remove',
message: 'SR is removed'
})
})
}
$scope.forgetSR = function (id) {
console.log('Forget SR', id)
return modal.confirm({
title: 'SR forget',
message: 'Are you sure you want to forget this SR? No VDI on this SR will be removed.'
}).then(function () {
return Bluebird.map($scope.SR.$PBDs, pbdId => {
let pbd = xoApi.get(pbdId)
return xoApi.call('pbd.disconnect', { id: pbd.id })
})
}).then(function () {
return xoApi.call('sr.forget', {id: id})
}).then(function () {
$state.go('index')
notify.info({
title: 'SR forget',
message: 'SR is forgotten'
})
})
}
$scope.saveDisks = function (data) {
// Group data by disk.
let disks = {}
forEach(data, function (value, key) {
let i = key.indexOf('/')
let id = key.slice(0, i)
let prop = key.slice(i + 1)
;(disks[id] || (disks[id] = {}))[prop] = value
})
let promises = []
forEach(disks, function (attributes, id) {
// Keep only changed attributes.
let disk = get(id)
forEach(attributes, function (value, name) {
if (value === disk[name]) {
delete attributes[name]
}
})
if (!isEmpty(attributes)) {
// Inject id.
attributes.id = id
// Ask the server to update the object.
promises.push(xoApi.call('vdi.set', attributes))
}
})
return $q.all(promises)
}
})
// A module exports its name.
.name

View File

@ -1,215 +0,0 @@
.grid
.panel.panel-default
p.page-title
i.xo-icon-sr
| {{SR.name_label}}
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="srSettings.$show()")
i.fa.fa-edit.fa-fw
.panel-body
form(editable-form="", name="srSettings", onbeforesave="saveSR($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="SR.name_label", e-name="name_label", e-form="srSettings")
| {{SR.name_label}}
dt Description
dd
span(editable-text="SR.name_description", e-name="name_description", e-form="srSettings")
| {{SR.name_description}}
dt Content type:
dd {{SR.SR_type}}
dt Tags
dd
xo-tag(ng-if = 'SR', object = 'SR')
dt Shared
div(ng-repeat="container in [SR.$container] | resolve")
dd(ng-if="'pool' === container.type")
| Yes (
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
| )
dd(ng-if="'host' === container.type") No
dt Size
dd {{SR.size | bytesToSize}}
dt UUID
dd {{SR.UUID}}
.btn-form(ng-show="srSettings.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="srSettings.$waiting", ng-click="srSettings.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="srSettings.$waiting")
i.fa.fa-save
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats
| Stats
.panel-body
.row
.col-sm-6.col-lg-4
p.stat-name Physical Alloc:
canvas.stat-simple(id="doughnut", class="chart chart-doughnut", data="[(SR.physical_usage), (SR.size - SR.physical_usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
.col-sm-6.col-lg-4
p.stat-name Virtual Alloc:
canvas.stat-simple(id="doughnut", class="chart chart-doughnut", data="[(SR.usage), (SR.size - SR.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
.col-sm-4.visible-lg
p.stat-name VDIs:
p.center.big-stat {{SR.VDIs.length}}
.row.hidden-lg
.col-sm-12
br
p.stat-name {{SR.VDIs.length}} VDIs
//- Action panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body.text-center
.grid
.grid-cell.btn-group
button.btn(tooltip="Rescan all the VDI", tooltip-placement="top", type="button", style="width: 90%", ng-click="rescanSr(SR.id)")
i.fa.fa-refresh.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Reconnect all hosts", tooltip-placement="top", type="button", style="width: 90%", ng-click="reconnectAllHosts()")
i.fa.fa-retweet.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Disconnect all hosts", tooltip-placement="top", type="button", style="width: 90%", xo-click="disconnectAllHosts()")
i.fa.fa-power-off.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Forget SR", tooltip-placement="top", type="button", style="width: 90%", xo-click="forgetSR(SR.id)")
i.fa.fa-2x.fa-fw.fa-ban
.grid-cell.btn-group
button.btn(tooltip="Remove SR", tooltip-placement="top", type="button", style="width: 90%", xo-click="removeSR(SR.id)")
i.fa.fa-2x.fa-trash-o
//- TODO: Space panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| VDI Map
.panel-body
.progress
.progress-bar.progress-bar-vm(ng-if="((VDI.size/SR.size)*100) > 0.5", ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VDI.size}}", aria-valuemax="{{SR.size}}", style="width: {{[VDI.size, SR.size] | percentage}}", tooltip="{{VDI.name_label}} ({{[VDI.size, SR.size] | percentage}})")
//- display the name only if it fits in its progress bar
span(ng-if="VDI.name_label.length < ((VDI.size/SR.size)*100)") {{VDI.name_label}}
ul.list-inline.text-center
li Total: {{SR.size | bytesToSize}}
li Currently used: {{SR.usage | bytesToSize}}
li Available: {{SR.size-SR.usage | bytesToSize}}
//- TODO: VDIs.
.grid
form(name = "disksForm" editable-form = '' onbeforesave = 'saveDisks($data)').panel.panel-default
.panel-heading.panel-title
i.xo-icon-disk
| Virtual disks
span.quick-edit(tooltip="Edit disks", ng-click="disksForm.$show()")
i.fa.fa-edit.fa-fw
span.quick-edit(tooltip="Rescan", ng-click="rescanSr(SR.id)")
i.fa.fa-refresh.fa-fw
.panel-body
table.table.table-hover
tr
th Name
th Description
th Tags
th Size
th Virtual Machine:
tr(ng-repeat="VDI in SR.VDIs | resolve | vdiFilter:vdiSearch | orderBy:natural('name_label') | slice:(10*(currentVDIPage-1)):(10*currentVDIPage)")
td.oneliner
span(
editable-text="VDI.name_label"
e-name = '{{VDI.id}}/name_label'
)
| {{VDI.name_label}} &nbsp;
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
td.oneliner
span(
editable-text="VDI.name_description"
e-name = '{{VDI.id}}/name_description'
)
| {{VDI.name_description}}
td
xo-tag(object = 'VDI')
td
//- FIXME: should be editable, but the server needs first
//- to accept a human readable string.
| {{VDI.size | bytesToSize}}
td.oneliner {{((VDI.$VBDs[0] | resolve).VM | resolve).name_label}}
span.pull-right.btn-group.quick-buttons
a(ng-if="(VDI.$VBDs[0] | resolve).attached", xo-click="disconnectVBD(VDI.$VBDs[0])")
i.fa.fa-unlink.fa-lg(tooltip="Disconnect this disk")
a(ng-if="!(VDI.$VBDs[0] | resolve).attached", xo-click="deleteVDI(VDI.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this disk")
//- TODO: Ability to create new VDIs.
.form-inline
.input-group
.input-group-addon: i.fa.fa-filter
input.form-control(type = 'text', ng-model = 'vdiSearch', placeholder = 'Enter your search here')
.center(ng-if = '(SR.VDIs | resolve | vdiFilter:vdiSearch).length > 10 || currentVDIPage > 1')
pagination(boundary-links="true", total-items="(SR.VDIs | resolve | vdiFilter:vdiSearch).length", ng-model="$parent.currentVDIPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.btn-form(ng-show="disksForm.$visible")
p.center
button.btn.btn-default(
type="reset"
ng-disabled="disksForm.$waiting"
ng-click="disksForm.$cancel()"
)
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(
type="submit"
ng-disabled="disksForm.$waiting"
)
i.fa.fa-save
| Save
//- /VDIs.
//- Hosts.
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-link
| Connected hosts
span.quick-edit(tooltip="Reconnect all hosts", ng-click="reconnectAllHosts()")
i.fa.fa-plus-square.fa-fw
.panel-body
table.table.table-hover
th Name
th Status
tr(ng-repeat="PBD in SR.$PBDs | resolve", xo-sref="hosts_view({id: (PBD.host | resolve).id})")
td {{(PBD.host | resolve).name_label}}
td(ng-if="PBD.attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(xo-click="disconnectPBD(PBD.id)")
i.fa.fa-unlink.fa-lg(tooltip="Disconnect to this host")
td(ng-if="!PBD.attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(xo-click="connectPBD(PBD.id)")
i.fa.fa-link.fa-lg(tooltip="Reconnect to this host")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
.panel-body
p.center(ng-if="SR.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="SR.messages | isNotEmpty")
th.col-md-1 Date
th.col-md-1 Name
tr(ng-repeat="message in SR.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
a.quick-remove(tooltip="Remove log")
i.fa.fa-trash-o.fa-fw
.center(ng-if = '(SR.messages | count) > 5 || currentLogPage > 1')
pagination(boundary-links="true", total-items="SR.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
//- /Hosts.

View File

@ -1,363 +0,0 @@
angular = require 'angular'
forEach = require 'lodash.foreach'
throttle = require 'lodash.throttle'
#=====================================================================
sourceHost = null
module.exports = angular.module 'xoWebApp.tree', [
require 'angular-file-upload'
require 'angular-ui-router'
require 'xo-api'
require 'xo-services'
require '../delete-vms'
]
.config ($stateProvider) ->
$stateProvider.state 'tree',
controller: 'TreeCtrl'
data: {
requireAdmin: true
},
template: require './view'
url: '/tree'
.controller 'TreeCtrl', (
$scope
$upload
dateFilter
deleteVmsModal
modal
notify
xo
xoApi
) ->
$scope.stats = xoApi.stats
$scope.hosts = xoApi.getView('hosts')
$scope.hostsByPool = xoApi.getIndex('hostsByPool')
$scope.pools = xoApi.getView('pools')
VMs = $scope.VMs = xoApi.getView('VM')
$scope.runningVms = xoApi.getView('runningVms')
$scope.runningVmsByPool = xoApi.getIndex('runningVmsByPool')
$scope.vmsByPool = xoApi.getIndex('vmsByPool')
$scope.vmsByContainer = xoApi.getIndex('vmsByContainer')
$scope.vmControllersByContainer = xoApi.getIndex('vmControllersByContainer')
$scope.srsByContainer = xoApi.getIndex('srsByContainer')
$scope.pool_disconnect = xo.pool.disconnect
$scope.new_sr = xo.pool.new_sr
$scope.pool_addHost = (id) ->
xo.host.attach id
$scope.enableHost = (id) ->
xo.host.enable id
notify.info {
title: 'Host action'
message: 'Host is enabled'
}
$scope.disableHost = (id) ->
modal.confirm({
title: 'Disable host'
message: 'Are you sure you want to disable this host? In disabled state, no new VMs can be started and currently active VMs on the host continue to execute.'
}).then ->
xo.host.disable id
.then ->
notify.info {
title: 'Host action'
message: 'Host is disabled'
}
$scope.pool_removeHost = (id) ->
modal.confirm({
title: 'Remove host from pool'
message: 'Are you sure you want to detach this host from its pool? It will be automatically rebooted'
}).then ->
xo.host.detach id
$scope.rebootHost = (id) ->
modal.confirm({
title: 'Reboot host'
message: 'Are you sure you want to reboot this host? It will be disabled then rebooted'
}).then ->
xo.host.restart id
$scope.restartToolStack = (id) ->
modal.confirm({
title: 'Restart XAPI'
message: 'Are you sure you want to restart the XAPI toolstack?'
}).then ->
xo.host.restartToolStack id
$scope.shutdownHost = (id) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to shutdown this host?'
}).then ->
xo.host.stop id
$scope.startHost = (id) ->
xo.host.start id
bulkConfirms = {
'stopVM': {
title: 'VM shutdown',
message: 'Are you sure you want to shutdown all selected VMs ?'
},
'rebootVM': {
title: 'VM reboot',
message: 'Are you sure you want to reboot all selected VMs ?'
},
'suspendVM': {
title: 'VM suspend',
message: 'Are you sure you want to suspend all selected VMs ?'
},
'force_rebootVM': {
title: 'VM force reboot',
message: 'Are you sure you want to force reboot for all selected VMs ?'
},
'force_stopVM': {
title: 'VM force shutdown',
message: 'Are you sure you want to force shutdown for all selected VMs ?'
},
'migrateVM': {
title: 'VM migrate',
message: 'Are you sure you want to migrate all selected VMs ?'
}
}
unitConfirms = {
'stopVM': {
title: 'VM shutdown',
message: 'Are you sure you want to shutdown this VM ?'
},
'rebootVM': {
title: 'VM reboot',
message: 'Are you sure you want to reboot this VM ?'
},
'suspendVM': {
title: 'VM suspend',
message: 'Are you sure you want to suspend this VM ?'
},
'force_rebootVM': {
title: 'VM force reboot',
message: 'Are you sure you want to force reboot for this VM ?'
},
'force_stopVM': {
title: 'VM force shutdown',
message: 'Are you sure you want to force shutdown for this VM ?'
},
'migrateVM': {
title: 'VM force shutdown',
message: 'Are you sure you want to migrate this VM ?'
}
}
$scope.startVM = xo.vm.start
$scope.stopVM = xo.vm.stop
$scope.force_stopVM = (id) -> xo.vm.stop id, true
$scope.rebootVM = xo.vm.restart
$scope.force_rebootVM = (id) -> xo.vm.restart id, true
$scope.suspendVM = (id) -> xo.vm.suspend id, true
$scope.resumeVM = (id) -> xo.vm.resume id, true
$scope.migrateVM = (id, hostId) -> xo.vm.migrate id, hostId
$scope.snapshotVM = (id) ->
vm = xoApi.get(id)
date = dateFilter Date.now(), 'yyyy-MM-ddTHH:mmZ'
snapshot_name = "#{vm.name_label}_#{date}"
xo.vm.createSnapshot id, snapshot_name
# check if there is any operation pending on a VM
$scope.isVMWorking = (VM) ->
return true for _ of VM.current_operations
false
$scope.deleteVMs = ->
{selected_VMs} = $scope
deleteVmsModal (id for id, selected of selected_VMs when selected)
# VMs checkboxes.
do ->
# This map marks which VMs are selected.
selected_VMs = $scope.selected_VMs = Object.create null
# Number of selected VMs.
$scope.n_selected_VMs = 0
# This is the master checkbox.
# Three states: true/false/null
$scope.master_selection = false
# Wheter all VMs are selected.
$scope.all = false
# Whether no VMs are selected.
$scope.none = true
# Updates `all`, `none` and `master_selection` when necessary.
$scope.$watch 'n_selected_VMs', (n) ->
$scope.all = (VMs.size is n)
$scope.none = (n is 0)
# When the master checkbox is clicked from indeterminate
# state, it should go to unchecked like Gmail.
$scope.master_selection = (n isnt 0)
make_matcher = (sieve) ->
(item) ->
for key, val of sieve
return false unless item[key] is val
true
$scope.selectVMs = (sieve) ->
if (sieve is true) or (sieve is false)
forEach(VMs.all, (VM) ->
selected_VMs[VM.id] = sieve
return
)
$scope.n_selected_VMs = if sieve then VMs.size else 0
return
matcher = make_matcher sieve
n = 0
forEach(VMs.all, (VM) ->
if (selected_VMs[VM.id] = matcher(VM))
++n
return
)
$scope.n_selected_VMs = n
$scope.updateVMSelection = (id) ->
if selected_VMs[id]
++$scope.n_selected_VMs
else
--$scope.n_selected_VMs
$scope.bulkAction = (action, args...) ->
fn = $scope[action]
unless angular.isFunction fn
throw new Error "invalid action #{action}"
runBulk = () ->
for id, selected of selected_VMs
fn id, args... if selected
# Unselects all VMs.
$scope.selectVMs false
if action of bulkConfirms
modal.confirm(bulkConfirms[action])
.then runBulk
else
runBulk()
$scope.confirmAction = (action, args...) ->
fn = $scope[action]
unless angular.isFunction fn
throw new Error "invalid action #{action}"
doAction = () ->
fn args...
if action of unitConfirms
modal.confirm(unitConfirms[action])
.then doAction
else
doAction()
$scope.importVm = ($files, id) ->
file = $files[0]
notify.info {
title: 'VM import started'
message: "Starting the VM import"
}
xo.vm.import id
.then ({ $sendTo: url }) ->
return $upload.http {
method: 'POST'
url
data: file
}
.then (result) ->
throw result.status if result.status isnt 200
notify.info
title: 'VM import'
message: 'Success'
$scope.patchPool = ($files, id) ->
file = $files[0]
xo.pool.patch id
.then ({ $sendTo: url }) ->
return $upload.http {
method: 'POST'
url
data: file
}
.progress throttle(
(event) ->
percentage = (100 * event.loaded / event.total)|0
notify.info
title: 'Upload patch'
message: "#{percentage}%"
6e3
)
.then (result) ->
throw result.status if result.status isnt 200
notify.info
title: 'Upload patch'
message: 'Success'
.directive 'draggable', () ->
{
link: (scope, element, attr) ->
element.on 'dragstart', (event) ->
event.originalEvent.dataTransfer.setData('vm', event.currentTarget.getAttribute('vm'))
# event.originalEvent.dataTransfer.setData('host', event.currentTarget.getAttribute('host'))
sourceHost = event.currentTarget.getAttribute('host')
element.addClass('xo-dragged')
$('[droppable]:not([host="' + sourceHost + '"])').addClass('xo-drop-legit')
element.on 'dragend', (event) ->
element.removeClass('xo-dragged')
$('[droppable]').removeClass('xo-drop-target xo-drop-legit')
sourceHost = null
restrict: 'A'
}
.directive 'droppable', (xo, notify, modal) ->
{
link: (scope, element, attr) ->
element.on 'dragover', (event) ->
event.preventDefault()
targetHost = event.currentTarget.getAttribute('host')
if sourceHost isnt targetHost
element.addClass('xo-drop-target').removeClass('xo-drop-legit')
element.on 'dragleave', (event) ->
targetHost = event.currentTarget.getAttribute('host')
if sourceHost isnt targetHost
element.removeClass('xo-drop-target')
element.addClass('xo-drop-legit')
element.on 'drop', (event) ->
event.preventDefault()
vm = event.originalEvent.dataTransfer.getData('vm')
# sourceHost = event.originalEvent.dataTransfer.getData('host')
targetHost = event.currentTarget.getAttribute('host')
if sourceHost isnt targetHost
modal.confirm
title: 'VM migrate'
message: 'Are you sure you want to migrate this VM?'
.then ->
xo.vm.migrate vm, targetHost
restrict: 'A'
}
# A module exports its name.
.name

View File

@ -1,404 +0,0 @@
//- @todo Remove code duplication for the VMs listing by using a macro.
.sub-bar
.grid
.grid-cell.overview
//- Stats
i(tooltip="{{pools.size}} pools").hidden-xs
i.small {{pools.size}}x
| &nbsp;
i.xo-icon-pool
| &nbsp;
| &nbsp;
i(tooltip="{{hosts.size}} hosts").hidden-xs
i.small {{hosts.size}}x
| &nbsp;
i.xo-icon-host
| &nbsp;
| &nbsp;
i(tooltip="{{runningVms.size}} of {{VMs.size}} VMs running")
i.small {{runningVms.size}}x
| &nbsp;
i.xo-icon-vm
| &nbsp;
| &nbsp;
i(tooltip="{{stats.$vCPUs}} vCPUs used of {{stats.$CPUs}} CPUs")
i.small {{stats.$vCPUs}}x
| &nbsp;
i.xo-icon-cpu
| &nbsp;
| &nbsp;
i(tooltip="{{stats.$memory.usage | bytesToSize}} RAM allocated of {{stats.$memory.size | bytesToSize}}")
i.small {{stats.$memory.usage | bytesToSize}}
| &nbsp;
i.xo-icon-memory
.grid-cell
.btn-group.before-action-bar.dropdown(dropdown)
a.btn.navbar-btn.btn-default.dropdown-toggle.inversed(dropdown-toggle)
input.inverse(
type="checkbox",
ng-model="master_selection",
ng-change="selectVMs(master_selection)",
ui-indeterminate="!(all || none)", stop-event="click"
)
| &nbsp;
i.fa.fa-caret-down
ul.dropdown-menu.inverse(role="menu")
li(ng-repeat="power_state in ['Halted', 'Running']")
a(ng-click="selectVMs({power_state: power_state})")
i.fa-fw(class="xo-icon-{{power_state | lowercase}}")
| {{power_state}}
li.divider
li(
ng-if="hosts.size"
ng-repeat="host in hosts.all | map | orderBy:natural('name_label') track by host.id"
)
a(ng-click="selectVMs({$container: host.id})")
i.xo-icon-host.fa-fw
| On {{host.name_label}}
.action-bar(ng-if="!none")
| &nbsp;
.btn-group
button.btn.navbar-btn.btn-default.inversed(tooltip="Stop VM", type="button", ng-click="bulkAction('stopVM')")
i.fa.fa-stop
button.btn.navbar-btn.btn-default.inversed(tooltip="Start VM", type="button", ng-click="bulkAction('startVM')")
i.fa.fa-play
button.btn.navbar-btn.btn-default.inversed(tooltip="Reboot VM", type="button", ng-click="bulkAction('rebootVM')")
i.fa.fa-refresh
| &nbsp;
.btn-group.dropdown(dropdown)
button.btn.navbar-btn.btn-default.dropdown-toggle.inversed(
dropdown-toggle
tooltip="Migrate VM"
type="button"
)
i.fa.fa-share
| &nbsp;
i.fa.fa-caret-down
ul.dropdown-menu.inverse(role="menu")
li(ng-repeat="host in hosts.all | map | orderBy:natural('name_label') track by host.id")
a(ng-click="bulkAction('migrateVM', host.id)")
i.xo-icon-host.fa-fw
| To {{host.name_label}}
| &nbsp;
.btn-group.dropdown(dropdown)
button.btn.navbar-btn.btn-default.dropdown-toggle.inversed(
dropdown-toggle
type="button"
)
| More
| &nbsp;
i.fa.fa-caret-down
ul.dropdown-menu.inverse(role="menu")
li
a(ng-click="bulkAction('suspendVM')")
i.fa.fa-pause.fa-fw
| Suspend
li
a(ng-click="bulkAction('resumeVM')")
i.fa.fa-play.fa-fw
| Resume
li
a(ng-click="bulkAction('force_rebootVM')")
i.fa.fa-bolt.fa-fw
| Force reboot
li
a(ng-click="bulkAction('force_stopVM')")
i.fa.fa-power-off.fa-fw
| Force shutdown
li.divider
li
a(ng-click="bulkAction('snapshotVM')")
i.xo-icon-snapshot.fa-fw
| Take a snapshot
li
a(ng-click="deleteVMs()")
i.fa.fa-trash-o.fa-fw
| Delete
//- FIXME: Ugly trick to force the pools to be under the sub bar.
//- Add +7px to the 50px for having some space before the first pool.
div(style="margin-top: 57px; visibility: hidden; height: 0") .
//- If we haven't any data
.panel.panel-default.text-center(ng-if="!pools.size")
h1 Welcome on Xen Orchestra!
h3 It seems you aren't connected to any Xen server:
br
a.btn.btn-success.big(ui-sref="settings.index")
i.fa.fa-plus-circle
| Add server
br
br
br
p You can add a new host anytime by clicking on the menu icon "
i.fa.fa-th
| " and choose "
i.fa.fa-cog
| Settings"
p Enjoy Xen Orchestra!
//- If we have data
.container-fluid(ng-if="pools.size")
//- Contains a pool and all its children (hosts).
.grid.pool-block(ng-repeat="pool in pools.all | map | orderBy:[natural('name_label'), 'id'] track by pool.id")
//- Pseudo pool if it is not a named pool.
//- .grid-cell.pool-cell(ng-if="!pool.name_label")
//- p.center(style="margin-top: 2em;") No pool connected
//- Contains information about the pool if it is a named pool.
.grid-cell.pool-cell.hidden-xs
//- Header (name + dropdown menu).
.dropdown.dropdown-pool(dropdown)
a.pool-name(ui-sref="pools_view({id: pool.id})")
span(ng-if="pool.name_label")
| {{pool.name_label}}
span.text-muted(ng-if="!pool.name_label")
| {{(pool.master | resolve).name_label}}
a.dropdown-toggle(ng-if="pool.name_label", dropdown-toggle)
| &nbsp;
i.fa.fa-caret-down.big-caret
ul.dropdown-menu.left(role="menu")
//- TODO: remove until handled this properly
//- li
//- a(xo-sref="SRs_new({container: pool.id})")
//- i.xo-icon-sr.fa-fw
//- | Add SR
li
a(xo-sref="VMs_new({container: pool.id})")
i.xo-icon-vm.fa-fw
| Create VM
//- TODO: solve the "a" problem for ng-file-select
li(ng-file-select="patchPool($files, pool.id)")
a
i.fa.fa-file-code-o.fa-fw
| Patch
li.divider
li
a.disabled(xo-click="pool_disconnect(pool.id)")
i.fa.fa-unlink.fa-fw
| Disconnect
//- /Header.
//- Stats & SRs list.
div
//- Stats.
ul.list-unstyled.stats
li
i(tooltip="{{hostsByPool[pool.id] | count}} hosts connected")
i.small {{hostsByPool[pool.id] | count}}x
| &nbsp;
i.xo-icon-host
| &nbsp;
| &nbsp;
i(tooltip="{{runningVmsByPool[pool.id] | count}} of {{vmsByPool[pool.id] | count}} VMs running")
i.small {{runningVmsByPool[pool.id] | count}}x
| &nbsp;
i.xo-icon-vm
li(ng-if="pool.master")
| Master:
| &nbsp;
a(ui-sref="hosts_view({id: (pool.master | resolve).id})") {{(pool.master | resolve).name_label}}
//- /Stats.
//- SRs.
div(ng-if="!(srsByContainer[pool.id] | isEmpty)")
p.center.small-caps SRs:
table.table.table-hover.table-condensed
tr(ng-repeat="SR in srsByContainer[pool.id] | map | orderBy:natural('name_label') track by SR.id", xo-sref="SRs_view({id: SR.id})")
td.col-md-6.sr-name.no-border(ng-class="{'default-sr': SR.id === pool.default_SR}", title="{{SR.name_label}}")
i.xo-icon-sr
| {{SR.name_label}}
td.col-md-6.right.no-border
.progress.progress-small(tooltip="Disk: {{[SR.usage, SR.size] | percentage}} allocated")
.progress-bar(role="progressbar", aria-valuenow="{{100*SR.usage/SR.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[SR.usage, SR.size] | percentage}}")
//- Contains all the hosts of this pool.
.grid-cell.grid--gutters.hosts-vms-cells
//- Contains a host and all its children (VMs).
.grid(ng-repeat="host in hostsByPool[pool.id] | map | orderBy:natural('name_label') track by host.id")
//- Contains information about the host.
.grid-cell.host-cell
//- Header (name + dropdown menu).
.dropdown.dropdown-pool(dropdown)
a.host-name(ui-sref="hosts_view({id: host.id})")
| {{host.name_label}}
a.dropdown-toggle(dropdown-toggle)
| &nbsp;
i.fa.fa-caret-down
ul.dropdown-menu.left(role="menu")
li
a(xo-sref="SRs_new({container: host.id})")
i.xo-icon-sr.fa-fw
| Add SR
li
a(xo-sref="VMs_new({container: host.id})")
i.xo-icon-vm.fa-fw
| Create VM
//- TODO: solve the "a" problem for ng-file-select
li(ng-file-select="importVm($files, host.id)")
a
i.fa.fa-upload.fa-fw
| Import VM
li.divider
li
a(ng-repeat="controller in [vmControllersByContainer[host.id]] track by controller.id", xo-sref="consoles_view({id: controller.id})")
i.xo-icon-console.fa-fw
| Console
li(ng-if="!host.enabled")
a(xo-click="enableHost(host.id)")
i.fa.fa-check-circle.fa-fw
| Enable
li(ng-if="host.enabled")
a(xo-click="disableHost(host.id)")
i.fa.fa-times-circle.fa-fw
| Disable
li
a(xo-click="rebootHost(host.id)")
i.fa.fa-refresh.fa-fw
| Reboot
li(ng-if="host.power_state === 'Halted'")
a(xo-click="startHost(host.id)")
i.fa.fa-power-off.fa-fw
| Start
li(ng-if="host.power_state === 'Running'")
a(xo-click="shutdownHost(host.id)")
i.fa.fa-power-off.fa-fw
| Shutdown
li
a(xo-click="restartToolStack(host.id)")
i.fa.fa-retweet.fa-fw
| Restart toolstack
//- /Header.
//- Stats.
ul.list-unstyled.stats
//- Warning icon if host is halted or disabled
li.text-danger(ng-if="host.power_state === 'Halted'")
i.fa.fa-warning
| Halted
li.text-warning(ng-if="!host.enabled && host.power_state === 'Running'")
i.fa.fa-warning
| Disabled
//- Memory
li(ng-if="host.power_state === 'Running' && host.enabled")
i.xo-icon-memory.i-progress
.progress.progress-small(tooltip="RAM: {{[host.memory.usage, host.memory.size] | percentage}} allocated")
.progress-bar(role="progressbar", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | percentage}}")
//- Host address
li.text-muted.substats
i.xo-icon-network
| {{host.address}}
//- Contains all the VMs of this host.
.grid-cell.vm-cell(droppable = 'true', host = '{{ host.id }}')
//- If no VMs, fill the space with a message.
.vms-notice(ng-if="vmsByContainer[host.id] | isEmpty")
//- | Host halted.
p(ng-if="host.power_state === 'Halted'")
| Host halted.
div(ng-if="host.power_state === 'Running'")
p(ng-if="!host.enabled")
| Host disabled.
p(ng-if="host.enabled")
| No VMs on this host.
//- /Message if no VMs.
//- TODO: comment
.table-responsive(ng-if="!(vmsByContainer[host.id] | isEmpty)")
table.table.table-hover.table-condensed
//- Contains a VM.
tr(ng-repeat="VM in vmsByContainer[host.id] | map | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})", draggable = 'true', vm = '{{ VM.id }}', host = '{{ host.id }}')
//- Handle used for drag & drop.
td.grab
//- Checkbox used for selection.
td.select-vm
input(type="checkbox", ng-model="selected_VMs[VM.id]", ng-change="updateVMSelection(VM.id)")
//- Power state
td.vm-power-state
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
p.vm {{VM.name_label}}
//- Quick actions.
td.vm-quick-buttons.col-md-2.hidden-xs
.quick-buttons
a(tooltip="Shutdown VM", xo-click="confirmAction('stopVM', VM.id)")
i.fa.fa-stop
a(tooltip="Start VM", xo-click="startVM(VM.id)")
i.fa.fa-play
a(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
//- Description.
td.vm-description.col-md-4.hidden-xs
i(class="icon-{{VM.os_version.distro | osFamily}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
| &nbsp;
i.fa.fa-fw(ng-if="!VM.os_version.distro")
| {{VM.name_description}}
//- Metrics.
//- Memory
td.vm-memory-stat.col-md-2.hidden-xs
.cpu
| {{VM.memory.size | bytesToSize}}
i.xo-icon-docker.fa-fw(ng-if="VM.docker", tooltip="Docker enabled")
i.fa.fa-fw(ng-if="VM.xenTools === 'up to date' && !VM.docker")
i.xo-icon-warning.fa-fw(ng-if="VM.xenTools === 'out of date'", tooltip="Xen tools outdated")
i.xo-icon-info.fa-fw(ng-if="VM.xenTools === false", tooltip="Xen tools not installed")
i.xo-icon-other.fa-fw(ng-if="VM.xenTools === undefined", tooltip="Unknown")
//- /Metrics.
//- Address.
td.text-muted.text-right.col-md-2.hidden-xs
| {{VM.addresses["0/ip"]}}
//- Contains a pseudo-host which contains all VMs not in any hosts.
.grid(ng-if="!(vmsByPool[pool.id] | isEmpty)")
//- This is where the information about a host would be displayed.
.grid-cell.host-cell
//- Contains all the VMs of this pool.
.grid-cell.vm-cell
//- TODO: comment
.table-responsive
table.table.table-hover.table-condensed
//- Contains a VM.
tr(ng-repeat="VM in vmsByContainer[pool.id] | map | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})")
//- Handle used for drag & drop.
td.grab
//- Checkbox used for selection.
td.select-vm
input(type="checkbox", ng-model="selected_VMs[VM.id]", ng-change="updateVMSelection(VM.id)")
//- Power state
td.vm-power-state
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
p.vm {{VM.name_label}}
//- Quick actions.
td.vm-quick-buttons.col-md-2.hidden-xs
.quick-buttons
a(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)")
i.fa.fa-refresh
a(tooltip="VM Console")
i.xo-icon-console
//- Description.
td.vm-description.col-md-4.hidden-xs
i(class="icon-{{VM.os_version.distro | osFamily}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
| &nbsp;
i.fa.fa-fw(ng-if="!VM.os_version.distro")
| {{VM.name_description}}
//- Metrics.
//- Memory
td.vm-memory-stat.col-md-2.hidden-xs
.cpu
| {{VM.memory.size | bytesToSize}}
i.xo-icon-docker.fa-fw(ng-if="VM.docker", tooltip="Docker enabled")
i.fa.fa-fw(ng-if="VM.xenTools === 'up to date' && !VM.docker")
i.xo-icon-warning.fa-fw(ng-if="VM.xenTools === 'out of date'", tooltip="Xen tools outdated")
i.xo-icon-info.fa-fw(ng-if="VM.xenTools === false", tooltip="Xen tools not installed")
i.xo-icon-other.fa-fw(ng-if="VM.xenTools === undefined", tooltip="Unknown")
//- /Metrics.
//- Address.
td.text-muted.text-right.col-md-2.hidden-xs
| {{VM.addresses["0/ip"]}}
//- /Pseudo host containing VMs not on any hosts.
//- /Hosts of this pool.
//- /Pool with its children.

View File

@ -1,363 +0,0 @@
import * as format from '@julien-f/json-rpc/format'
import angular from 'angular'
import 'angular-bootstrap'
import Bluebird from 'bluebird'
import makeError from 'make-error'
import parse from '@julien-f/json-rpc/parse'
import WebSocket from 'ws'
import {EventEmitter} from 'events'
import modal from './modal'
const calls = {}
function jsonRpcCall (socket, method, params = {}) {
const req = format.request(method, params)
const reqId = req.id
socket.send(JSON.stringify(req))
let waiter = {}
const promise = new Bluebird((resolve, reject) => {
waiter.resolve = resolve
waiter.reject = reject
})
calls[reqId] = waiter
return promise
}
function jsonRpcNotify (socket, method, params = {}) {
return Bluebird.resolve(socket.send(JSON.stringify(format.notification(method, params))))
}
function getCurrentUrl () {
if (typeof window === 'undefined') {
throw new Error('cannot get current URL')
}
return String(window.location)
}
function adaptUrl (url, port = null) {
const matches = /^http(s?):\/\/([^\/:]*(?::[^\/]*)?)(?:[^:]*)?$/.exec(url)
if (!matches || !matches[2]) {
throw new Error('current URL not recognized')
}
return 'ws' + matches[1] + '://' + matches[2] + '/api/updater'
}
function blockXoaAccess (xoaState) {
return xoaState.state === 'untrustedTrial'
}
export const NotRegistered = makeError('NotRegistered')
export const AuthenticationFailed = makeError('AuthenticationFailed')
export default angular.module('updater', [
'ui.bootstrap'
])
.factory('updater', function ($interval, $timeout, $window, $modal) {
class Updater extends EventEmitter {
constructor () {
super()
this._log = []
this._lastRun = 0
this._lowState = null
this.state = null
this.registerState = 'uknown'
this.registerError = ''
this._connection = null
this.isConnected = false
this.updating = false
this.upgrading = false
this.token = null
}
update () {
this.emit('updating')
this.updating = true
return this._update(false)
}
upgrade () {
this.emit('upgrading')
this.upgrading = true
return this._update(true)
}
_promptForReload () {
const modalInstance = $modal.open({
template: modal,
backdrop: false
})
return modalInstance.result
.then(() => {
$window.location.reload()
})
.catch(() => true)
}
_open () {
if (this._connection) {
return this._connection
} else {
this._connection = new Bluebird((resolve, reject) => {
const socket = new WebSocket(adaptUrl(getCurrentUrl()))
const middle = new EventEmitter()
this.isConnected = true
const timeout = $timeout(() => {
middle.emit('reconnect_failed')
}, 4000)
socket.onmessage = ({data}) => {
const message = parse(data)
if (message.type === 'response' && message.id !== undefined) {
if (calls[message.id]) {
if (message.result) {
calls[message.id].resolve(message.result)
} else {
calls[message.id].reject(message.error)
}
delete calls[message.id]
}
} else {
middle.emit(message.method, message.params)
}
}
socket.onclose = () => {
middle.emit('disconnect')
}
middle.on('connected', ({message}) => {
$timeout.cancel(timeout)
this.log('success', message)
this.state = 'connected'
resolve(socket)
if (!this.updating) {
this.update()
}
this.emit('connected', message)
})
middle.on('print', ({content}) => {
Array.isArray(content) || (content = [content])
content.forEach(elem => this.log('info', elem))
this.emit('print', content)
})
middle.on('end', end => {
this._lowState = end
switch (this._lowState.state) {
case 'xoa-up-to-date':
case 'xoa-upgraded':
case 'updater-upgraded':
this.state = 'upToDate'
break
case 'xoa-upgrade-needed':
case 'updater-upgrade-needed':
this.state = 'upgradeNeeded'
break
case 'register-needed':
this.state = 'registerNeeded'
break
case 'error':
this.state = 'error'
break
default:
this.state = null
}
this.log(end.level, end.message)
this._lastRun = Date.now()
this.upgrading = this.updating = false
this.emit('end', end)
if (this._lowState.state === 'updater-upgraded') {
this.update()
} else if (this._lowState.state === 'xoa-upgraded') {
this._promptForReload()
}
this.xoaState()
})
middle.on('warning', warning => {
this.log('warning', warning.message)
this.emit('warning', warning)
})
middle.on('server-error', error => {
this.log('error', error.message)
this._lowState = error
this.state = 'error'
this.upgrading = this.updating = false
this.emit('error', error)
})
middle.on('disconnect', () => {
this._lowState = null
this.state = null
this.upgrading = this.updating = false
this.log('warning', 'Lost connection with xoa-updater')
this.emit('disconnect')
middle.emit('reconnect_failed') // No reconnecting attempts implemented so far
})
middle.on('reconnect_failed', () => {
this.isConnected = false
middle.removeAllListeners()
socket.close()
this._connection = null
const message = 'xoa-updater could not be reached'
this._xoaStateError({message})
reject(new Error(message))
this.log('error', message)
this.emit('reconnect_failed')
})
})
return this._connection
}
}
isRegistered () {
return this._open()
.then(socket => {
return jsonRpcCall(socket, 'isRegistered')
.then(token => {
if (token.registrationToken === undefined) {
throw new NotRegistered('Your Xen Orchestra Appliance is not registered')
} else {
this.registerState = 'registered'
this.token = token
return token
}
})
})
.catch(NotRegistered, () => {
this.registerState = 'unregistered'
})
.catch(error => {
this.registerError = error.message
this.registerState = 'error'
})
}
register (email, password, renew = false) {
return this._open()
.then(socket => {
return jsonRpcCall(socket, 'register', {email, password, renew})
.then(token => {
this.registerState = 'registered'
this.registerError = ''
this.token = token
return token
})
})
.catch(error => {
if (error.code && error.code === 1) {
this.registerError = 'Authentication failed'
throw new AuthenticationFailed('Authentication failed')
} else {
this.registerError = error.message
this.registerState = 'error'
throw error
}
})
}
xoaState () {
return this._open()
.then(socket => {
return jsonRpcCall(socket, 'xoaState')
.then(state => {
this._xoaState = state
this._xoaStateTS = Date.now()
return state
})
})
.catch(error => this._xoaStateError(error))
}
_xoaStateError (error) {
this._xoaState = {
state: 'ERROR',
message: error.message
}
this._xoaStateTS = Date.now()
return this._xoaState
}
_update (upgrade = false) {
return this._open()
.tap(() => this.log('info', 'Start ' + (upgrade ? 'upgrading' : 'updating' + '...')))
.then(socket => jsonRpcNotify(socket, 'update', {upgrade}))
}
start () {
if (!this._xoaState) {
this.xoaState()
}
if (!this._interval) {
this._interval = $interval(() => this.run(), 60 * 60 * 1000)
return this.run()
} else {
return Bluebird.resolve()
}
}
stop () {
if (this._interval) {
$interval.cancel(this._interval)
delete this._interval
}
}
run () {
if (Date.now() - this._lastRun < 24 * 60 * 60 * 1000) {
return Bluebird.resolve()
} else {
return this.update()
}
}
isStarted () {
return this._interval !== null
}
log (level, message) {
const date = new Date()
this._log.unshift({
date: date.toLocaleString(),
level,
message
})
while (this._log.length > 10) {
this._log.pop()
}
}
getConfiguration () {
return this._open()
.then(socket => {
return jsonRpcCall(socket, 'getConfiguration')
.then(configuration => this._configuration = configuration)
})
}
configure (config) {
return this._open()
.then(socket => {
return jsonRpcCall(socket, 'configure', config)
.then(configuration => this._configuration = configuration)
})
}
}
return new Updater()
})
.run(function (updater, $rootScope, $state, xoApi) {
updater.start()
.catch(() => {})
$rootScope.$on('$stateChangeStart', function (event, state) {
if (Date.now() - updater._xoaStateTS > (60 * 60 * 1000)) {
updater.xoaState()
}
let {user} = xoApi
let loggedIn = !!user
if (!loggedIn || !updater._xoaState || state.name === 'settings.update') {
return
} else if (blockXoaAccess(updater._xoaState)) {
event.preventDefault()
$state.go('settings.update')
}
})
})
.name

View File

@ -1,19 +0,0 @@
.modal-header
button.close(
type = 'button',
ng-click = '$dismiss()'
)
span(aria-hidden = 'true') &times;
h4.modal-title
i.fa.fa-bell.text-info
| &nbsp;Upgrade successful
.modal-body
p Your XOA has successfully upgraded, and your browser must reload the application.
button.btn.btn-primary(type = 'button', ng-click = '$close()')
i.fa.fa-repeat
| &nbsp;Reload now
| &ensp;
button.btn.btn-default(type = 'button', ng-click = '$dismiss()')
i.fa.fa-hand-paper-o
| &nbsp;No, I'll reload myself later (F5)

View File

@ -1,826 +0,0 @@
angular = require 'angular'
filter = require 'lodash.filter'
forEach = require 'lodash.foreach'
isEmpty = require 'lodash.isempty'
sortBy = require 'lodash.sortby'
#=====================================================================
module.exports = angular.module 'xoWebApp.vm', [
require 'angular-ui-router',
require 'angular-ui-bootstrap'
require 'iso-device'
require 'tag'
]
.config ($stateProvider) ->
$stateProvider.state 'VMs_view',
url: '/vms/:id'
controller: 'VmCtrl'
template: require './view'
.controller 'VmCtrl', (
$scope, $state, $stateParams, $location, $q
xoApi, xo
sizeToBytesFilter, bytesToSizeFilter, xoHideUnauthorizedFilter
modal
$window
$timeout
dateFilter
notify
) ->
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
{get} = xoApi
pool = null
host = null
vm = null
do (
networksByPool = xoApi.getIndex('networksByPool')
srsByContainer = xoApi.getIndex('srsByContainer')
poolSrs = null
hostSrs = null
) ->
Object.defineProperties($scope, {
networks: {
get: () => pool && networksByPool[pool.id]
}
})
updateSrs = () =>
srs = []
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
srs = xoHideUnauthorizedFilter(srs)
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
$scope.SRs = srs
vm and prepareDiskData()
$scope.$watchCollection(
() => pool and srsByContainer[pool.id],
(srs) =>
poolSrs = srs
updateSrs()
)
$scope.$watchCollection(
() => host and srsByContainer[host.id],
(srs) =>
hostSrs = srs
updateSrs()
)
$scope.$watchCollection(
() => vm and vm.$VBDs,
(vbds) =>
return unless vbds?
prepareDiskData()
)
$scope.objects = xoApi.all
$scope.currentLogPage = 1
$scope.currentSnapPage = 1
$scope.currentPCIPage = 1
$scope.currentGPUPage = 1
$scope.refreshStatControl = refreshStatControl = {
baseStatInterval: 5000
baseTimeOut: 10000
period: null
running: false
attempt: 0
start: () ->
return if this.running
this.stop()
this.running = true
this._reset()
$scope.$on('$destroy', () => this.stop())
return this._trig(Date.now())
_trig: (t1) ->
if this.running
timeoutSecurity = $timeout(
() => this.stop(),
this.baseTimeOut
)
return $scope.refreshStats($scope.VM.id)
.then () => this._reset()
.catch (err) =>
if !this.running || this.attempt >= 2 || $scope.VM.power_state isnt 'Running' || $scope.isVMWorking($scope.VM)
return this.stop()
else
this.attempt++
.finally () =>
$timeout.cancel(timeoutSecurity)
if this.running
t2 = Date.now()
return this.period = $timeout(
() => this._trig(t2),
Math.max(this.baseStatInterval - (t2 - t1), 0)
)
_reset: () ->
this.attempt = 0
stop: () ->
if this.period
$timeout.cancel(this.period)
this.running = false
return
}
$scope.hosts = xoApi.getView('hosts')
$scope.hostsByPool = xoApi.getIndex('hostsByPool')
$scope.$watch(
-> get $stateParams.id, 'VM'
(VM) ->
$scope.VM = vm = VM
return unless VM?
# For the edition of this VM.
$scope.memorySize = bytesToSizeFilter VM.memory.size
$scope.bootParams = parseBootParams($scope.VM.boot.order)
$scope.prepareVDIs()
container = get VM.$container
if container.type is 'host'
host = $scope.host = container
pool = $scope.pool = (get container.$poolId) ? {}
else
host = $scope.host = {}
pool = $scope.pool = container
if VM.power_state is 'Running' && !($scope.isVMWorking($scope.VM))
refreshStatControl.start()
else
refreshStatControl.stop()
)
$scope.prepareVDIs = () ->
return unless $scope.VM
# build VDI list of this VM
VDIs = []
for VBD in $scope.VM.$VBDs
oVbd = get VBD
continue unless oVbd
oVdi = get oVbd.VDI
continue unless oVdi
VDIs.push oVdi if oVdi and not oVbd.is_cd_drive
$scope.VDIs = sortBy(VDIs, (value) -> (get resolveVBD(value))?.position);
descriptor = (obj) ->
if !obj
return ''
return obj.name_label + (if obj.name_description.length then ' - ' + obj.name_description else '')
prepareDiskData = () ->
# For populating adding position choice
unfreePositions = [];
maxPos = 0;
# build VDI list of this VM
for VBD in $scope.VM.$VBDs
oVbd = get VBD
oVdi = get oVbd?.VDI
if oVdi?
unfreePositions.push parseInt oVbd.position
maxPos = if (oVbd.position > maxPos) then parseInt oVbd.position else maxPos
$scope.maxPos = maxPos
VDIOpts = []
authSRs = xoHideUnauthorizedFilter($scope.SRs)
for SR in authSRs
if 'iso' isnt SR.SR_type
for rVdi in SR.VDIs
oVdi = get rVdi
if oVdi
VDIOpts.push({
sr: descriptor(SR),
label: descriptor(oVdi),
vdi: oVdi
})
$scope.VDIOpts = VDIOpts
parseBootParams = (params) ->
texts = {
c: 'Hard-Drive',
d: 'DVD-Drive',
n: 'Network'
}
bootParams = []
i = 0
if params
while (i < params.length)
char = params.charAt(i++)
bootParams.push({
e: char,
t: texts[char],
v: true
})
delete texts[char]
for key, text of texts
bootParams.push({
e: key,
t: text,
v: false
})
return bootParams
$scope.bootMove = (index, move) ->
tmp = $scope.bootParams[index + move]
$scope.bootParams[index + move] = $scope.bootParams[index]
$scope.bootParams[index] = tmp
$scope.saveBootParams = (id, bootParams) ->
if $scope.savingBootOrder
return
$scope.savingBootOrder = true
paramString = ''
forEach(bootParams, (boot) -> boot.v && paramString += boot.e)
return xoApi.call 'vm.bootOrder', {vm: id, order: paramString}
.finally () ->
$scope.savingBootOrder = false
$scope.bootReordering = false
$scope.refreshStats = (id) ->
return xo.vm.refreshStats id
.then (result) ->
result.stats.cpuSeries = []
if result.stats.cpus.length >= 12
nValues = result.stats.cpus[0].length
nCpus = result.stats.cpus.length
cpuAVG = (0 for [1..nValues])
forEach result.stats.cpus, (cpu) ->
forEach cpu, (stat, index) ->
cpuAVG[index] += stat
return
return
forEach cpuAVG, (cpu, index) ->
cpuAVG[index] /= nCpus
return
result.stats.cpus = [cpuAVG]
result.stats.cpuSeries.push 'CPU AVG'
else
forEach result.stats.cpus, (v,k) ->
result.stats.cpuSeries.push 'CPU ' + k
return
result.stats.vifSeries = []
vifsArray = []
forEach result.stats.vifs.rx, (v,k) ->
return unless v
result.stats.vifSeries.push '#' + k + ' in'
result.stats.vifSeries.push '#' + k + ' out'
vifsArray.push (v || [])
vifsArray.push (result.stats.vifs.tx[k] || [])
return
result.stats.vifs = vifsArray
result.stats.xvdSeries = []
xvdsArray = []
forEach result.stats.xvds.r, (v,k) ->
return unless v
result.stats.xvdSeries.push 'xvd' + k + ' read'
result.stats.xvdSeries.push 'xvd' + k + ' write'
xvdsArray.push (v || [])
xvdsArray.push (result.stats.xvds.w[k] || [])
return
result.stats.xvds = xvdsArray
result.stats.date = []
timestamp = result.endTimestamp
for i in [result.stats.memory.length-1..0] by -1
result.stats.date.unshift new Date(timestamp*1000).toLocaleTimeString()
timestamp -= 5
$scope.stats = result.stats
$scope.startVM = (id) ->
xo.vm.start id
notify.info {
title: 'VM starting...'
message: 'Start VM'
}
$scope.stopVM = (id) ->
modal.confirm
title: 'VM shutdown'
message: 'Are you sure you want to shutdown this VM ?'
.then ->
xo.vm.stop id
notify.info {
title: 'VM shutdown...'
message: 'Gracefully shutdown the VM'
}
$scope.force_stopVM = (id) ->
modal.confirm
title: 'VM force shutdown'
message: 'Are you sure you want to force shutdown for this VM ?'
.then ->
xo.vm.stop id, true
notify.info {
title: 'VM force shutdown...'
message: 'Force shutdown the VM'
}
$scope.rebootVM = (id) ->
modal.confirm
title: 'VM reboot'
message: 'Are you sure you want to reboot this VM ?'
.then ->
xo.vm.restart id
notify.info {
title: 'VM reboot...'
message: 'Gracefully reboot the VM'
}
$scope.force_rebootVM = (id) ->
modal.confirm
title: 'VM reboot'
message: 'Are you sure you want to force reboot for this VM ?'
.then ->
xo.vm.restart id, true
notify.info {
title: 'VM reboot...'
message: 'Force reboot the VM'
}
$scope.suspendVM = (id) ->
modal.confirm
title: 'VM suspend'
message: 'Are you sure you want to suspend this VM ?'
.then ->
xo.vm.suspend id
notify.info {
title: 'VM suspend...'
message: 'Suspend the VM'
}
$scope.resumeVM = (id) ->
xo.vm.resume id, true
notify.info {
title: 'VM resume...'
message: 'Resume the VM'
}
$scope.migrateVM = (id, hostId) ->
modal.confirm
title: 'VM migrate'
message: 'Are you sure you want to migrate this VM?'
.then ->
xo.vm.migrate id, hostId
$scope.destroyVM = (id) ->
modal.confirm
title: 'VM deletion'
message: 'Are you sure you want to delete this VM? (including its disks)'
.then ->
# FIXME: provides a way to not delete its disks.
xo.vm.delete id, true
.then ->
$state.go 'index'
notify.info {
title: 'VM deletion'
message: 'VM is removed'
}
$scope.saveSnapshot = (id, $data) ->
snapshot = get (id)
result = {
id: snapshot.id
name_label: $data
}
if $data isnt snapshot.name_label
result.name_label = $data
xoApi.call 'vm.set', result
$scope.saveVM = ($data) ->
{VM} = $scope
{CPUs, memory, name_label, name_description, high_availability, auto_poweron, PV_args} = $data
$data = {
id: VM.id
}
if memory isnt $scope.memorySize and (memory = sizeToBytesFilter memory)
$data.memory = memory
$scope.memorySize = bytesToSizeFilter memory
if CPUs isnt VM.CPUs.number
$data.CPUs = +CPUs
if name_label isnt VM.name_label
$data.name_label = name_label
if name_description isnt VM.name_description
$data.name_description = name_description
if high_availability isnt VM.high_availability
$data.high_availability = high_availability
if auto_poweron isnt VM.auto_poweron
$data.auto_poweron = auto_poweron
if PV_args isnt VM.PV_args
$data.PV_args = PV_args
xoApi.call 'vm.set', $data
#-----------------------------------------------------------------
# Disks
#-----------------------------------------------------------------
$scope.moveDisk = (index, direction) ->
{VDIs} = $scope
newIndex = index + direction
[VDIs[index], VDIs[newIndex]] = [VDIs[newIndex], VDIs[index]]
return
migrateDisk = (id, sr_id) ->
return modal.confirm({
title: 'Disk migration'
message: 'Are you sure you want to migrate (move) this disk to another SR?'
}).then ->
notify.info {
title: 'Disk migration'
message: 'Disk migration started'
}
xo.vdi.migrate id, sr_id
return
$scope.saveDisks = (data) ->
# Group data by disk.
disks = {}
forEach data, (value, key) ->
i = key.indexOf '/'
(disks[key.slice 0, i] ?= {})[key.slice i + 1] = value
return
promises = []
# Handle SR change.
forEach disks, (attributes, id) ->
disk = get id
if attributes.$SR isnt disk.$SR
promises.push (migrateDisk id, attributes.$SR)
return
forEach disks, (attributes, id) ->
# Keep only changed attributes.
disk = get id
forEach attributes, (value, name) ->
delete attributes[name] if value is disk[name]
return
unless isEmpty attributes
# Inject id.
attributes.id = id
# Ask the server to update the object.
promises.push xoApi.call 'vdi.set', attributes
return
# Handle Position changes
vbds = xoApi.get($scope.VM.$VBDs)
notFreePositions = Object.create(null)
forEach vbds, (vbd) ->
if vbd.is_cd_drive
notFreePositions[vbd.position] = null
position = 0
forEach $scope.VDIs, (vdi) ->
oVbd = get(resolveVBD(vdi))
unless oVbd
return
while position of notFreePositions
++position
if +oVbd.position isnt position
promises.push(
xoApi.call('vbd.set', {
id: oVbd.id,
position: String(position)
})
)
++position
return $q.all promises
.catch (err) ->
console.log(err);
notify.error {
title: 'saveDisks'
message: err
}
$scope.deleteDisk = (id) ->
modal.confirm({
title: 'Disk deletion'
message: 'Are you sure you want to delete this disk? This operation is irreversible'
}).then ->
xoApi.call 'vdi.delete', {id: id}
return
return
#-----------------------------------------------------------------
# returns the id of the VBD that links the VDI to the VM
$scope.resolveVBD = resolveVBD = (vdi) ->
if not vdi?
return
for vbd in vdi.$VBDs
rVbd = vbd if (get vbd).VM is $scope.VM.id
return rVbd || null
$scope.disconnectVBD = (vdi) ->
id = resolveVBD(vdi)
if id?
modal.confirm({
title: 'VBD disconnection'
message: 'Are you sure you want to detach this VM disk ?'
}).then ->
console.log "Disconnect VBD #{id}"
xo.vbd.disconnect id
$scope.connectVBD = (vdi) ->
id = resolveVBD(vdi)
if id?
console.log "Connect VBD #{id}"
xo.vbd.connect id
$scope.deleteVBD = (vdi) ->
id = resolveVBD(vdi)
if id?
modal.confirm({
title: 'VBD deletion'
message: 'Are you sure you want to delete this VM disk attachment (the disk will NOT be destroyed)?'
}).then ->
console.log "Delete VBD #{id}"
xo.vbd.delete id
$scope.connectVIF = (id) ->
console.log "Connect VIF #{id}"
xo.vif.connect id
$scope.disconnectVIF = (id) ->
modal.confirm
title: 'Disconnect VIF'
message: 'Are you sure you want to disconnect this interface ?'
.then ->
console.log "Disconnect VIF #{id}"
xo.vif.disconnect id
$scope.deleteVIF = (id) ->
console.log "Delete VIF #{id}"
modal.confirm({
title: 'VIF deletion'
message: 'Are you sure you want to delete this Virtual Interface (VIF)?'
}).then ->
xo.vif.delete id
$scope.cloneVM = (id, vm_name, full_copy) ->
clone_name = "#{vm_name}_clone"
console.log "Copy VM #{id} #{clone_name} with full copy at #{full_copy}"
notify.info {
title: 'Clone creation'
message: 'Clone creation started'
}
xo.vm.clone id, clone_name, full_copy
$scope.snapshotVM = (id, vm_name) ->
date = dateFilter Date.now(), 'yyyy-MM-ddTHH:mmZ'
snapshot_name = "#{vm_name}_#{date}"
console.log "Snapshot #{snapshot_name} from VM #{id}"
notify.info {
title: 'Snapshot creation'
message: 'Snapshot creation started'
}
xo.vm.createSnapshot id, snapshot_name
$scope.exportVM = (id) ->
console.log "Export VM #{id}"
notify.info {
title: 'VM export'
message: 'VM export started'
}
xo.vm.export id
.then ({$getFrom: url}) ->
window.open '.' + url
$scope.copyVM = (id, srId) ->
console.log "Copy VM #{id} tp SR #{srId}"
notify.info {
title: 'VM copy'
message: 'VM copy started'
}
xo.vm.copy id, srId, $scope.VM.name_label + '_COPY'
$scope.exportOnlyMetadataVM = (id) ->
console.log "Export Metadata only for VM #{id}"
notify.info {
title: 'VM export'
message: 'VM export started'
}
xo.vm.export id, true, true
.then ({$getFrom: url}) ->
window.open '.' + url
$scope.convertVM = (id) ->
console.log "Convert VM #{id}"
modal.confirm({
title: 'VM to template'
message: 'Are you sure you want to convert this VM into a template?'
})
.then ->
xo.vm.convert id
.then ->
$state.go 'index'
notify.info {
title: 'VM conversion'
message: 'VM is converted to template'
}
$scope.deleteSnapshot = (id) ->
console.log "Delete snapshot #{id}"
modal.confirm({
title: 'Snapshot deletion'
message: 'Are you sure you want to delete this snapshot? (including its disks)'
}).then ->
# FIXME: provides a way to not delete its disks.
xo.vm.delete id, true
$scope.connectPci = (id, pciId) ->
console.log "Connect PCI device "+pciId+" on VM "+id
xo.vm.connectPci id, pciId
$scope.disconnectPci = (id) ->
xo.vm.disconnectPci id
$scope.deleteAllLog = ->
modal.confirm({
title: 'Log deletion'
message: 'Are you sure you want to delete all the logs?'
}).then ->
forEach($scope.VM.messages, (log) =>
console.log "Remove log #{log.id}"
xo.log.delete log.id
return
)
$scope.deleteLog = (id) ->
console.log "Remove log #{id}"
xo.log.delete id
$scope.revertSnapshot = (id) ->
console.log "Revert snapshot to #{id}"
modal.confirm({
title: 'Revert to snapshot'
message: 'Are you sure you want to revert your VM to this snapshot? The VM will be halted and this operation is irreversible'
}).then ->
notify.info {
title: 'Reverting to snapshot'
message: 'VM revert started'
}
xo.vm.revert id
$scope.isVMWorking = (VM) ->
return false unless VM
return true for _ of VM.current_operations
false
$scope.startContainer = (VM,container) ->
console.log "Start from VM "+VM+" to container "+container
xo.docker.start VM, container
$scope.stopContainer = (VM,container) ->
console.log "Stop from VM "+VM+" to container "+container
xo.docker.stop VM, container
$scope.restartContainer = (VM,container) ->
console.log "Restart from VM "+VM+" to container "+container
xo.docker.restart VM, container
$scope.pauseContainer = (VM,container) ->
console.log "Pause from VM "+VM+" to container "+container
xo.docker.pause VM, container
$scope.resumeContainer = (VM,container) ->
console.log "Unpause from VM "+VM+" to container "+container
xo.docker.unpause VM, container
$scope.addVdi = (vdi, readonly, bootable) ->
$scope.addWaiting = true # disables form fields
position = $scope.maxPos + 1
mode = if (readonly || !isFreeForWriting(vdi)) then 'RO' else 'RW'
return xo.vm.attachDisk $scope.VM.id, vdi.id, bootable, mode, String(position)
.then -> $scope.adding = false # Closes form block
.catch (err) ->
console.log(err);
notify.error {
title: 'vm.attachDisk'
message: err
}
.finally ->
$scope.addWaiting = false
$scope.isConnected = isConnected = (vdi) -> (get resolveVBD(vdi))?.attached
$scope.isFreeForWriting = isFreeForWriting = (vdi) ->
free = true
for vbd in vdi.$VBDs
oVbd = get vbd
free = free && (!oVbd?.attached || oVbd?.read_only)
return free
$scope.createVdi = (name, size, sr, bootable, readonly) ->
$scope.createVdiWaiting = true # disables form fields
position = $scope.maxPos + 1
return xo.disk.create name, String(size), sr
.then (diskUuid) ->
mode = if readonly then 'RO' else 'RW'
return xo.vm.attachDisk $scope.VM.id, diskUuid, bootable, mode, String(position)
.then -> $scope.creatingVdi = false # Closes form block
.catch (err) ->
console.log(err);
notify.error {
title: 'Attach Disk'
message: err
}
.catch (err) ->
console.log(err);
notify.error {
title: 'Create Disk'
message: err
}
.finally ->
$scope.createVdiWaiting = false
$scope.updateMTU = (network) ->
$scope.newInterfaceMTU = network.MTU
$scope.createInterface = (network, mtu, automac, mac) ->
$scope.createVifWaiting = true # disables form fields
position = 0
forEach $scope.VM.VIFs, (vf) ->
int = get vf
position = if int?.device > position then (get vf)?.device else position
position++
mtu = String(mtu || network.mtu)
mac = if automac then undefined else mac
return xo.vm.createInterface $scope.VM.id, network.id, String(position), mtu, mac
.then (id) ->
$scope.creatingVif = false
.catch (err) ->
console.log(err);
notify.error {
title: 'Create Interface'
message: err
}
.finally ->
$scope.createVifWaiting = false
$scope.statView = {
cpuOnly: false,
ramOnly: false,
netOnly: false,
diskOnly: false
}
$scope.canAdmin = (id = undefined) ->
if id == undefined
id = $scope.VM && $scope.VM.id
return id && xoApi.canInteract(id, 'administrate') || false
$scope.canOperate = (id = undefined) ->
if id == undefined
id = $scope.VM && $scope.VM.id
return id && xoApi.canInteract(id, 'operate') || false
$scope.canView = (id = undefined) ->
if id == undefined
id = $scope.VM && $scope.VM.id
return id && xoApi.canInteract(id, 'view') || false
# A module exports its name.
.name

View File

@ -1,829 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-vm(ng-if="isVMWorking(VM)", class="xo-color-pending", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
i.xo-icon-vm(class="xo-color-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
| {{VM.name_label}}
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="vmSettings.$show()", ng-if = '!vmSettings.$visible && canAdmin()')
i.fa.fa-edit.fa-fw
span.quick-edit(ng-if="vmSettings.$visible", tooltip="Cancel Edition", ng-click="vmSettings.$cancel()")
i.fa.fa-undo.fa-fw
.panel-body
form(editable-form="", name="vmSettings", onbeforesave="saveVM($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="VM.name_label", e-name="name_label", e-form="vmSettings")
| {{VM.name_label}}
dt Description
dd
span(editable-text="VM.name_description", e-name="name_description", e-form="vmSettings")
| {{VM.name_description}}
dt(ng-if="VM.power_state === 'Running' || VM.power_state === 'Paused'") Running on
dt(ng-if="VM.power_state == 'Halted' || VM.power_state === 'Suspended'") Resident on
dd(ng-repeat="container in [VM.$container] | resolve")
span(ng-if = 'container.type === "host" && canView(container.id)')
a(xo-sref="hosts_view({id: container.id})")
| {{container.name_label}}
small
span(ng-if="(container.$poolId | resolve).name_label")
| (
a(ui-sref="pools_view({id: (container.$poolId | resolve).id})") {{(container.$poolId | resolve).name_label}}
| )
a(
ng-if = 'container.type === "pool" && canView(container.id)'
xo-sref="pools_view({id: container.id})"
)
| {{container.name_label}}
dt Tags
dd
xo-tag(ng-if = 'VM', object = 'VM')
dt Addresses
dd(ng-if="!VM.addresses") -
dd(ng-repeat="IP in VM.addresses") {{IP}}
dt(ng-if="!(VM.$poolId | resolve).HA_enabled") Auto Power
dd(ng-if="!(VM.$poolId | resolve).HA_enabled")
span(
editable-select="VM.auto_poweron"
e-ng-options="ap.v as ap.t for ap in [{v: true, t:'Yes'}, {v: false, t:'No'}]"
e-name="auto_poweron"
e-form="vmSettings"
)
| {{VM.auto_poweron ? 'Yes' : 'No'}}
dt(ng-if="(VM.$poolId | resolve).HA_enabled") HA
dd(ng-if="(VM.$poolId | resolve).HA_enabled")
span(
editable-checkbox="VM.high_availability"
e-name="high_availability"
e-form="vmSettings"
)
| {{VM.high_availability}}
dt vCPUs
dd
span(
editable-text="VM.CPUs.number"
e-name="CPUs"
e-form="vmSettings"
)
| {{VM.CPUs.number}}
dt RAM
dd
span(
editable-text="memorySize"
e-name="memory"
e-form="vmSettings"
)
| {{memorySize}}
dt UUID
dd {{VM.UUID}}
dt(ng-if="VM.PV_args") PV Args
dd(ng-if="VM.PV_args")
span(editable-text="VM.PV_args", e-name="PV_args", e-form="vmSettings")
| {{VM.PV_args}}
dt(ng-if="refreshStatControl.running && stats") Xen tools
dd(ng-if="refreshStatControl.running && stats")
span.text-success(ng-if="VM.xenTools === 'up to date'") Installed
span(ng-if="!VM.xenTools") NOT installed
span.text-warning(ng-if="VM.xenTools === 'out of date'") Outdated
dt(ng-if="refreshStatControl.running && stats && VM.os_version") OS
dd(ng-if="refreshStatControl.running && stats && VM.os_version")
| {{VM.os_version.name}} ({{VM.os_version.distro}})
dt(ng-if="refreshStatControl.running && stats && VM.os_version") Kernel
dd(ng-if="refreshStatControl.running && stats && VM.os_version")
| {{VM.os_version.uname}}
.btn-form(ng-show="vmSettings.$visible")
p.center
button.btn.btn-default(
type="button"
ng-disabled="vmSettings.$waiting"
ng-click="vmSettings.$cancel()"
)
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(
type="submit"
ng-disabled="vmSettings.$waiting"
)
i.fa.fa-save
| Save
.panel.panel-default.panel-height.center
.panel-heading.panel-title
i.xo-icon-stats(style="color: #e25440;", xo-click="refreshStats(VM.id)")
| Stats
.panel-body-stats(ng-if="refreshStatControl.running && stats")
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
p.stat-name
i.xp-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-full(
id="bigCpu"
data="stats.cpus"
labels="stats.date"
series="stats.cpuSeries"
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= Math.round(10*value)/10 %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= Math.round(10*value)/10 %>", pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.ramOnly", ng-click="statView.ramOnly = false")
p.stat-name
i.xo-icon-memory
//- i.fa.fa-bar-chart
//- i.fa.fa-tasks
//- i.fa.fa-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-full(
id="bigRam"
data="[stats.memoryUsed,stats.memory]"
labels="stats.date"
series="['Used RAM', 'Total RAM']"
colours="['#ff0000', '#ffbbbb']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.netOnly", ng-click="statView.netOnly = false")
p.stat-name
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-full(
id="bigNet"
data="stats.vifs"
labels="stats.date"
series="stats.vifSeries"
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.diskOnly", ng-click="statView.diskOnly = false")
p.stat-name
i.xo-icon-disk
| &nbsp; Disk I/O
canvas.chart.chart-line.chart-stat-full(
id="bigDisk"
data="stats.xvds"
labels="stats.date"
series="stats.xvdSeries"
colours="['#00dd00', '#77dd77', '#007700', '#33dd33', '#003300']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="!statView.netOnly && !statView.diskOnly && !statView.cpuOnly && !statView.ramOnly")
.row
.col-sm-6(ng-click="statView.cpuOnly=true")
p.stat-name
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-preview(
id="smallCpu"
data="stats.cpus"
labels="stats.date"
series="stats.cpuSeries"
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1}"
)
.col-sm-6(ng-click="statView.ramOnly=true")
p.stat-name
i.xo-icon-memory
//- i.fa.fa-bar-chart
//- i.fa.fa-tasks
//- i.fa.fa-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-preview(
id="smallRam"
data="[stats.memoryUsed,stats.memory]"
labels="stats.date"
series="['Used RAM', 'Total RAM']"
colours="['#ff0000', '#ffbbbb']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.row
.col-sm-6(ng-click="statView.netOnly=true")
p.stat-name
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-preview(
id="smallNet"
data="stats.vifs"
labels="stats.date"
series="stats.vifSeries"
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.col-sm-6(ng-click="statView.diskOnly=true")
p.stat-name
i.xo-icon-disk
| &nbsp; Disk I/O
canvas.chart.chart-line.chart-stat-preview(
id="smallDisk"
data="stats.xvds"
labels="stats.date"
series="stats.xvdSeries"
colours="['#00dd00', '#77dd77', '#007700', '#33dd33', '#003300']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.panel-body(ng-if="!refreshStatControl.running || !stats")
.grid
.grid-cell
p.stat-name vCPUs
p.center.big {{VM.CPUs.number}}
.grid-cell
p.stat-name RAM
p.center.big {{VM.memory.size | bytesToSize}}
.grid-cell
p.stat-name Disks
p.center.big {{VM.$VBDs.length || 0}}
br
p.center(ng-if="refreshStatControl.running")
i.xo-icon-loading
| &nbsp; Fetching stats...
.grid
.grid-cell(ng-if="VM.os_version.distro")
p.stat-name OS:
p.center.big
i(class="icon-{{VM.os_version.distro | osFamily}}",tooltip="{{VM.os_version.name}}", style="color: black;")
.grid-cell
p.stat-name Xen tools:
p.center
span(ng-if="VM.PV_drivers", style="color:green;") Installed
span(ng-if="!VM.PV_drivers") NOT installed
span(ng-if="VM.PV_drivers && !VM.PV_drivers_up_to_date") Outdated
//- Action panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body.text-center
.grid-sm.grid--gutters
.grid.grid-cell
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
button.btn(tooltip="Stop VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="stopVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-stop.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="VM.power_state == ('Running')")
button.btn(tooltip="Suspend VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="suspendVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-pause.fa-2x.fa-fw
.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 == ('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
.grid-cell.btn-group.dropdown(
ng-if="VM.power_state == ('Running' || 'Paused')"
dropdown
)
button.btn.disabled(
ng-if="canAdmin() && (hosts.all | count)==1"
tooltip = "No other host available"
style="width: 90%"
)
i.fa.fa-share.fa-2x.fa-fw
span.caret
button.btn.dropdown-toggle(
ng-if = "canAdmin() && (hosts.all | count)>1"
dropdown-toggle
tooltip="Migrate VM"
tooltip-placement="top"
type="button"
style="width: 90%"
)
i.fa.fa-share.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu", ng-if = 'canAdmin()')
li(ng-repeat="h in hosts.all | orderBy:natural('name_label') track by h.id" ng-if="h!=host")
a(xo-click="migrateVM(VM.id, h.id)")
i.xo-icon-host.fa-fw
| To {{h.name_label}}
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
button.btn(tooltip="Force Reboot", tooltip-placement="top", type="button", style="width: 90%", xo-click="force_rebootVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-flash.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
button.btn(tooltip="Delete VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="destroyVM(VM.id)", ng-if = 'canAdmin()')
i.fa.fa-trash-o.fa-2x.fa-fw
.grid-cell.btn-group.dropdown(
ng-if="VM.power_state == ('Halted')"
dropdown
)
button.btn.dropdown-toggle(
ng-if = 'canAdmin()'
dropdown-toggle
tooltip="Create a clone"
tooltip-placement="top"
style="width: 90%"
type="button"
)
i.fa.fa-files-o.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu")
li(ng-if = 'canAdmin()')
a(xo-click="cloneVM(VM.id,VM.name_label,false)")
i.fa.fa-code-fork.fa-fw
| Fast clone
li(ng-if = 'canAdmin()')
a(xo-click="cloneVM(VM.id,VM.name_label,true)")
i.xo-icon-disk.fa-fw
| Full disk copy
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
button.btn(tooltip="Convert to template", tooltip-placement="top", type="button", style="width: 90%", xo-click="convertVM(VM.id)", ng-if = 'canAdmin()')
i.fa.fa-thumb-tack.fa-2x.fa-fw
.grid.grid-cell
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
button.btn(tooltip="Force Shutdown", tooltip-placement="top", type="button", style="width: 90%", xo-click="force_stopVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-power-off.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="VM.power_state == ('Suspended')")
button.btn(tooltip="Resume VM", tooltip-placement="top", type="button", style="width: 90%", xo-click="resumeVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-play.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Create a snapshot", tooltip-placement="top", style="width: 90%", type="button", xo-click="snapshotVM(VM.id,VM.name_label)", ng-if = 'canAdmin()')
i.xo-icon-snapshot.fa-2x.fa-fw
.grid-cell.btn-group.dropdown(dropdown, ng-if="canAdmin()")
button.btn.dropdown-toggle(
dropdown-toggle
tooltip="Export the VM"
tooltip-placement="top"
style="width: 90%"
type="button"
)
i.fa.fa-download.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu")
li(ng-if = 'canAdmin()')
a(xo-click="exportVM(VM.id)")
i.fa.fa-download.fa-fw
| Full export
li(ng-if = 'canAdmin()')
a(xo-click="exportOnlyMetadataVM(VM.id)")
i.fa.fa-database.fa-fw
| Only Metadata
.grid-cell.btn-group.dropdown(dropdown, ng-if="canAdmin()")
button.btn.dropdown-toggle(
dropdown-toggle
tooltip="Copy the VM"
tooltip-placement="top"
style="width: 90%"
type="button"
)
i.fa.fa-clone.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu")
li(ng-repeat = 'SR in objects | selectHighLevel | filter:{type: "SR", content_type: "!iso"} | orderBy:"($container | resolve).name_label"')
a(xo-click = 'copyVM(VM.id, SR.id)') {{ SR.name_label}} ({{(SR.$container | resolve).name_label}})
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(tooltip="VM Console", tooltip-placement="top", type="button", style="width: 90%", xo-sref="consoles_view({id: VM.id})")
i.xo-icon-console.fa-2x.fa-fw
//- Docker Panel (if Docker VM)
.grid-sm(ng-if="VM.docker")
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-docker
| Docker containers
.panel-body
p.text-center(ng-if="!VM.docker.process.item") No Docker container on this VM.
table.table.table-hover(ng-if="VM.docker.process.item")
tr
th.col-sm-2 Name
th.col-sm-6 Command
th.col-sm-2 Created
th.col-sm-2 Status
tr(ng-repeat = "container in VM.docker.process.item")
td.oneliner {{container.entry.names}}
td.oneliner {{container.entry.command}}
td.oneliner {{container.entry.created*1e3 | date:"medium"}}
td(ng-if="container.entry.status === 'Up'")
span.label.label-success Running
span.pull-right.btn-group.quick-buttons
a(
tooltip="Stop this container"
xo-click="stopContainer(VM.id,container.entry.container)"
)
i.fa.fa-stop
a(
tooltip="Pause this container"
xo-click="pauseContainer(VM.id,container.entry.container)"
)
i.fa.fa-pause
a(
tooltip="Restart this container"
xo-click="restartContainer(VM.id,container.entry.container)"
)
i.fa.fa-refresh
td(ng-if="container.entry.status === 'Up (Paused)'")
span.label.label-info Paused
span.pull-right.btn-group.quick-buttons
a(
tooltip="Resume this container"
xo-click="resumeContainer(VM.id,container.entry.container)"
)
i.fa.fa-play
td(ng-if="container.entry.status !== 'Up' && container.entry.status !== 'Up (Paused)'")
span.label.label-danger Halted
span.pull-right.btn-group.quick-buttons
a(
tooltip="Start this container"
xo-click="startContainer(VM.id,container.entry.container)"
)
i.fa.fa-play
//- Disk panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-disk
| Disk
span.quick-edit(
ng-if="!disksForm.$visible && canOperate()"
tooltip="Edit disks"
ng-click="disksForm.$show()"
)
i.fa.fa-edit.fa-fw
span.quick-edit(
ng-if="disksForm.$visible"
tooltip="Cancel Edition"
ng-click="disksForm.$cancel()"
)
i.fa.fa-undo.fa-fw
.panel-body
form(name = "disksForm", editable-form = '', onbeforesave = 'saveDisks($data)')
table.table.table-hover
tr
th Name
th Description
th Tags
th Size
th SR
th Status
th(ng-show="disksForm.$visible")
//- FIXME: ng-init seems to disrupt the implicit $watch.
tr(ng-repeat = 'VDI in VDIs track by VDI.id')
td.oneliner
span(
editable-text="VDI.name_label"
e-name = '{{VDI.id}}/name_label'
)
| {{VDI.name_label}}
td.oneliner
span(
editable-text="VDI.name_description"
e-name = '{{VDI.id}}/name_description'
)
| {{VDI.name_description}}
td
xo-tag(object = 'VDI')
td
//- FIXME: should be editable, but the server needs first
//- to accept a human readable string.
| {{VDI.size | bytesToSize}}
td.oneliner
span(
ng-if = 'canView((VDI.$SR | resolve).id)'
editable-select="(VDI.$SR | resolve).id"
e-ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs"
e-name = '{{VDI.id}}/$SR'
)
//- Are SR editable? will trigger moving VDI to the new SR
a(xo-sref="SRs_view({id: (VDI.$SR | resolve).id})")
| {{(VDI.$SR | resolve).name_label}}
td(ng-if="isConnected(VDI)")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
a(
tooltip="Disconnect this disk"
xo-click="disconnectVBD(VDI)"
)
i.fa.fa-unlink.fa-lg
td(ng-if="!isConnected(VDI)")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
a(
tooltip="Plug this disk"
ng-if="VM.power_state == ('Running' || 'Paused')"
xo-click="connectVBD(VDI)"
)
i.fa.fa-plug.fa-lg
a(
tooltip="Forget this disk"
xo-click="deleteVBD(VDI)"
)
i.fa.fa-ban.fa-lg
a(
tooltip="Remove this disk"
xo-click="deleteDisk(VDI.id)"
)
i.fa.fa-trash-o.fa-lg
td(ng-show="disksForm.$visible")
.btn-group
button.btn.btn-default(
type="button"
ng-click="moveDisk($index, -1)"
ng-disabled="$first"
title="Move this disk up"
)
i.fa.fa-chevron-up
button.btn.btn-default(
type="button"
ng-click="moveDisk($index, 1)"
ng-disabled="$last"
title="Move this disk down"
)
i.fa.fa-chevron-down
//- TODO: Ability to create new VDIs.
.btn-form(ng-show="disksForm.$visible")
p.center
button.btn.btn-default(
type="reset"
ng-disabled="disksForm.$waiting"
ng-click="disksForm.$cancel()"
)
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(
type="submit"
ng-disabled="disksForm.$waiting"
)
i.fa.fa-save
| Save
.container-fluid
.row
.col-sm-5
iso-device(ng-if = '(VM && SRs) && canOperate()', vm = 'VM', srs = 'SRs')
.col-sm-7.text-right
div
button.btn(type="button", ng-class = '{"btn-success": adding, "btn-primary": !adding}', ng-disabled="disksForm.$waiting", ng-click="adding = !adding;creatingVdi = false;bootReordering = false", ng-hide = '!canAdmin()')
i.fa.fa-plus(ng-if = '!adding')
i.fa.fa-minus(ng-if = 'adding')
| Attach Disk
| &nbsp;
button.btn(type="button", ng-class = '{"btn-success": creatingVdi, "btn-primary": !creatingVdi}', ng-disabled="disksForm.$waiting", ng-click="creatingVdi = !creatingVdi;adding = false;bootReordering = false", ng-hide = '!canAdmin()')
i.fa.fa-plus(ng-if = '!creatingVdi')
i.fa.fa-minus(ng-if = 'creatingVdi')
| New Disk
| &nbsp;
button.btn(type="button", ng-class = '{"btn-success": bootReordering, "btn-primary": !bootReordering}', ng-disabled="disksForm.$waiting", ng-click="bootReordering = !bootReordering;adding = false;creatingVdi = false", ng-hide = '!canAdmin()')
i.fa.fa-plus(ng-if = '!bootReordering')
i.fa.fa-minus(ng-if = 'bootReordering')
| Boot order
br
.text-right
form.form-inline#addDiskForm(ng-if = 'adding', name = 'addForm', ng-submit = 'addVdi(vdiToAdd.vdi, vdiReadOnly, vdiBootable)')
fieldset(ng-attr-disabled = '{{ addWaiting ? true : undefined }}')
.form-group
select#vdiToAdd.form-control(ng-model = 'vdiToAdd', ng-options = 'vdi.label group by vdi.sr for vdi in VDIOpts', required)
option(value = '', disabled) -- Choose disk --
| &nbsp;
.form-group(ng-if = 'vdiToAdd')
//- .form-group
label(for = 'vdiPosition') Position&nbsp;
select#vdiPosition.form-control(ng-model = '$parent.vdiPos', ng-options = 'vPos for vPos in vdiFreePos', required)
option(value = '', disabled) --
| &nbsp;
label
input(type='checkbox', ng-model = '$parent.$parent.vdiBootable')
| &nbsp;Bootable&nbsp;
label
input(ng-if = '!isFreeForWriting(vdiToAdd.vdi)', type='checkbox', ng-model = '$parent.$parent.vdiReadOnly', ng-checked = 'true', ng-disabled = 'true')
input(ng-if = 'isFreeForWriting(vdiToAdd.vdi)', type='checkbox', ng-model = '$parent.$parent.vdiReadOnly')
| &nbsp;Read-only&nbsp;
.form-group(ng-if = 'vdiToAdd')
button.btn.btn-primary(type = 'submit', ng-disabled="disksForm.$waiting", ng-if = 'canAdmin()')
| Add
span(ng-if = 'addWaiting')
| &nbsp;
i.xo-icon-loading-sm
br
form#createDiskForm(ng-if = 'creatingVdi', name = 'createForm', ng-submit = 'createVdi(newDiskName, newDiskSize, newDiskSR, newDiskBootable, newDiskReadonly)')
fieldset(ng-attr-disabled = '{{ createVdiWaiting ? true : undefined }}')
.form-inline
.form-group
//- label(for = 'newDiskName') Name&nbsp;
input#newDiskName.form-control(type = 'text', ng-model = 'newDiskName', placeholder = 'Disk Name', required)
| &nbsp;
.form-group
//- label(for = 'newDiskSize') Size&nbsp;
input#newDiskSize.form-control(type = 'text', ng-model = 'newDiskSize', required, placeholder = 'Size e.g 128MB, 8GB, 2TB...')
| &nbsp;
.form-group
//- label(for = 'newDiskSR') SR&nbsp;
select.form-control(ng-model = 'newDiskSR', required, ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs")
option(value = '', disabled) Choose your SR
| &nbsp;
br
.form-group
label
input(type='checkbox', ng-model = 'newDiskBootable')
| &nbsp;Bootable&nbsp;
label
input(type='checkbox', ng-model = 'newDiskReadonly')
| &nbsp;Read-only&nbsp;
//- .form-group
label(for = 'diskPosition') Position&nbsp;
select#diskPosition.form-control(ng-model = 'newDiskPosition', ng-options = 'vPos for vPos in vdiFreePos', required)
option(value = '', disabled) --
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit', ng-disabled="disksForm.$waiting", ng-if = 'canAdmin()')
i.fa.fa-plus-square
| &nbsp;Create
span(ng-if = 'createVdiWaiting')
| &nbsp;
i.xo-icon-loading-sm
br
form#bootOrderForm(ng-if = 'bootReordering', ng-submit = 'saveBootParams(VM.id, bootParams)')
fieldset(ng-disabled = 'savingBootOrder')
.form-group(ng-repeat = 'elem in bootParams')
label
span(ng-class = '{"text-muted": !elem.v}') {{ elem.t }}&nbsp;
input(type = 'checkbox', ng-model = 'elem.v')
| &nbsp;
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, -1)', ng-disabled = '$first'): i.fa.fa-chevron-up
| &nbsp;
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, 1)', ng-disabled = '$last'): i.fa.fa-chevron-down
.form-group
button.btn.btn-primary(type = 'submit', ng-disabled = 'disksForm.$waiting', ng-if = 'canAdmin()')
i.fa.fa-database
| &nbsp;Save
//- TODO: add interface in this panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| Interface
.panel-body
table.table.table-hover
th Device
th MAC
th MTU
th Network
th Link status
tr(ng-repeat="VIF in VM.VIFs | resolve | orderBy:natural('device') track by VIF.id")
td VIF \#{{VIF.device}}
td
| {{VIF.MAC}}
td
| {{VIF.MTU}}
td.oneliner
span(ng-if = 'canView((VIF.$network | resolve).id)') {{(VIF.$network | resolve).name_label}}
td(ng-if="VIF.attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this interface", ng-if="VM.power_state == ('Running' || 'Paused') && canAdmin()", xo-click="disconnectVIF(VIF.id)")
i.fa.fa-unlink.fa-lg
td(ng-if="!VIF.attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Connect this interface", xo-click="connectVIF(VIF.id)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
a(tooltip="Remove this interface", xo-click="deleteVIF(VIF.id)", ng-if = 'canAdmin()')
i.fa.fa-trash-o.fa-lg
.text-right
button.btn(type="button", ng-class = '{"btn-success": creatingVif, "btn-primary": !creatingVif}', ng-click="creatingVif = !creatingVif", ng-hide = '!canAdmin()')
i.fa.fa-plus(ng-if = '!creatingVif')
i.fa.fa-minus(ng-if = 'creatingVif')
| Create Interface
br
form.form-inline.text-right#createInterfaceForm(
ng-if = 'creatingVif'
name = 'createInterfaceForm'
ng-submit = 'createInterface(newInterfaceNetwork, newInterfaceMTU, autoMac, newInterfaceMAC)'
)
fieldset(ng-disabled = 'createVifWaiting')
.form-group
label(for = 'newVifNetwork') Network&nbsp;
select.form-control(
ng-options='network.name_label for network in networks | xoHideUnauthorized'
ng-model = 'newInterfaceNetwork'
ng-change = 'updateMTU(newInterfaceNetwork)'
required
)
option(value = '', disabled) --
| &nbsp;
.form-group
fieldset(ng-disabled = 'autoMac')
label.control-label(for = 'newInterfaceMAC') MAC address&nbsp;
input#newInterfaceMAC.form-control(ng-class = '{hidden: autoMac}', type = 'text', ng-model = 'newInterfaceMAC', ng-required = '!autoMac')
| &nbsp;
.checkbox
label
input(type='checkbox', ng-model = 'autoMac')
| &nbsp;Auto-generate&nbsp;
| &nbsp;
.form-group
label(for = 'newInterfaceMTU') MTU&nbsp;
input#newInterfaceMTU.form-control(type = 'text', ng-model = 'newInterfaceMTU', required)
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit', ng-if = 'canAdmin()')
i.fa.fa-plus-square
| &nbsp;Create
span(ng-if = 'createVifWaiting')
| &nbsp;
i.xo-icon-loading-sm
br
//- Snap panel
.grid-sm
form(editable-form="", name="vmSnap", oncancel="cancel()").panel.panel-default
.panel-heading.panel-title
i.xo-icon-snapshot
| Snapshots
span.quick-edit(tooltip="Edit snapshots", ng-click="vmSnap.$show()", ng-if="!vmSnap.$visible && canAdmin()")
i.fa.fa-edit.fa-fw
span.quick-edit(ng-if="vmSnap.$visible", tooltip="Cancel Edition", ng-click="vmSnap.$cancel()")
i.fa.fa-undo.fa-fw
.panel-body
p.center(ng-if="!VM.snapshots.length") No snapshots
table.table.table-hover(ng-if="VM.snapshots.length")
th Date
th 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.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
a(tooltip="Revert VM to this snapshot", xo-click="revertSnapshot(snapshot.id)", ng-if = 'canAdmin()')
i.fa.fa-undo.fa-lg
a(tooltip="Remove this snapshot", xo-click="deleteSnapshot(snapshot.id)", ng-if = 'canAdmin()')
i.fa.fa-trash-o.fa-lg
.center(ng-if = '(VM.snapshots | count) > 5 || currentSnapPage > 1')
pagination(boundary-links="true", total-items="VM.snapshots | count", ng-model="$parent.currentSnapPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.btn-form(ng-show="vmSnap.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="vmSnap.$waiting", ng-click="vmSnap.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="vmSnap.$waiting", ng-click="saveChanges()")
i.fa.fa-save
| Save
//- Logs panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="(VM.messages | isNotEmpty) && canAdmin()", tooltip="Remove all logs", xo-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
.panel-body
p.center(ng-if="VM.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="VM.messages | isNotEmpty")
th Date
th Name
tr(ng-repeat="message in VM.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td.oneliner {{message.time*1e3 | date:"medium"}}
td.oneliner
| {{message.name}}
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 = '(VM.messages | count) > 5 || currentLogPage > 1')
pagination(boundary-links="true", total-items="VM.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid-sm(ng-if="canAdmin()")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-plug
| PCI Devices
.panel-body
p.center(ng-if="!(VM.$container | resolve).$PCIs") No PCI devices available
table.table.table-hover(ng-if="(VM.$container | resolve).$PCIs")
th PCI Info
th Device Name
th Status
tr(ng-repeat="pci in ((VM.$container | resolve).$PCIs | resolve) | orderBy:'pci_id' | slice:(5*(currentPCIPage-1)):(5*currentPCIPage) track by pci.id")
td.oneliner {{pci.pci_id}} ({{pci.class_name}})
td.oneliner {{pci.device_name}}
td(ng-if="pci.pci_id === VM.other.pci")
span.label.label-success Attached
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this PCI device", xo-click="disconnectPci(VM.id)", ng-if = 'canAdmin()')
i.fa.fa-unlink.fa-lg
td(ng-if="pci.pci_id !== VM.other.pci")
span.label.label-default Not attached
span.pull-right.btn-group.quick-buttons
a(tooltip="Connect this PCI device", xo-click="connectPci(VM.id, pci.pci_id)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
.center(ng-if = '((VM.$container | resolve).$PCIs | resolve).length > 5 || currentPCIPage > 1')
pagination(boundary-links="true", total-items="((VM.$container | resolve).$PCIs | resolve).length", ng-model="$parent.currentPCIPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-desktop
| vGPUs
.panel-body
p.center(ng-if="!VM.$VGPus") No vGPUs available
table.table.table-hover(ng-if="VM.$VGPus")
th Device
th Status
tr(ng-repeat="vgpu in VM.$VGPUs | resolve | orderBy:'device' | slice:(5*(currentGPUPage-1)):(5*currentGPUPage) track by vgpu.id")
td.oneliner {{vgpu.device}}
td(ng-if="vgu.currentlyAttached")
span.label.label-success Attached
td(ng-if="!vgu.currentlyAttached")
span.label.label-default Not attached
.center(ng-if = '(VM.$VGPUs | resolve).length > 5 || currentGPUPage > 1')
pagination(boundary-links="true", total-items="(VM.$VGPUs | resolve).length", ng-model="$parent.currentGPUPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")

View File

@ -1,130 +0,0 @@
import angular from 'angular'
import {
format as formatUrl,
parse as parseUrl,
resolve as resolveUrl
} from 'url'
import {RFB} from 'novnc-node'
import view from './view'
// ===================================================================
function parseRelativeUrl (url) {
/* global window: false */
return parseUrl(resolveUrl(String(window.location), url))
}
const PROTOCOL_ALIASES = {
'http:': 'ws:',
'https:': 'wss:'
}
function fixProtocol (url) {
let protocol = PROTOCOL_ALIASES[url.protocol]
if (protocol) {
url.protocol = protocol
}
}
// ===================================================================
export default angular.module('no-vnc', [])
.controller('NoVncCtrl', function ($attrs, $element, $scope, $timeout) {
this.height = 480
$attrs.$observe('height', (height) => {
this.height = height
})
this.width = 640
$attrs.$observe('width', (width) => {
this.width = width
})
const retrySteps = {
0: 1000,
1: 3000,
2: 5000,
3: 5000
}
let retryStep = 0
let rfb
let reconnectTry
function clean () {
// If there was a previous connection.
if (rfb) {
rfb._onUpdateState = () => {}
rfb.disconnect()
rfb = undefined
}
}
function reset (url) {
// Remove previous connection.
clean()
// If the URL is empty, stop now.
if (!url) {
return
}
// Parse the URL.
url = parseRelativeUrl(url)
fixProtocol(url)
let isSecure = url.protocol === 'wss:'
rfb = new RFB({
encrypt: isSecure,
target: canvas,
wsProtocols: ['chat']
})
rfb._onUpdateState = (rfb, state) => {
if (state === 'normal') {
retryStep = 0
if (reconnectTry) {
$timeout.cancel(reconnectTry)
reconnectTry = undefined
}
}
if (state !== 'disconnected') {
return
}
if (retryStep in retrySteps) {
reconnectTry = $timeout(() => reset(url), retrySteps[retryStep++])
}
}
// Connect.
rfb.connect(formatUrl(url))
}
this.remoteControl = {
sendCtrlAltDel () {
if (rfb) {
rfb.sendCtrlAltDel()
}
}
}
let canvas = $element.find('canvas')[0]
$attrs.$observe('url', (url) => {
reset(url)
})
$scope.$on('$destroy', clean)
})
.directive('noVnc', function () {
return {
bindToController: true,
controller: 'NoVncCtrl as noVnc',
restrict: 'E',
scope: {
remoteControl: '='
},
template: view
}
})
.name

View File

@ -1,9 +0,0 @@
{
"private": true,
"browserify": {
"transform": [
"babelify",
"browserify-plain-jade"
]
}
}

View File

@ -1,5 +0,0 @@
canvas.center-block(
height = "{{noVnc.height}}"
width = "{{noVnc.width}}"
)
| Sorry, your browser does not support the canvas element.

76
app/node_modules/iso-device/index.js generated vendored
View File

@ -1,76 +0,0 @@
import angular from 'angular'
import view from './view'
// =====================================================================
export default angular.module('xoWebApp.isoDevice', [])
.directive('isoDevice', () => ({
restrict: 'E',
template: view,
scope: {
srs: '=',
vm: '='
},
controller: 'IsoDevice as isoDevice',
bindToController: true
}))
.controller('IsoDevice', function ($scope, xo, xoApi) {
const {get} = xoApi
const descriptor = obj => obj.name_label + (obj.name_description.length ? (' - ' + obj.name_description) : '')
this.eject = VM => xo.vm.ejectCd(VM.id)
this.insert = (VM, disc_id) => xo.vm.insertCd(VM.id, disc_id, true)
const prepareDiskData = (srs, vbds) => {
const ISOOpts = []
for (let key in srs) {
const SR = srs[key]
if (SR.SR_type === 'iso') {
for (let key in SR.VDIs) {
const rIso = SR.VDIs[key]
const oIso = get(rIso)
ISOOpts.push({
sr: SR.name_label,
label: descriptor(oIso),
iso: oIso
})
}
}
}
let mounted = ''
for (let key in vbds) {
const VBD = vbds[key]
const oVbd = get(VBD)
if (oVbd && oVbd.is_cd_drive) {
const oVdi = get(oVbd.VDI)
oVdi && (mounted = oVdi.id)
}
}
return {
opts: ISOOpts,
mounted
}
}
$scope.$watchCollection(() => this.srs, srs => this.isos = prepareDiskData(srs, this.vm.$VBDs))
$scope.$watch(() => this.vm && this.vm.$VBDs, vbds => this.isos = prepareDiskData(this.srs, vbds))
})
.directive('unfocusOnChange', () => {
return {
restrict: 'A',
require: '^ngModel',
link ($scope, $elem, $attrs, modelCtrl) {
const elem = $elem[0]
modelCtrl.$viewChangeListeners.push(() => {
elem.blur()
})
}
}
})
// A module exports its name.
.name

View File

@ -1,9 +0,0 @@
{
"private": true,
"browserify": {
"transform": [
"babelify",
"browserify-plain-jade"
]
}
}

View File

@ -1,16 +0,0 @@
.form-inline
.form-group
.input-group
select.form-control(
ng-model = 'isoDevice.isos.mounted'
ng-change = 'isoDevice.insert(isoDevice.vm, isoDevice.isos.mounted)'
ng-options = 'iso.iso.id as iso.label group by iso.sr for iso in isoDevice.isos.opts'
unfocus-on-change
)
option(value = '', disabled) -- CD Drive (empty) --
.input-group-btn
button.btn.btn-default(
ng-click = 'isoDevice.eject(isoDevice.vm)'
ng-disabled = '!isoDevice.isos.mounted'
)
i.fa.fa-eject

View File

@ -1,36 +0,0 @@
import angular from 'angular'
import angularUiRouter from 'angular-ui-router'
import masterSelect from './'
import view from './view'
// -------------------------------------------------------------------
class MasterSelectDemoCtrl {
constructor () {
this.master = null
const items = this.items = new Array(10)
for (let i = 0, n = items.length; i < n; ++i) {
items[i] = {
label: `Item ${i}`,
selected: (Math.random() < 0.5)
}
}
}
}
export default angular.module('xoWebApp.foo', [
angularUiRouter,
masterSelect
])
.config($stateProvider => {
$stateProvider.state('master-select-demo', {
url: '/master-select-demo',
controller: 'MasterSelectDemoCtrl as demo',
template: view
})
})
.controller('MasterSelectDemoCtrl', MasterSelectDemoCtrl)
.name

View File

@ -1,87 +0,0 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
// -------------------------------------------------------------------
const defaultIsSelected = (object) => object != null && object.selected
const defaultSelect = (object) => {
if (object != null) {
object.selected = true
}
}
const defaultUnselect = (object) => {
if (object != null) {
object.selected = false
}
}
export default angular.module('master-select', [])
.directive('masterSelect', function () {
return {
link ($scope, $elem, $attrs) {
const isSelected = defaultIsSelected
const select = defaultSelect
const unselect = defaultUnselect
let internalChange = false
$scope.$watch('items', items => {
if (internalChange) {
internalChange = false
return
}
const selected = $scope.selectedItems = Object.create(null)
let nAll = 0
let nSelected = 0
forEach(items, (item, key) => {
++nAll
if (isSelected(item, key, items)) {
selected[key] = item
++nSelected
}
})
const indeterminate = nSelected && nSelected !== nAll
const previousNgModel = $scope.ngModel
$scope.ngModel = indeterminate || Boolean(nSelected)
if (previousNgModel !== $scope.ngModel) {
internalChange = true
}
$elem.prop('indeterminate', indeterminate)
}, true)
$scope.$watch('ngModel', selected => {
if (internalChange) {
internalChange = false
return
}
internalChange = true
$elem.prop('indeterminate', false)
const {items} = $scope
const selectedItems = $scope.selectedItems = Object.create(null)
forEach(items, selected
? (item, key) => {
select(item, key, items)
selectedItems[key] = item
}
: (item, key) => {
unselect(item, key, items)
}
)
})
},
restrict: 'A',
scope: {
items: '=masterSelect',
ngModel: '=',
selectedItems: '=?'
}
}
})
.name

Some files were not shown because too many files have changed in this diff Show More