diff --git a/serverside/jsmod/6.0-4/charts.js b/serverside/jsmod/6.0-4/charts.js new file mode 100644 index 0000000..713bec3 --- /dev/null +++ b/serverside/jsmod/6.0-4/charts.js @@ -0,0 +1,22013 @@ +Ext.define("Ext.draw.ContainerBase", { + extend: "Ext.panel.Panel", + requires: ["Ext.window.Window"], + previewTitleText: "Chart Preview", + previewAltText: "Chart preview", + layout: "container", + addElementListener: function() { + var b = this, + a = arguments; + if (b.rendered) { + b.el.on.apply(b.el, a) + } else { + b.on("render", function() { + b.el.on.apply(b.el, a) + }) + } + }, + removeElementListener: function() { + var b = this, + a = arguments; + if (b.rendered) { + b.el.un.apply(b.el, a) + } + }, + afterRender: function() { + this.callParent(arguments); + this.initAnimator() + }, + getItems: function() { + var b = this, + a = b.items; + if (!a || !a.isMixedCollection) { + b.initItems() + } + return b.items + }, + onRender: function() { + this.callParent(arguments); + this.element = this.el; + this.innerElement = this.body + }, + setItems: function(a) { + this.items = a; + return a + }, + setSurfaceSize: function(b, a) { + this.resizeHandler({ + width: b, + height: a + }); + this.renderFrame() + }, + onResize: function(c, a, b, e) { + var d = this; + d.callParent([c, a, b, e]); + d.setBodySize({ + width: c, + height: a + }) + }, + preview: function() { + var a = this.getImage(); + new Ext.window.Window({ + title: this.previewTitleText, + closeable: true, + renderTo: Ext.getBody(), + autoShow: true, + maximizeable: true, + maximized: true, + border: true, + layout: { + type: "hbox", + pack: "center", + align: "middle" + }, + items: { + xtype: "container", + items: { + xtype: "image", + mode: "img", + cls: Ext.baseCSSPrefix + "chart-image", + alt: this.previewAltText, + src: a.data, + listeners: { + afterrender: function() { + var e = this, + b = e.imgEl.dom, + d = a.type === "svg" ? 1 : (window.devicePixelRatio || 1), + c; + if (!b.naturalWidth || !b.naturalHeight) { + b.onload = function() { + var g = b.naturalWidth, + f = b.naturalHeight; + e.setWidth(Math.floor(g / d)); + e.setHeight(Math.floor(f / d)) + } + } else { + c = e.getSize(); + e.setWidth(Math.floor(c.width / d)); + e.setHeight(Math.floor(c.height / d)) + } + } + } + } + } + }) + }, + privates: { + getTargetEl: function() { + return this.innerElement + }, + reattachToBody: function() { + var a = this; + if (a.pendingDetachSize) { + a.onBodyResize() + } + a.pendingDetachSize = false; + a.callParent() + } + } +}); +Ext.define("Ext.draw.SurfaceBase", { + extend: "Ext.Widget", + getOwnerBody: function() { + return this.ownerCt.body + }, + destroy: function() { + var a = this; + if (a.hasListeners.destroy) { + a.fireEvent("destroy", a) + } + a.callParent() + } +}); +Ext.define("Ext.draw.Color", { + statics: { + colorToHexRe: /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/, + rgbToHexRe: /\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)/, + rgbaToHexRe: /\s*rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\.\d]+)\)/, + hexRe: /\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/, + NONE: "none", + RGBA_NONE: "rgba(0, 0, 0, 0)" + }, + isColor: true, + lightnessFactor: 0.2, + constructor: function(d, b, a, c) { + this.setRGB(d, b, a, c) + }, + setRGB: function(e, c, a, d) { + var b = this; + b.r = Math.min(255, Math.max(0, e)); + b.g = Math.min(255, Math.max(0, c)); + b.b = Math.min(255, Math.max(0, a)); + if (d === undefined) { + b.a = 1 + } else { + b.a = Math.min(1, Math.max(0, d)) + } + }, + getGrayscale: function() { + return this.r * 0.3 + this.g * 0.59 + this.b * 0.11 + }, + getHSL: function() { + var i = this, + a = i.r / 255, + f = i.g / 255, + j = i.b / 255, + k = Math.max(a, f, j), + d = Math.min(a, f, j), + m = k - d, + e, n = 0, + c = 0.5 * (k + d); + if (d !== k) { + n = (c <= 0.5) ? m / (k + d) : m / (2 - k - d); + if (a === k) { + e = 60 * (f - j) / m + } else { + if (f === k) { + e = 120 + 60 * (j - a) / m + } else { + e = 240 + 60 * (a - f) / m + } + } + if (e < 0) { + e += 360 + } + if (e >= 360) { + e -= 360 + } + } + return [e, n, c] + }, + getHSV: function() { + var i = this, + a = i.r / 255, + f = i.g / 255, + j = i.b / 255, + k = Math.max(a, f, j), + d = Math.min(a, f, j), + c = k - d, + e, m = 0, + l = k; + if (d != k) { + m = l ? c / l : 0; + if (a === k) { + e = 60 * (f - j) / c + } else { + if (f === k) { + e = 60 * (j - a) / c + 120 + } else { + e = 60 * (a - f) / c + 240 + } + } + if (e < 0) { + e += 360 + } + if (e >= 360) { + e -= 360 + } + } + return [e, m, l] + }, + setHSL: function(g, f, e) { + var i = this, + d = Math.abs, + j, b, a; + g = (g % 360 + 360) % 360; + f = f > 1 ? 1 : f < 0 ? 0 : f; + e = e > 1 ? 1 : e < 0 ? 0 : e; + if (f === 0 || g === null) { + e *= 255; + i.setRGB(e, e, e) + } else { + g /= 60; + j = f * (1 - d(2 * e - 1)); + b = j * (1 - d(g % 2 - 1)); + a = e - j / 2; + a *= 255; + j *= 255; + b *= 255; + switch (Math.floor(g)) { + case 0: + i.setRGB(j + a, b + a, a); + break; + case 1: + i.setRGB(b + a, j + a, a); + break; + case 2: + i.setRGB(a, j + a, b + a); + break; + case 3: + i.setRGB(a, b + a, j + a); + break; + case 4: + i.setRGB(b + a, a, j + a); + break; + case 5: + i.setRGB(j + a, a, b + a); + break + } + } + return i + }, + setHSV: function(f, e, d) { + var g = this, + i, b, a; + f = (f % 360 + 360) % 360; + e = e > 1 ? 1 : e < 0 ? 0 : e; + d = d > 1 ? 1 : d < 0 ? 0 : d; + if (e === 0 || f === null) { + d *= 255; + g.setRGB(d, d, d) + } else { + f /= 60; + i = d * e; + b = i * (1 - Math.abs(f % 2 - 1)); + a = d - i; + a *= 255; + i *= 255; + b *= 255; + switch (Math.floor(f)) { + case 0: + g.setRGB(i + a, b + a, a); + break; + case 1: + g.setRGB(b + a, i + a, a); + break; + case 2: + g.setRGB(a, i + a, b + a); + break; + case 3: + g.setRGB(a, b + a, i + a); + break; + case 4: + g.setRGB(b + a, a, i + a); + break; + case 5: + g.setRGB(i + a, a, b + a); + break + } + } + return g + }, + createLighter: function(b) { + if (!b && b !== 0) { + b = this.lightnessFactor + } + var a = this.getHSL(); + a[2] = Ext.Number.constrain(a[2] + b, 0, 1); + return Ext.draw.Color.fromHSL(a[0], a[1], a[2]) + }, + createDarker: function(a) { + if (!a && a !== 0) { + a = this.lightnessFactor + } + return this.createLighter(-a) + }, + toString: function() { + var f = this, + c = Math.round; + if (f.a === 1) { + var e = c(f.r).toString(16), + d = c(f.g).toString(16), + a = c(f.b).toString(16); + e = (e.length === 1) ? "0" + e : e; + d = (d.length === 1) ? "0" + d : d; + a = (a.length === 1) ? "0" + a : a; + return ["#", e, d, a].join("") + } else { + return "rgba(" + [c(f.r), c(f.g), c(f.b), f.a === 0 ? 0 : f.a.toFixed(15)].join(", ") + ")" + } + }, + toHex: function(b) { + if (Ext.isArray(b)) { + b = b[0] + } + if (!Ext.isString(b)) { + return "" + } + if (b.substr(0, 1) === "#") { + return b + } + var e = Ext.draw.Color.colorToHexRe.exec(b); + if (Ext.isArray(e)) { + var f = parseInt(e[2], 10), + d = parseInt(e[3], 10), + a = parseInt(e[4], 10), + c = a | (d << 8) | (f << 16); + return e[1] + "#" + ("000000" + c.toString(16)).slice(-6) + } else { + return "" + } + }, + setFromString: function(j) { + var e, h, f, c, d = 1, + i = parseInt; + if (j === Ext.draw.Color.NONE) { + this.r = this.g = this.b = this.a = 0; + return this + } + if ((j.length === 4 || j.length === 7) && j.substr(0, 1) === "#") { + e = j.match(Ext.draw.Color.hexRe); + if (e) { + h = i(e[1], 16) >> 0; + f = i(e[2], 16) >> 0; + c = i(e[3], 16) >> 0; + if (j.length === 4) { + h += (h * 16); + f += (f * 16); + c += (c * 16) + } + } + } else { + if ((e = j.match(Ext.draw.Color.rgbToHexRe))) { + h = +e[1]; + f = +e[2]; + c = +e[3] + } else { + if ((e = j.match(Ext.draw.Color.rgbaToHexRe))) { + h = +e[1]; + f = +e[2]; + c = +e[3]; + d = +e[4] + } else { + if (Ext.draw.Color.ColorList.hasOwnProperty(j.toLowerCase())) { + return this.setFromString(Ext.draw.Color.ColorList[j.toLowerCase()]) + } + } + } + } + if (typeof h === "undefined") { + return this + } + this.r = h; + this.g = f; + this.b = c; + this.a = d; + return this + } +}, function() { + var a = new this(); + this.addStatics({ + fly: function(f, e, c, d) { + switch (arguments.length) { + case 1: + a.setFromString(f); + break; + case 3: + case 4: + a.setRGB(f, e, c, d); + break; + default: + return null + } + return a + }, + ColorList: { + aliceblue: "#f0f8ff", + antiquewhite: "#faebd7", + aqua: "#00ffff", + aquamarine: "#7fffd4", + azure: "#f0ffff", + beige: "#f5f5dc", + bisque: "#ffe4c4", + black: "#000000", + blanchedalmond: "#ffebcd", + blue: "#0000ff", + blueviolet: "#8a2be2", + brown: "#a52a2a", + burlywood: "#deb887", + cadetblue: "#5f9ea0", + chartreuse: "#7fff00", + chocolate: "#d2691e", + coral: "#ff7f50", + cornflowerblue: "#6495ed", + cornsilk: "#fff8dc", + crimson: "#dc143c", + cyan: "#00ffff", + darkblue: "#00008b", + darkcyan: "#008b8b", + darkgoldenrod: "#b8860b", + darkgray: "#a9a9a9", + darkgreen: "#006400", + darkkhaki: "#bdb76b", + darkmagenta: "#8b008b", + darkolivegreen: "#556b2f", + darkorange: "#ff8c00", + darkorchid: "#9932cc", + darkred: "#8b0000", + darksalmon: "#e9967a", + darkseagreen: "#8fbc8f", + darkslateblue: "#483d8b", + darkslategray: "#2f4f4f", + darkturquoise: "#00ced1", + darkviolet: "#9400d3", + deeppink: "#ff1493", + deepskyblue: "#00bfff", + dimgray: "#696969", + dodgerblue: "#1e90ff", + firebrick: "#b22222", + floralwhite: "#fffaf0", + forestgreen: "#228b22", + fuchsia: "#ff00ff", + gainsboro: "#dcdcdc", + ghostwhite: "#f8f8ff", + gold: "#ffd700", + goldenrod: "#daa520", + gray: "#808080", + green: "#008000", + greenyellow: "#adff2f", + honeydew: "#f0fff0", + hotpink: "#ff69b4", + indianred: "#cd5c5c", + indigo: "#4b0082", + ivory: "#fffff0", + khaki: "#f0e68c", + lavender: "#e6e6fa", + lavenderblush: "#fff0f5", + lawngreen: "#7cfc00", + lemonchiffon: "#fffacd", + lightblue: "#add8e6", + lightcoral: "#f08080", + lightcyan: "#e0ffff", + lightgoldenrodyellow: "#fafad2", + lightgray: "#d3d3d3", + lightgrey: "#d3d3d3", + lightgreen: "#90ee90", + lightpink: "#ffb6c1", + lightsalmon: "#ffa07a", + lightseagreen: "#20b2aa", + lightskyblue: "#87cefa", + lightslategray: "#778899", + lightsteelblue: "#b0c4de", + lightyellow: "#ffffe0", + lime: "#00ff00", + limegreen: "#32cd32", + linen: "#faf0e6", + magenta: "#ff00ff", + maroon: "#800000", + mediumaquamarine: "#66cdaa", + mediumblue: "#0000cd", + mediumorchid: "#ba55d3", + mediumpurple: "#9370d8", + mediumseagreen: "#3cb371", + mediumslateblue: "#7b68ee", + mediumspringgreen: "#00fa9a", + mediumturquoise: "#48d1cc", + mediumvioletred: "#c71585", + midnightblue: "#191970", + mintcream: "#f5fffa", + mistyrose: "#ffe4e1", + moccasin: "#ffe4b5", + navajowhite: "#ffdead", + navy: "#000080", + oldlace: "#fdf5e6", + olive: "#808000", + olivedrab: "#6b8e23", + orange: "#ffa500", + orangered: "#ff4500", + orchid: "#da70d6", + palegoldenrod: "#eee8aa", + palegreen: "#98fb98", + paleturquoise: "#afeeee", + palevioletred: "#d87093", + papayawhip: "#ffefd5", + peachpuff: "#ffdab9", + peru: "#cd853f", + pink: "#ffc0cb", + plum: "#dda0dd", + powderblue: "#b0e0e6", + purple: "#800080", + red: "#ff0000", + rosybrown: "#bc8f8f", + royalblue: "#4169e1", + saddlebrown: "#8b4513", + salmon: "#fa8072", + sandybrown: "#f4a460", + seagreen: "#2e8b57", + seashell: "#fff5ee", + sienna: "#a0522d", + silver: "#c0c0c0", + skyblue: "#87ceeb", + slateblue: "#6a5acd", + slategray: "#708090", + snow: "#fffafa", + springgreen: "#00ff7f", + steelblue: "#4682b4", + tan: "#d2b48c", + teal: "#008080", + thistle: "#d8bfd8", + tomato: "#ff6347", + turquoise: "#40e0d0", + violet: "#ee82ee", + wheat: "#f5deb3", + white: "#ffffff", + whitesmoke: "#f5f5f5", + yellow: "#ffff00", + yellowgreen: "#9acd32" + }, + fromHSL: function(d, c, b) { + return (new this(0, 0, 0, 0)).setHSL(d, c, b) + }, + fromHSV: function(d, c, b) { + return (new this(0, 0, 0, 0)).setHSL(d, c, b) + }, + fromString: function(b) { + return (new this(0, 0, 0, 0)).setFromString(b) + }, + create: function(b) { + if (b instanceof this) { + return b + } else { + if (Ext.isArray(b)) { + return new Ext.draw.Color(b[0], b[1], b[2], b[3]) + } else { + if (Ext.isString(b)) { + return Ext.draw.Color.fromString(b) + } else { + if (arguments.length > 2) { + return new Ext.draw.Color(arguments[0], arguments[1], arguments[2], arguments[3]) + } else { + return new Ext.draw.Color(0, 0, 0, 0) + } + } + } + } + } + }) +}); +Ext.define("Ext.draw.sprite.AnimationParser", function() { + function a(d, c, b) { + return d + (c - d) * b + } + return { + singleton: true, + attributeRe: /^url\(#([a-zA-Z\-]+)\)$/, + requires: ["Ext.draw.Color"], + color: { + parseInitial: function(c, b) { + if (Ext.isString(c)) { + c = Ext.draw.Color.create(c) + } + if (Ext.isString(b)) { + b = Ext.draw.Color.create(b) + } + if ((c instanceof Ext.draw.Color) && (b instanceof Ext.draw.Color)) { + return [ + [c.r, c.g, c.b, c.a], + [b.r, b.g, b.b, b.a] + ] + } else { + return [c || b, b || c] + } + }, + compute: function(d, c, b) { + if (!Ext.isArray(d) || !Ext.isArray(c)) { + return c || d + } else { + return [a(d[0], c[0], b), a(d[1], c[1], b), a(d[2], c[2], b), a(d[3], c[3], b)] + } + }, + serve: function(c) { + var b = Ext.draw.Color.fly(c[0], c[1], c[2], c[3]); + return b.toString() + } + }, + number: { + parse: function(b) { + return b === null ? null : +b + }, + compute: function(d, c, b) { + if (!Ext.isNumber(d) || !Ext.isNumber(c)) { + return c || d + } else { + return a(d, c, b) + } + } + }, + angle: { + parseInitial: function(c, b) { + if (b - c > Math.PI) { + b -= Math.PI * 2 + } else { + if (b - c < -Math.PI) { + b += Math.PI * 2 + } + } + return [c, b] + }, + compute: function(d, c, b) { + if (!Ext.isNumber(d) || !Ext.isNumber(c)) { + return c || d + } else { + return a(d, c, b) + } + } + }, + path: { + parseInitial: function(m, n) { + var c = m.toStripes(), + o = n.toStripes(), + e, d, k = c.length, + p = o.length, + h, f, b, g = o[p - 1], + l = [g[g.length - 2], g[g.length - 1]]; + for (e = k; e < p; e++) { + c.push(c[k - 1].slice(0)) + } + for (e = p; e < k; e++) { + o.push(l.slice(0)) + } + b = c.length; + o.path = n; + o.temp = new Ext.draw.Path(); + for (e = 0; e < b; e++) { + h = c[e]; + f = o[e]; + k = h.length; + p = f.length; + o.temp.commands.push("M"); + for (d = p; d < k; d += 6) { + f.push(l[0], l[1], l[0], l[1], l[0], l[1]) + } + g = o[o.length - 1]; + l = [g[g.length - 2], g[g.length - 1]]; + for (d = k; d < p; d += 6) { + h.push(l[0], l[1], l[0], l[1], l[0], l[1]) + } + for (e = 0; e < f.length; e++) { + f[e] -= h[e] + } + for (e = 2; e < f.length; e += 6) { + o.temp.commands.push("C") + } + } + return [c, o] + }, + compute: function(c, l, m) { + if (m >= 1) { + return l.path + } + var e = 0, + f = c.length, + d = 0, + b, k, h, n = l.temp.params, + g = 0; + for (; e < f; e++) { + k = c[e]; + h = l[e]; + b = k.length; + for (d = 0; d < b; d++) { + n[g++] = h[d] * m + k[d] + } + } + return l.temp + } + }, + data: { + compute: function(h, j, k, g) { + var m = h.length - 1, + b = j.length - 1, + e = Math.max(m, b), + d, l, c; + if (!g || g === h) { + g = [] + } + g.length = e + 1; + for (c = 0; c <= e; c++) { + d = h[Math.min(c, m)]; + l = j[Math.min(c, b)]; + if (Ext.isNumber(d)) { + if (!Ext.isNumber(l)) { + l = 0 + } + g[c] = (l - d) * k + d + } else { + g[c] = l + } + } + return g + } + }, + text: { + compute: function(d, c, b) { + return d.substr(0, Math.round(d.length * (1 - b))) + c.substr(Math.round(c.length * (1 - b))) + } + }, + limited: "number", + limited01: "number" + } +}); +(function() { + if (!Ext.global.Float32Array) { + var a = function(d) { + if (typeof d === "number") { + this.length = d + } else { + if ("length" in d) { + this.length = d.length; + for (var c = 0, b = d.length; c < b; c++) { + this[c] = +d[c] + } + } + } + }; + a.prototype = []; + Ext.global.Float32Array = a + } +})(); +Ext.define("Ext.draw.Draw", { + singleton: true, + radian: Math.PI / 180, + pi2: Math.PI * 2, + reflectFn: function(b) { + return b + }, + rad: function(a) { + return (a % 360) * this.radian + }, + degrees: function(a) { + return (a / this.radian) % 360 + }, + isBBoxIntersect: function(b, a, c) { + c = c || 0; + return (Math.max(b.x, a.x) - c > Math.min(b.x + b.width, a.x + a.width)) || (Math.max(b.y, a.y) - c > Math.min(b.y + b.height, a.y + a.height)) + }, + isPointInBBox: function(a, c, b) { + return !!b && a >= b.x && a <= (b.x + b.width) && c >= b.y && c <= (b.y + b.height) + }, + spline: function(m) { + var e, c, k = m.length, + b, h, l, f, a = 0, + g = new Float32Array(m.length), + n = new Float32Array(m.length * 3 - 2); + g[0] = 0; + g[k - 1] = 0; + for (e = 1; e < k - 1; e++) { + g[e] = (m[e + 1] + m[e - 1] - 2 * m[e]) - g[e - 1]; + a = 1 / (4 - a); + g[e] *= a + } + for (e = k - 2; e > 0; e--) { + a = 3.732050807568877 + 48.248711305964385 / (-13.928203230275537 + Math.pow(0.07179676972449123, e)); + g[e] -= g[e + 1] * a + } + f = m[0]; + b = f - g[0]; + for (e = 0, c = 0; e < k - 1; c += 3) { + l = f; + h = b; + e++; + f = m[e]; + b = f - g[e]; + n[c] = l; + n[c + 1] = (b + 2 * h) / 3; + n[c + 2] = (b * 2 + h) / 3 + } + n[c] = f; + return n + }, + getAnchors: function(e, d, i, h, t, s, o) { + o = o || 4; + var n = Math.PI, + p = n / 2, + k = Math.abs, + a = Math.sin, + b = Math.cos, + f = Math.atan, + r, q, g, j, m, l, v, u, c; + r = (i - e) / o; + q = (t - i) / o; + if ((h >= d && h >= s) || (h <= d && h <= s)) { + g = j = p + } else { + g = f((i - e) / k(h - d)); + if (d < h) { + g = n - g + } + j = f((t - i) / k(h - s)); + if (s < h) { + j = n - j + } + } + c = p - ((g + j) % (n * 2)) / 2; + if (c > p) { + c -= n + } + g += c; + j += c; + m = i - r * a(g); + l = h + r * b(g); + v = i + q * a(j); + u = h + q * b(j); + if ((h > d && l < d) || (h < d && l > d)) { + m += k(d - l) * (m - i) / (l - h); + l = d + } + if ((h > s && u < s) || (h < s && u > s)) { + v -= k(s - u) * (v - i) / (u - h); + u = s + } + return { + x1: m, + y1: l, + x2: v, + y2: u + } + }, + smooth: function(l, j, o) { + var k = l.length, + h, g, c, b, q, p, n, m, f = [], + e = [], + d, a; + for (d = 0; d < k - 1; d++) { + h = l[d]; + g = j[d]; + if (d === 0) { + n = h; + m = g; + f.push(n); + e.push(m); + if (k === 1) { + break + } + } + c = l[d + 1]; + b = j[d + 1]; + q = l[d + 2]; + p = j[d + 2]; + if (!Ext.isNumber(q + p)) { + f.push(n, c, c); + e.push(m, b, b); + break + } + a = this.getAnchors(h, g, c, b, q, p, o); + f.push(n, a.x1, c); + e.push(m, a.y1, b); + n = a.x2; + m = a.y2 + } + return { + smoothX: f, + smoothY: e + } + }, + beginUpdateIOS: Ext.os.is.iOS ? function() { + this.iosUpdateEl = Ext.getBody().createChild({ + style: "position: absolute; top: 0px; bottom: 0px; left: 0px; right: 0px; background: rgba(0,0,0,0.001); z-index: 100000" + }) + } : Ext.emptyFn, + endUpdateIOS: function() { + this.iosUpdateEl = Ext.destroy(this.iosUpdateEl) + } +}); +Ext.define("Ext.draw.gradient.Gradient", { + requires: ["Ext.draw.Color"], + isGradient: true, + config: { + stops: [] + }, + applyStops: function(f) { + var e = [], + d = f.length, + c, b, a; + for (c = 0; c < d; c++) { + b = f[c]; + a = b.color; + if (!(a && a.isColor)) { + a = Ext.draw.Color.fly(a || Ext.draw.Color.NONE) + } + e.push({ + offset: Math.min(1, Math.max(0, "offset" in b ? b.offset : b.position || 0)), + color: a.toString() + }) + } + e.sort(function(h, g) { + return h.offset - g.offset + }); + return e + }, + onClassExtended: function(a, b) { + if (!b.alias && b.type) { + b.alias = "gradient." + b.type + } + }, + constructor: function(a) { + this.initConfig(a) + }, + generateGradient: Ext.emptyFn +}); +Ext.define("Ext.draw.gradient.GradientDefinition", { + singleton: true, + urlStringRe: /^url\(#([\w\-]+)\)$/, + gradients: {}, + add: function(a) { + var b = this.gradients, + c, e, d; + for (c = 0, e = a.length; c < e; c++) { + d = a[c]; + if (Ext.isString(d.id)) { + b[d.id] = d + } + } + }, + get: function(d) { + var a = this.gradients, + b = d.match(this.urlStringRe), + c; + if (b && b[1] && (c = a[b[1]])) { + return c || d + } + return d + } +}); +Ext.define("Ext.draw.sprite.AttributeParser", { + singleton: true, + attributeRe: /^url\(#([a-zA-Z\-]+)\)$/, + requires: ["Ext.draw.Color", "Ext.draw.gradient.GradientDefinition"], + "default": Ext.identityFn, + string: function(a) { + return String(a) + }, + number: function(a) { + if (Ext.isNumber(+a)) { + return a + } + }, + angle: function(a) { + if (Ext.isNumber(a)) { + a %= Math.PI * 2; + if (a < -Math.PI) { + a += Math.PI * 2 + } else { + if (a >= Math.PI) { + a -= Math.PI * 2 + } + } + return a + } + }, + data: function(a) { + if (Ext.isArray(a)) { + return a.slice() + } else { + if (a instanceof Float32Array) { + return new Float32Array(a) + } + } + }, + bool: function(a) { + return !!a + }, + color: function(a) { + if (a instanceof Ext.draw.Color) { + return a.toString() + } else { + if (a instanceof Ext.draw.gradient.Gradient) { + return a + } else { + if (!a) { + return Ext.draw.Color.NONE + } else { + if (Ext.isString(a)) { + if (a.substr(0, 3) === "url") { + a = Ext.draw.gradient.GradientDefinition.get(a); + if (Ext.isString(a)) { + return a + } + } else { + return Ext.draw.Color.fly(a).toString() + } + } + } + } + } + if (a.type === "linear") { + return Ext.create("Ext.draw.gradient.Linear", a) + } else { + if (a.type === "radial") { + return Ext.create("Ext.draw.gradient.Radial", a) + } else { + if (a.type === "pattern") { + return Ext.create("Ext.draw.gradient.Pattern", a) + } else { + return Ext.draw.Color.NONE + } + } + } + }, + limited: function(a, b) { + return function(c) { + c = +c; + return Ext.isNumber(c) ? Math.min(Math.max(c, a), b) : undefined + } + }, + limited01: function(a) { + a = +a; + return Ext.isNumber(a) ? Math.min(Math.max(a, 0), 1) : undefined + }, + enums: function() { + var d = {}, + a = Array.prototype.slice.call(arguments, 0), + b, c; + for (b = 0, c = a.length; b < c; b++) { + d[a[b]] = true + } + return function(e) { + return e in d ? e : undefined + } + } +}); +Ext.define("Ext.draw.sprite.AttributeDefinition", { + requires: ["Ext.draw.sprite.AttributeParser", "Ext.draw.sprite.AnimationParser"], + config: { + defaults: { + $value: {}, + lazy: true + }, + aliases: {}, + animationProcessors: {}, + processors: { + $value: {}, + lazy: true + }, + dirtyTriggers: {}, + triggers: {}, + updaters: {} + }, + inheritableStatics: { + processorFactoryRe: /^(\w+)\(([\w\-,]*)\)$/ + }, + spriteClass: null, + constructor: function(a) { + var b = this; + b.initConfig(a) + }, + applyDefaults: function(b, a) { + a = Ext.apply(a || {}, this.normalize(b)); + return a + }, + applyAliases: function(b, a) { + return Ext.apply(a || {}, b) + }, + applyProcessors: function(e, i) { + this.getAnimationProcessors(); + var j = i || {}, + h = Ext.draw.sprite.AttributeParser, + a = this.self.processorFactoryRe, + g = {}, + d, b, c, f; + for (b in e) { + f = e[b]; + if (typeof f === "string") { + c = f.match(a); + if (c) { + f = h[c[1]].apply(h, c[2].split(",")) + } else { + if (h[f]) { + g[b] = f; + d = true; + f = h[f] + } + } + } + j[b] = f + } + if (d) { + this.setAnimationProcessors(g) + } + return j + }, + applyAnimationProcessors: function(c, a) { + var e = Ext.draw.sprite.AnimationParser, + b, d; + if (!a) { + a = {} + } + for (b in c) { + d = c[b]; + if (d === "none") { + a[b] = null + } else { + if (Ext.isString(d) && !(b in a)) { + if (d in e) { + while (Ext.isString(e[d])) { + d = e[d] + } + a[b] = e[d] + } + } else { + if (Ext.isObject(d)) { + a[b] = d + } + } + } + } + return a + }, + updateDirtyTriggers: function(a) { + this.setTriggers(a) + }, + applyTriggers: function(b, c) { + if (!c) { + c = {} + } + for (var a in b) { + c[a] = b[a].split(",") + } + return c + }, + applyUpdaters: function(b, a) { + return Ext.apply(a || {}, b) + }, + batchedNormalize: function(f, n) { + if (!f) { + return {} + } + var j = this.getProcessors(), + d = this.getAliases(), + a = f.translation || f.translate, + o = {}, + g, h, b, e, p, c, m, l, k; + if ("rotation" in f) { + p = f.rotation + } else { + p = ("rotate" in f) ? f.rotate : undefined + } + if ("scaling" in f) { + c = f.scaling + } else { + c = ("scale" in f) ? f.scale : undefined + } + if (typeof c !== "undefined") { + if (Ext.isNumber(c)) { + o.scalingX = c; + o.scalingY = c + } else { + if ("x" in c) { + o.scalingX = c.x + } + if ("y" in c) { + o.scalingY = c.y + } + if ("centerX" in c) { + o.scalingCenterX = c.centerX + } + if ("centerY" in c) { + o.scalingCenterY = c.centerY + } + } + } + if (typeof p !== "undefined") { + if (Ext.isNumber(p)) { + p = Ext.draw.Draw.rad(p); + o.rotationRads = p + } else { + if ("rads" in p) { + o.rotationRads = p.rads + } else { + if ("degrees" in p) { + if (Ext.isArray(p.degrees)) { + o.rotationRads = Ext.Array.map(p.degrees, function(i) { + return Ext.draw.Draw.rad(i) + }) + } else { + o.rotationRads = Ext.draw.Draw.rad(p.degrees) + } + } + } + if ("centerX" in p) { + o.rotationCenterX = p.centerX + } + if ("centerY" in p) { + o.rotationCenterY = p.centerY + } + } + } + if (typeof a !== "undefined") { + if ("x" in a) { + o.translationX = a.x + } + if ("y" in a) { + o.translationY = a.y + } + } + if ("matrix" in f) { + m = Ext.draw.Matrix.create(f.matrix); + k = m.split(); + o.matrix = m; + o.rotationRads = k.rotation; + o.rotationCenterX = 0; + o.rotationCenterY = 0; + o.scalingX = k.scaleX; + o.scalingY = k.scaleY; + o.scalingCenterX = 0; + o.scalingCenterY = 0; + o.translationX = k.translateX; + o.translationY = k.translateY + } + for (b in f) { + e = f[b]; + if (typeof e === "undefined") { + continue + } else { + if (Ext.isArray(e)) { + if (b in d) { + b = d[b] + } + if (b in j) { + o[b] = []; + for (g = 0, h = e.length; g < h; g++) { + l = j[b].call(this, e[g]); + if (typeof l !== "undefined") { + o[b][g] = l + } + } + } else { + if (n) { + o[b] = e + } + } + } else { + if (b in d) { + b = d[b] + } + if (b in j) { + e = j[b].call(this, e); + if (typeof e !== "undefined") { + o[b] = e + } + } else { + if (n) { + o[b] = e + } + } + } + } + } + return o + }, + normalize: function(i, j) { + if (!i) { + return {} + } + var f = this.getProcessors(), + d = this.getAliases(), + a = i.translation || i.translate, + k = {}, + b, e, l, c, h, g; + if ("rotation" in i) { + l = i.rotation + } else { + l = ("rotate" in i) ? i.rotate : undefined + } + if ("scaling" in i) { + c = i.scaling + } else { + c = ("scale" in i) ? i.scale : undefined + } + if (a) { + if ("x" in a) { + k.translationX = a.x + } + if ("y" in a) { + k.translationY = a.y + } + } + if (typeof c !== "undefined") { + if (Ext.isNumber(c)) { + k.scalingX = c; + k.scalingY = c + } else { + if ("x" in c) { + k.scalingX = c.x + } + if ("y" in c) { + k.scalingY = c.y + } + if ("centerX" in c) { + k.scalingCenterX = c.centerX + } + if ("centerY" in c) { + k.scalingCenterY = c.centerY + } + } + } + if (typeof l !== "undefined") { + if (Ext.isNumber(l)) { + l = Ext.draw.Draw.rad(l); + k.rotationRads = l + } else { + if ("rads" in l) { + k.rotationRads = l.rads + } else { + if ("degrees" in l) { + k.rotationRads = Ext.draw.Draw.rad(l.degrees) + } + } + if ("centerX" in l) { + k.rotationCenterX = l.centerX + } + if ("centerY" in l) { + k.rotationCenterY = l.centerY + } + } + } + if ("matrix" in i) { + h = Ext.draw.Matrix.create(i.matrix); + g = h.split(); + k.matrix = h; + k.rotationRads = g.rotation; + k.rotationCenterX = 0; + k.rotationCenterY = 0; + k.scalingX = g.scaleX; + k.scalingY = g.scaleY; + k.scalingCenterX = 0; + k.scalingCenterY = 0; + k.translationX = g.translateX; + k.translationY = g.translateY + } + for (b in i) { + e = i[b]; + if (typeof e === "undefined") { + continue + } + if (b in d) { + b = d[b] + } + if (b in f) { + e = f[b].call(this, e); + if (typeof e !== "undefined") { + k[b] = e + } + } else { + if (j) { + k[b] = e + } + } + } + return k + }, + setBypassingNormalization: function(a, c, b) { + return c.pushDown(a, b) + }, + set: function(a, c, b) { + b = this.normalize(b); + return this.setBypassingNormalization(a, c, b) + } +}); +Ext.define("Ext.draw.Matrix", { + isMatrix: true, + statics: { + createAffineMatrixFromTwoPair: function(h, t, g, s, k, o, i, j) { + var v = g - h, + u = s - t, + e = i - k, + q = j - o, + d = 1 / (v * v + u * u), + p = v * e + u * q, + n = e * u - v * q, + m = -p * h - n * t, + l = n * h - p * t; + return new this(p * d, -n * d, n * d, p * d, m * d + k, l * d + o) + }, + createPanZoomFromTwoPair: function(q, e, p, c, h, s, n, g) { + if (arguments.length === 2) { + return this.createPanZoomFromTwoPair.apply(this, q.concat(e)) + } + var k = p - q, + j = c - e, + d = (q + p) * 0.5, + b = (e + c) * 0.5, + o = n - h, + a = g - s, + f = (h + n) * 0.5, + l = (s + g) * 0.5, + m = k * k + j * j, + i = o * o + a * a, + t = Math.sqrt(i / m); + return new this(t, 0, 0, t, f - t * d, l - t * b) + }, + fly: (function() { + var a = null, + b = function(c) { + a.elements = c; + return a + }; + return function(c) { + if (!a) { + a = new Ext.draw.Matrix() + } + a.elements = c; + Ext.draw.Matrix.fly = b; + return a + } + })(), + create: function(a) { + if (a instanceof this) { + return a + } + return new this(a) + } + }, + constructor: function(e, d, a, f, c, b) { + if (e && e.length === 6) { + this.elements = e.slice() + } else { + if (e !== undefined) { + this.elements = [e, d, a, f, c, b] + } else { + this.elements = [1, 0, 0, 1, 0, 0] + } + } + }, + prepend: function(a, l, h, g, m, k) { + var b = this.elements, + d = b[0], + j = b[1], + e = b[2], + c = b[3], + i = b[4], + f = b[5]; + b[0] = a * d + h * j; + b[1] = l * d + g * j; + b[2] = a * e + h * c; + b[3] = l * e + g * c; + b[4] = a * i + h * f + m; + b[5] = l * i + g * f + k; + return this + }, + prependMatrix: function(a) { + return this.prepend.apply(this, a.elements) + }, + append: function(a, l, h, g, m, k) { + var b = this.elements, + d = b[0], + j = b[1], + e = b[2], + c = b[3], + i = b[4], + f = b[5]; + b[0] = a * d + l * e; + b[1] = a * j + l * c; + b[2] = h * d + g * e; + b[3] = h * j + g * c; + b[4] = m * d + k * e + i; + b[5] = m * j + k * c + f; + return this + }, + appendMatrix: function(a) { + return this.append.apply(this, a.elements) + }, + set: function(f, e, a, g, c, b) { + var d = this.elements; + d[0] = f; + d[1] = e; + d[2] = a; + d[3] = g; + d[4] = c; + d[5] = b; + return this + }, + inverse: function(i) { + var g = this.elements, + o = g[0], + m = g[1], + l = g[2], + k = g[3], + j = g[4], + h = g[5], + n = 1 / (o * k - m * l); + o *= n; + m *= n; + l *= n; + k *= n; + if (i) { + i.set(k, -m, -l, o, l * h - k * j, m * j - o * h); + return i + } else { + return new Ext.draw.Matrix(k, -m, -l, o, l * h - k * j, m * j - o * h) + } + }, + translate: function(a, c, b) { + if (b) { + return this.prepend(1, 0, 0, 1, a, c) + } else { + return this.append(1, 0, 0, 1, a, c) + } + }, + scale: function(f, e, c, a, b) { + var d = this; + if (e == null) { + e = f + } + if (c === undefined) { + c = 0 + } + if (a === undefined) { + a = 0 + } + if (b) { + return d.prepend(f, 0, 0, e, c - c * f, a - a * e) + } else { + return d.append(f, 0, 0, e, c - c * f, a - a * e) + } + }, + rotate: function(g, e, c, b) { + var d = this, + f = Math.cos(g), + a = Math.sin(g); + e = e || 0; + c = c || 0; + if (b) { + return d.prepend(f, a, -a, f, e - f * e + c * a, c - f * c - e * a) + } else { + return d.append(f, a, -a, f, e - f * e + c * a, c - f * c - e * a) + } + }, + rotateFromVector: function(a, h, c) { + var e = this, + g = Math.sqrt(a * a + h * h), + f = a / g, + b = h / g; + if (c) { + return e.prepend(f, b, -b, f, 0, 0) + } else { + return e.append(f, b, -b, f, 0, 0) + } + }, + clone: function() { + return new Ext.draw.Matrix(this.elements) + }, + flipX: function() { + return this.append(-1, 0, 0, 1, 0, 0) + }, + flipY: function() { + return this.append(1, 0, 0, -1, 0, 0) + }, + skewX: function(a) { + return this.append(1, 0, Math.tan(a), 1, 0, 0) + }, + skewY: function(a) { + return this.append(1, Math.tan(a), 0, 1, 0, 0) + }, + shearX: function(a) { + return this.append(1, 0, a, 1, 0, 0) + }, + shearY: function(a) { + return this.append(1, a, 0, 1, 0, 0) + }, + reset: function() { + return this.set(1, 0, 0, 1, 0, 0) + }, + precisionCompensate: function(j, g) { + var c = this.elements, + f = c[0], + e = c[1], + i = c[2], + h = c[3], + d = c[4], + b = c[5], + a = e * i - f * h; + g.b = j * e / f; + g.c = j * i / h; + g.d = j; + g.xx = f / j; + g.yy = h / j; + g.dx = (b * f * i - d * f * h) / a / j; + g.dy = (d * e * h - b * f * h) / a / j + }, + precisionCompensateRect: function(j, g) { + var b = this.elements, + f = b[0], + e = b[1], + i = b[2], + h = b[3], + c = b[4], + a = b[5], + d = i / f; + g.b = j * e / f; + g.c = j * d; + g.d = j * h / f; + g.xx = f / j; + g.yy = f / j; + g.dx = (a * i - c * h) / (e * d - h) / j; + g.dy = -(a * f - c * e) / (e * d - h) / j + }, + x: function(a, c) { + var b = this.elements; + return a * b[0] + c * b[2] + b[4] + }, + y: function(a, c) { + var b = this.elements; + return a * b[1] + c * b[3] + b[5] + }, + get: function(b, a) { + return +this.elements[b + a * 2].toFixed(4) + }, + transformPoint: function(b) { + var c = this.elements, + a, d; + if (b.isPoint) { + a = b.x; + d = b.y + } else { + a = b[0]; + d = b[1] + } + return [a * c[0] + d * c[2] + c[4], a * c[1] + d * c[3] + c[5]] + }, + transformBBox: function(q, i, j) { + var b = this.elements, + d = q.x, + r = q.y, + g = q.width * 0.5, + o = q.height * 0.5, + a = b[0], + s = b[1], + n = b[2], + k = b[3], + e = d + g, + c = r + o, + p, f, m; + if (i) { + g -= i; + o -= i; + m = [Math.sqrt(b[0] * b[0] + b[2] * b[2]), Math.sqrt(b[1] * b[1] + b[3] * b[3])]; + p = Math.abs(g * a) + Math.abs(o * n) + Math.abs(m[0] * i); + f = Math.abs(g * s) + Math.abs(o * k) + Math.abs(m[1] * i) + } else { + p = Math.abs(g * a) + Math.abs(o * n); + f = Math.abs(g * s) + Math.abs(o * k) + } + if (!j) { + j = {} + } + j.x = e * a + c * n + b[4] - p; + j.y = e * s + c * k + b[5] - f; + j.width = p + p; + j.height = f + f; + return j + }, + transformList: function(e) { + var b = this.elements, + a = b[0], + h = b[2], + l = b[4], + k = b[1], + g = b[3], + j = b[5], + f = e.length, + c, d; + for (d = 0; d < f; d++) { + c = e[d]; + e[d] = [c[0] * a + c[1] * h + l, c[0] * k + c[1] * g + j] + } + return e + }, + isIdentity: function() { + var a = this.elements; + return a[0] === 1 && a[1] === 0 && a[2] === 0 && a[3] === 1 && a[4] === 0 && a[5] === 0 + }, + isEqual: function(a) { + var c = a && a.isMatrix ? a.elements : a, + b = this.elements; + return b[0] === c[0] && b[1] === c[1] && b[2] === c[2] && b[3] === c[3] && b[4] === c[4] && b[5] === c[5] + }, + equals: function(a) { + return this.isEqual(a) + }, + toArray: function() { + var a = this.elements; + return [a[0], a[2], a[4], a[1], a[3], a[5]] + }, + toVerticalArray: function() { + return this.elements.slice() + }, + toString: function() { + var a = this; + return [a.get(0, 0), a.get(0, 1), a.get(1, 0), a.get(1, 1), a.get(2, 0), a.get(2, 1)].join(",") + }, + toContext: function(a) { + a.transform.apply(a, this.elements); + return this + }, + toSvg: function() { + var a = this.elements; + return "matrix(" + a[0].toFixed(9) + "," + a[1].toFixed(9) + "," + a[2].toFixed(9) + "," + a[3].toFixed(9) + "," + a[4].toFixed(9) + "," + a[5].toFixed(9) + ")" + }, + getScaleX: function() { + var a = this.elements; + return Math.sqrt(a[0] * a[0] + a[2] * a[2]) + }, + getScaleY: function() { + var a = this.elements; + return Math.sqrt(a[1] * a[1] + a[3] * a[3]) + }, + getXX: function() { + return this.elements[0] + }, + getXY: function() { + return this.elements[1] + }, + getYX: function() { + return this.elements[2] + }, + getYY: function() { + return this.elements[3] + }, + getDX: function() { + return this.elements[4] + }, + getDY: function() { + return this.elements[5] + }, + split: function() { + var b = this.elements, + d = b[0], + c = b[1], + e = b[3], + a = { + translateX: b[4], + translateY: b[5] + }; + a.rotate = a.rotation = Math.atan2(c, d); + a.scaleX = d / Math.cos(a.rotate); + a.scaleY = e / d * a.scaleX; + return a + } +}, function() { + function b(e, c, d) { + e[c] = { + get: function() { + return this.elements[d] + }, + set: function(f) { + this.elements[d] = f + } + } + } + if (Object.defineProperties) { + var a = {}; + b(a, "a", 0); + b(a, "b", 1); + b(a, "c", 2); + b(a, "d", 3); + b(a, "e", 4); + b(a, "f", 5); + Object.defineProperties(this.prototype, a) + } + this.prototype.multiply = this.prototype.appendMatrix +}); +Ext.define("Ext.draw.modifier.Modifier", { + mixins: { + observable: "Ext.mixin.Observable" + }, + config: { + previous: null, + next: null, + sprite: null + }, + constructor: function(a) { + this.mixins.observable.constructor.call(this, a) + }, + updateNext: function(a) { + if (a) { + a.setPrevious(this) + } + }, + updatePrevious: function(a) { + if (a) { + a.setNext(this) + } + }, + prepareAttributes: function(a) { + if (this._previous) { + this._previous.prepareAttributes(a) + } + }, + popUp: function(a, b) { + if (this._next) { + this._next.popUp(a, b) + } else { + Ext.apply(a, b) + } + }, + pushDown: function(a, c) { + if (this._previous) { + return this._previous.pushDown(a, c) + } else { + for (var b in c) { + if (c[b] === a[b]) { + delete c[b] + } + } + return c + } + } +}); +Ext.define("Ext.draw.modifier.Target", { + requires: ["Ext.draw.Matrix"], + extend: "Ext.draw.modifier.Modifier", + alias: "modifier.target", + statics: { + uniqueId: 0 + }, + prepareAttributes: function(a) { + var b = this.getPrevious(); + if (b) { + b.prepareAttributes(a) + } + a.attributeId = "attribute-" + Ext.draw.modifier.Target.uniqueId++; + if (!a.hasOwnProperty("canvasAttributes")) { + a.bbox = { + plain: { + dirty: true + }, + transform: { + dirty: true + } + }; + a.dirty = true; + a.pendingUpdaters = {}; + a.canvasAttributes = {}; + a.matrix = new Ext.draw.Matrix(); + a.inverseMatrix = new Ext.draw.Matrix() + } + }, + applyChanges: function(f, k) { + Ext.apply(f, k); + var l = this.getSprite(), + o = f.pendingUpdaters, + h = l.self.def.getTriggers(), + p, a, m, b, e, n, d, c, g; + for (b in k) { + e = true; + if ((p = h[b])) { + l.scheduleUpdaters(f, p, [b]) + } + if (f.template && k.removeFromInstance && k.removeFromInstance[b]) { + delete f[b] + } + } + if (!e) { + return + } + if (o.canvas) { + n = o.canvas; + delete o.canvas; + for (d = 0, g = n.length; d < g; d++) { + b = n[d]; + f.canvasAttributes[b] = f[b] + } + } + if (f.hasOwnProperty("children")) { + a = f.children; + for (d = 0, g = a.length; d < g; d++) { + m = a[d]; + Ext.apply(m.pendingUpdaters, o); + if (n) { + for (c = 0; c < n.length; c++) { + b = n[c]; + m.canvasAttributes[b] = m[b] + } + } + l.callUpdaters(m) + } + } + l.setDirty(true); + l.callUpdaters(f) + }, + popUp: function(a, b) { + this.applyChanges(a, b) + }, + pushDown: function(a, b) { + var c = this.getPrevious(); + if (c) { + b = c.pushDown(a, b) + } + this.applyChanges(a, b); + return b + } +}); +Ext.define("Ext.draw.TimingFunctions", function() { + var g = Math.pow, + j = Math.sin, + m = Math.cos, + l = Math.sqrt, + e = Math.PI, + b = ["quad", "cube", "quart", "quint"], + c = { + pow: function(o, i) { + return g(o, i || 6) + }, + expo: function(i) { + return g(2, 8 * (i - 1)) + }, + circ: function(i) { + return 1 - l(1 - i * i) + }, + sine: function(i) { + return 1 - j((1 - i) * e / 2) + }, + back: function(i, o) { + o = o || 1.616; + return i * i * ((o + 1) * i - o) + }, + bounce: function(q) { + for (var o = 0, i = 1; 1; o += i, i /= 2) { + if (q >= (7 - 4 * o) / 11) { + return i * i - g((11 - 6 * o - 11 * q) / 4, 2) + } + } + }, + elastic: function(o, i) { + return g(2, 10 * --o) * m(20 * o * e * (i || 1) / 3) + } + }, + k = {}, + a, f, d; + + function h(i) { + return function(o) { + return g(o, i) + } + } + + function n(i, o) { + k[i + "In"] = function(p) { + return o(p) + }; + k[i + "Out"] = function(p) { + return 1 - o(1 - p) + }; + k[i + "InOut"] = function(p) { + return (p <= 0.5) ? o(2 * p) / 2 : (2 - o(2 * (1 - p))) / 2 + } + } + for (d = 0, f = b.length; d < f; ++d) { + c[b[d]] = h(d + 2) + } + for (a in c) { + n(a, c[a]) + } + k.linear = Ext.identityFn; + k.easeIn = k.quadIn; + k.easeOut = k.quadOut; + k.easeInOut = k.quadInOut; + return { + singleton: true, + easingMap: k + } +}, function(a) { + Ext.apply(a, a.easingMap) +}); +Ext.define("Ext.draw.Animator", { + uses: ["Ext.draw.Draw"], + singleton: true, + frameCallbacks: {}, + frameCallbackId: 0, + scheduled: 0, + frameStartTimeOffset: Ext.now(), + animations: [], + running: false, + animationTime: function() { + return Ext.AnimationQueue.frameStartTime - this.frameStartTimeOffset + }, + add: function(b) { + var a = this; + if (!a.contains(b)) { + a.animations.push(b); + a.ignite(); + if ("fireEvent" in b) { + b.fireEvent("animationstart", b) + } + } + }, + remove: function(d) { + var c = this, + e = c.animations, + b = 0, + a = e.length; + for (; b < a; ++b) { + if (e[b] === d) { + e.splice(b, 1); + if ("fireEvent" in d) { + d.fireEvent("animationend", d) + } + return + } + } + }, + contains: function(a) { + return Ext.Array.indexOf(this.animations, a) > -1 + }, + empty: function() { + return this.animations.length === 0 + }, + step: function(d) { + var c = this, + f = c.animations, + e, a = 0, + b = f.length; + for (; a < b; a++) { + e = f[a]; + e.step(d); + if (!e.animating) { + f.splice(a, 1); + a--; + b--; + if (e.fireEvent) { + e.fireEvent("animationend", e) + } + } + } + }, + schedule: function(c, a) { + a = a || this; + var b = "frameCallback" + (this.frameCallbackId++); + if (Ext.isString(c)) { + c = a[c] + } + Ext.draw.Animator.frameCallbacks[b] = { + fn: c, + scope: a, + once: true + }; + this.scheduled++; + Ext.draw.Animator.ignite(); + return b + }, + scheduleIf: function(e, b) { + b = b || this; + var c = Ext.draw.Animator.frameCallbacks, + a, d; + if (Ext.isString(e)) { + e = b[e] + } + for (d in c) { + a = c[d]; + if (a.once && a.fn === e && a.scope === b) { + return null + } + } + return this.schedule(e, b) + }, + cancel: function(a) { + if (Ext.draw.Animator.frameCallbacks[a] && Ext.draw.Animator.frameCallbacks[a].once) { + this.scheduled--; + delete Ext.draw.Animator.frameCallbacks[a] + } + }, + addFrameCallback: function(c, a) { + a = a || this; + if (Ext.isString(c)) { + c = a[c] + } + var b = "frameCallback" + (this.frameCallbackId++); + Ext.draw.Animator.frameCallbacks[b] = { + fn: c, + scope: a + }; + return b + }, + removeFrameCallback: function(a) { + delete Ext.draw.Animator.frameCallbacks[a] + }, + fireFrameCallbacks: function() { + var c = this.frameCallbacks, + d, b, a; + for (d in c) { + a = c[d]; + b = a.fn; + if (Ext.isString(b)) { + b = a.scope[b] + } + b.call(a.scope); + if (c[d] && a.once) { + this.scheduled--; + delete c[d] + } + } + }, + handleFrame: function() { + this.step(this.animationTime()); + this.fireFrameCallbacks(); + if (!this.scheduled && this.empty()) { + Ext.AnimationQueue.stop(this.handleFrame, this); + this.running = false; + Ext.draw.Draw.endUpdateIOS() + } + }, + ignite: function() { + if (!this.running) { + this.running = true; + Ext.AnimationQueue.start(this.handleFrame, this); + Ext.draw.Draw.beginUpdateIOS() + } + } +}); +Ext.define("Ext.draw.modifier.Animation", { + requires: ["Ext.draw.TimingFunctions", "Ext.draw.Animator"], + extend: "Ext.draw.modifier.Modifier", + alias: "modifier.animation", + config: { + easing: Ext.identityFn, + duration: 0, + customEasings: {}, + customDurations: {}, + customDuration: null + }, + constructor: function(a) { + var b = this; + b.anyAnimation = b.anySpecialAnimations = false; + b.animating = 0; + b.animatingPool = []; + b.callParent([a]) + }, + prepareAttributes: function(a) { + if (!a.hasOwnProperty("timers")) { + a.animating = false; + a.timers = {}; + a.animationOriginal = Ext.Object.chain(a); + a.animationOriginal.prototype = a + } + if (this._previous) { + this._previous.prepareAttributes(a.animationOriginal) + } + }, + updateSprite: function(a) { + this.setConfig(a.config.fx) + }, + updateDuration: function(a) { + this.anyAnimation = a > 0 + }, + applyEasing: function(a) { + if (typeof a === "string") { + a = Ext.draw.TimingFunctions.easingMap[a] + } + return a + }, + applyCustomEasings: function(a, e) { + e = e || {}; + var g, d, b, h, c, f; + for (d in a) { + g = true; + h = a[d]; + b = d.split(","); + if (typeof h === "string") { + h = Ext.draw.TimingFunctions.easingMap[h] + } + for (c = 0, f = b.length; c < f; c++) { + e[b[c]] = h + } + } + if (g) { + this.anySpecialAnimations = g + } + return e + }, + setEasingOn: function(a, e) { + a = Ext.Array.from(a).slice(); + var c = {}, + d = a.length, + b = 0; + for (; b < d; b++) { + c[a[b]] = e + } + this.setCustomEasings(c) + }, + clearEasingOn: function(a) { + a = Ext.Array.from(a, true); + var b = 0, + c = a.length; + for (; b < c; b++) { + delete this._customEasings[a[b]] + } + }, + applyCustomDurations: function(g, h) { + h = h || {}; + var e, c, f, a, b, d; + for (c in g) { + e = true; + f = g[c]; + a = c.split(","); + for (b = 0, d = a.length; b < d; b++) { + h[a[b]] = f + } + } + if (e) { + this.anySpecialAnimations = e + } + return h + }, + applyCustomDuration: function(a, b) { + if (a) { + this.getCustomDurations(); + this.setCustomDurations(a) + } + }, + setDurationOn: function(b, e) { + b = Ext.Array.from(b).slice(); + var a = {}, + c = 0, + d = b.length; + for (; c < d; c++) { + a[b[c]] = e + } + this.setCustomDurations(a) + }, + clearDurationOn: function(a) { + a = Ext.Array.from(a, true); + var b = 0, + c = a.length; + for (; b < c; b++) { + delete this._customDurations[a[b]] + } + }, + setAnimating: function(a, b) { + var e = this, + d = e.animatingPool; + if (a.animating !== b) { + a.animating = b; + if (b) { + d.push(a); + if (e.animating === 0) { + Ext.draw.Animator.add(e) + } + e.animating++ + } else { + for (var c = d.length; c--;) { + if (d[c] === a) { + d.splice(c, 1) + } + } + e.animating = d.length + } + } + }, + setAttrs: function(r, t) { + var s = this, + m = r.timers, + h = s._sprite.self.def._animationProcessors, + f = s._easing, + e = s._duration, + j = s._customDurations, + i = s._customEasings, + g = s.anySpecialAnimations, + n = s.anyAnimation || g, + o = r.animationOriginal, + d = false, + k, u, l, p, c, q, a; + if (!n) { + for (u in t) { + if (r[u] === t[u]) { + delete t[u] + } else { + r[u] = t[u] + } + delete o[u]; + delete m[u] + } + return t + } else { + for (u in t) { + l = t[u]; + p = r[u]; + if (l !== p && p !== undefined && p !== null && (c = h[u])) { + q = f; + a = e; + if (g) { + if (u in i) { + q = i[u] + } + if (u in j) { + a = j[u] + } + } + if (p && p.isGradient || l && l.isGradient) { + a = 0 + } + if (a) { + if (!m[u]) { + m[u] = {} + } + k = m[u]; + k.start = 0; + k.easing = q; + k.duration = a; + k.compute = c.compute; + k.serve = c.serve || Ext.identityFn; + k.remove = t.removeFromInstance && t.removeFromInstance[u]; + if (c.parseInitial) { + var b = c.parseInitial(p, l); + k.source = b[0]; + k.target = b[1] + } else { + if (c.parse) { + k.source = c.parse(p); + k.target = c.parse(l) + } else { + k.source = p; + k.target = l + } + } + o[u] = l; + delete t[u]; + d = true; + continue + } else { + delete o[u] + } + } else { + delete o[u] + } + delete m[u] + } + } + if (d && !r.animating) { + s.setAnimating(r, true) + } + return t + }, + updateAttributes: function(g) { + if (!g.animating) { + return {} + } + var h = {}, + e = false, + d = g.timers, + f = g.animationOriginal, + c = Ext.draw.Animator.animationTime(), + a, b, i; + if (g.lastUpdate === c) { + return null + } + for (a in d) { + b = d[a]; + if (!b.start) { + b.start = c; + i = 0 + } else { + i = (c - b.start) / b.duration + } + if (i >= 1) { + h[a] = f[a]; + delete f[a]; + if (d[a].remove) { + h.removeFromInstance = h.removeFromInstance || {}; + h.removeFromInstance[a] = true + } + delete d[a] + } else { + h[a] = b.serve(b.compute(b.source, b.target, b.easing(i), g[a])); + e = true + } + } + g.lastUpdate = c; + this.setAnimating(g, e); + return h + }, + pushDown: function(a, b) { + b = this.callParent([a.animationOriginal, b]); + return this.setAttrs(a, b) + }, + popUp: function(a, b) { + a = a.prototype; + b = this.setAttrs(a, b); + if (this._next) { + return this._next.popUp(a, b) + } else { + return Ext.apply(a, b) + } + }, + step: function(g) { + var f = this, + c = f.animatingPool.slice(), + e = c.length, + b = 0, + a, d; + for (; b < e; b++) { + a = c[b]; + d = f.updateAttributes(a); + if (d && f._next) { + f._next.popUp(a, d) + } + } + }, + stop: function() { + this.step(); + var d = this, + b = d.animatingPool, + a, c; + for (a = 0, c = b.length; a < c; a++) { + b[a].animating = false + } + d.animatingPool.length = 0; + d.animating = 0; + Ext.draw.Animator.remove(d) + }, + destroy: function() { + this.animatingPool.length = 0; + this.animating = 0; + this.callParent() + } +}); +Ext.define("Ext.draw.modifier.Highlight", { + extend: "Ext.draw.modifier.Modifier", + alias: "modifier.highlight", + config: { + enabled: false, + highlightStyle: null + }, + preFx: true, + applyHighlightStyle: function(b, a) { + a = a || {}; + if (this.getSprite()) { + Ext.apply(a, this.getSprite().self.def.normalize(b)) + } else { + Ext.apply(a, b) + } + return a + }, + prepareAttributes: function(a) { + if (!a.hasOwnProperty("highlightOriginal")) { + a.highlighted = false; + a.highlightOriginal = Ext.Object.chain(a); + a.highlightOriginal.prototype = a; + a.highlightOriginal.removeFromInstance = {} + } + if (this._previous) { + this._previous.prepareAttributes(a.highlightOriginal) + } + }, + updateSprite: function(b, a) { + if (b) { + if (this.getHighlightStyle()) { + this._highlightStyle = b.self.def.normalize(this.getHighlightStyle()) + } + this.setHighlightStyle(b.config.highlight) + } + b.self.def.setConfig({ + defaults: { + highlighted: false + }, + processors: { + highlighted: "bool" + } + }); + this.setSprite(b) + }, + filterChanges: function(a, d) { + var e = this, + f = a.highlightOriginal, + c = e.getHighlightStyle(), + b; + if (a.highlighted) { + for (b in d) { + if (c.hasOwnProperty(b)) { + f[b] = d[b]; + delete d[b] + } + } + } + for (b in d) { + if (b !== "highlighted" && f[b] === d[b]) { + delete d[b] + } + } + return d + }, + pushDown: function(e, g) { + var f = this.getHighlightStyle(), + c = e.highlightOriginal, + i = c.removeFromInstance, + d, a, h, b; + if (g.hasOwnProperty("highlighted")) { + d = g.highlighted; + delete g.highlighted; + if (this._previous) { + g = this._previous.pushDown(c, g) + } + g = this.filterChanges(e, g); + if (d !== e.highlighted) { + if (d) { + for (a in f) { + if (a in g) { + c[a] = g[a] + } else { + h = e.template && e.template.ownAttr; + if (h && !e.prototype.hasOwnProperty(a)) { + i[a] = true; + c[a] = h.animationOriginal[a] + } else { + b = c.timers[a]; + if (b && b.remove) { + i[a] = true + } + c[a] = e[a] + } + } + if (c[a] !== f[a]) { + g[a] = f[a] + } + } + } else { + for (a in f) { + if (!(a in g)) { + g[a] = c[a] + } + delete c[a] + } + g.removeFromInstance = g.removeFromInstance || {}; + Ext.apply(g.removeFromInstance, i); + c.removeFromInstance = {} + } + g.highlighted = d + } + } else { + if (this._previous) { + g = this._previous.pushDown(c, g) + } + g = this.filterChanges(e, g) + } + return g + }, + popUp: function(a, b) { + b = this.filterChanges(a, b); + Ext.draw.modifier.Modifier.prototype.popUp.call(this, a, b) + } +}); +Ext.define("Ext.draw.sprite.Sprite", { + alias: "sprite.sprite", + mixins: { + observable: "Ext.mixin.Observable" + }, + requires: ["Ext.draw.Draw", "Ext.draw.gradient.Gradient", "Ext.draw.sprite.AttributeDefinition", "Ext.draw.modifier.Target", "Ext.draw.modifier.Animation", "Ext.draw.modifier.Highlight"], + isSprite: true, + statics: { + defaultHitTestOptions: { + fill: true, + stroke: true + } + }, + inheritableStatics: { + def: { + processors: { + strokeStyle: "color", + fillStyle: "color", + strokeOpacity: "limited01", + fillOpacity: "limited01", + lineWidth: "number", + lineCap: "enums(butt,round,square)", + lineJoin: "enums(round,bevel,miter)", + lineDash: "data", + lineDashOffset: "number", + miterLimit: "number", + shadowColor: "color", + shadowOffsetX: "number", + shadowOffsetY: "number", + shadowBlur: "number", + globalAlpha: "limited01", + globalCompositeOperation: "enums(source-over,destination-over,source-in,destination-in,source-out,destination-out,source-atop,destination-atop,lighter,xor,copy)", + hidden: "bool", + transformFillStroke: "bool", + zIndex: "number", + translationX: "number", + translationY: "number", + rotationRads: "number", + rotationCenterX: "number", + rotationCenterY: "number", + scalingX: "number", + scalingY: "number", + scalingCenterX: "number", + scalingCenterY: "number", + constrainGradients: "bool" + }, + aliases: { + stroke: "strokeStyle", + fill: "fillStyle", + color: "fillStyle", + "stroke-width": "lineWidth", + "stroke-linecap": "lineCap", + "stroke-linejoin": "lineJoin", + "stroke-miterlimit": "miterLimit", + "text-anchor": "textAlign", + opacity: "globalAlpha", + translateX: "translationX", + translateY: "translationY", + rotateRads: "rotationRads", + rotateCenterX: "rotationCenterX", + rotateCenterY: "rotationCenterY", + scaleX: "scalingX", + scaleY: "scalingY", + scaleCenterX: "scalingCenterX", + scaleCenterY: "scalingCenterY" + }, + defaults: { + hidden: false, + zIndex: 0, + strokeStyle: "none", + fillStyle: "none", + lineWidth: 1, + lineDash: [], + lineDashOffset: 0, + lineCap: "butt", + lineJoin: "miter", + miterLimit: 10, + shadowColor: "none", + shadowOffsetX: 0, + shadowOffsetY: 0, + shadowBlur: 0, + globalAlpha: 1, + strokeOpacity: 1, + fillOpacity: 1, + transformFillStroke: false, + translationX: 0, + translationY: 0, + rotationRads: 0, + rotationCenterX: null, + rotationCenterY: null, + scalingX: 1, + scalingY: 1, + scalingCenterX: null, + scalingCenterY: null, + constrainGradients: false + }, + triggers: { + zIndex: "zIndex", + globalAlpha: "canvas", + globalCompositeOperation: "canvas", + transformFillStroke: "canvas", + strokeStyle: "canvas", + fillStyle: "canvas", + strokeOpacity: "canvas", + fillOpacity: "canvas", + lineWidth: "canvas", + lineCap: "canvas", + lineJoin: "canvas", + lineDash: "canvas", + lineDashOffset: "canvas", + miterLimit: "canvas", + shadowColor: "canvas", + shadowOffsetX: "canvas", + shadowOffsetY: "canvas", + shadowBlur: "canvas", + translationX: "transform", + translationY: "transform", + rotationRads: "transform", + rotationCenterX: "transform", + rotationCenterY: "transform", + scalingX: "transform", + scalingY: "transform", + scalingCenterX: "transform", + scalingCenterY: "transform", + constrainGradients: "canvas" + }, + updaters: { + bbox: "bboxUpdater", + zIndex: function(a) { + a.dirtyZIndex = true + }, + transform: function(a) { + a.dirtyTransform = true; + a.bbox.transform.dirty = true + } + } + } + }, + config: { + parent: null, + surface: null + }, + onClassExtended: function(d, c) { + var b = d.superclass.self.def.initialConfig, + e = c.inheritableStatics && c.inheritableStatics.def, + a; + if (e) { + a = Ext.Object.merge({}, b, e); + d.def = new Ext.draw.sprite.AttributeDefinition(a); + delete c.inheritableStatics.def + } else { + d.def = new Ext.draw.sprite.AttributeDefinition(b) + } + d.def.spriteClass = d + }, + constructor: function(b) { + var d = this, + c = d.self.def, + e = c.getDefaults(), + a; + b = Ext.isObject(b) ? b : {}; + d.id = b.id || Ext.id(null, "ext-sprite-"); + d.attr = {}; + d.mixins.observable.constructor.apply(d, arguments); + a = Ext.Array.from(b.modifiers, true); + d.prepareModifiers(a); + d.initializeAttributes(); + d.setAttributes(e, true); + d.setAttributes(b) + }, + getDirty: function() { + return this.attr.dirty + }, + setDirty: function(b) { + this.attr.dirty = b; + if (b) { + var a = this.getParent(); + if (a) { + a.setDirty(true) + } + } + }, + addModifier: function(a, b) { + var c = this; + if (!(a instanceof Ext.draw.modifier.Modifier)) { + a = Ext.factory(a, null, null, "modifier") + } + a.setSprite(c); + if (a.preFx || a.config && a.config.preFx) { + if (c.fx.getPrevious()) { + c.fx.getPrevious().setNext(a) + } + a.setNext(c.fx) + } else { + c.topModifier.getPrevious().setNext(a); + a.setNext(c.topModifier) + } + if (b) { + c.initializeAttributes() + } + return a + }, + prepareModifiers: function(d) { + var c = this, + a, b; + c.topModifier = new Ext.draw.modifier.Target({ + sprite: c + }); + c.fx = new Ext.draw.modifier.Animation({ + sprite: c + }); + c.fx.setNext(c.topModifier); + for (a = 0, b = d.length; a < b; a++) { + c.addModifier(d[a], false) + } + }, + getAnimation: function() { + return this.fx + }, + setAnimation: function(a) { + this.fx.setConfig(a) + }, + initializeAttributes: function() { + this.topModifier.prepareAttributes(this.attr) + }, + callUpdaters: function(d) { + var e = this, + h = d.pendingUpdaters, + i = e.self.def.getUpdaters(), + c = false, + a = false, + b, g, f; + e.callUpdaters = Ext.emptyFn; + do { + c = false; + for (g in h) { + c = true; + b = h[g]; + delete h[g]; + f = i[g]; + if (typeof f === "string") { + f = e[f] + } + if (f) { + f.call(e, d, b) + } + } + a = a || c + } while (c); + delete e.callUpdaters; + if (a) { + e.setDirty(true) + } + }, + scheduleUpdaters: function(a, e, c) { + var f; + if (c) { + for (var b = 0, d = e.length; b < d; b++) { + f = e[b]; + this.scheduleUpdater(a, f, c) + } + } else { + for (f in e) { + c = e[f]; + this.scheduleUpdater(a, f, c) + } + } + }, + scheduleUpdater: function(a, c, b) { + b = b || []; + var d = a.pendingUpdaters; + if (c in d) { + if (b.length) { + d[c] = Ext.Array.merge(d[c], b) + } + } else { + d[c] = b + } + }, + setAttributes: function(d, g, c) { + var a = this.attr, + b, e, f; + if (g) { + if (c) { + this.topModifier.pushDown(a, d) + } else { + f = {}; + for (b in d) { + e = d[b]; + if (e !== a[b]) { + f[b] = e + } + } + this.topModifier.pushDown(a, f) + } + } else { + this.topModifier.pushDown(a, this.self.def.normalize(d)) + } + }, + setAttributesBypassingNormalization: function(b, a) { + return this.setAttributes(b, true, a) + }, + bboxUpdater: function(b) { + var c = b.rotationRads !== 0, + a = b.scalingX !== 1 || b.scalingY !== 1, + d = b.rotationCenterX === null || b.rotationCenterY === null, + e = b.scalingCenterX === null || b.scalingCenterY === null; + b.bbox.plain.dirty = true; + b.bbox.transform.dirty = true; + if (c && d || a && e) { + this.scheduleUpdater(b, "transform") + } + }, + getBBox: function(d) { + var e = this, + a = e.attr, + f = a.bbox, + c = f.plain, + b = f.transform; + if (c.dirty) { + e.updatePlainBBox(c); + c.dirty = false + } + if (!d) { + e.applyTransformations(); + if (b.dirty) { + e.updateTransformedBBox(b, c); + b.dirty = false + } + return b + } + return c + }, + updatePlainBBox: Ext.emptyFn, + updateTransformedBBox: function(a, b) { + this.attr.matrix.transformBBox(b, 0, a) + }, + getBBoxCenter: function(a) { + var b = this.getBBox(a); + if (b) { + return [b.x + b.width * 0.5, b.y + b.height * 0.5] + } else { + return [0, 0] + } + }, + hide: function() { + this.attr.hidden = true; + this.setDirty(true); + return this + }, + show: function() { + this.attr.hidden = false; + this.setDirty(true); + return this + }, + useAttributes: function(i, f) { + this.applyTransformations(); + var d = this.attr, + h = d.canvasAttributes, + e = h.strokeStyle, + g = h.fillStyle, + b = h.lineDash, + c = h.lineDashOffset, + a; + if (e) { + if (e.isGradient) { + i.strokeStyle = "black"; + i.strokeGradient = e + } else { + i.strokeGradient = false + } + } + if (g) { + if (g.isGradient) { + i.fillStyle = "black"; + i.fillGradient = g + } else { + i.fillGradient = false + } + } + if (b) { + i.setLineDash(b) + } + if (Ext.isNumber(c + i.lineDashOffset)) { + i.lineDashOffset = c + } + for (a in h) { + if (h[a] !== undefined && h[a] !== i[a]) { + i[a] = h[a] + } + } + this.setGradientBBox(i, f) + }, + setGradientBBox: function(b, c) { + var a = this.attr; + if (a.constrainGradients) { + b.setGradientBBox({ + x: c[0], + y: c[1], + width: c[2], + height: c[3] + }) + } else { + b.setGradientBBox(this.getBBox(a.transformFillStroke)) + } + }, + applyTransformations: function(b) { + if (!b && !this.attr.dirtyTransform) { + return + } + var r = this, + k = r.attr, + p = r.getBBoxCenter(true), + g = p[0], + f = p[1], + q = k.translationX, + o = k.translationY, + j = k.scalingX, + i = k.scalingY === null ? k.scalingX : k.scalingY, + m = k.scalingCenterX === null ? g : k.scalingCenterX, + l = k.scalingCenterY === null ? f : k.scalingCenterY, + s = k.rotationRads, + e = k.rotationCenterX === null ? g : k.rotationCenterX, + d = k.rotationCenterY === null ? f : k.rotationCenterY, + c = Math.cos(s), + a = Math.sin(s), + n, h; + if (j === 1 && i === 1) { + m = 0; + l = 0 + } + if (s === 0) { + e = 0; + d = 0 + } + n = m * (1 - j) - e; + h = l * (1 - i) - d; + k.matrix.elements = [c * j, a * j, -a * i, c * i, c * n - a * h + e + q, a * n + c * h + d + o]; + k.matrix.inverse(k.inverseMatrix); + k.dirtyTransform = false; + k.bbox.transform.dirty = true + }, + transform: function(b, c) { + var a = this.attr, + e = a.matrix, + d; + if (b && b.isMatrix) { + d = b.elements + } else { + d = b + } + e.prepend.apply(e, d.slice()); + e.inverse(a.inverseMatrix); + if (c) { + this.updateTransformAttributes() + } + a.dirtyTransform = false; + a.bbox.transform.dirty = true; + this.setDirty(true); + return this + }, + updateTransformAttributes: function() { + var a = this.attr, + b = a.matrix.split(); + a.rotationRads = b.rotate; + a.rotationCenterX = 0; + a.rotationCenterY = 0; + a.scalingX = b.scaleX; + a.scalingY = b.scaleY; + a.scalingCenterX = 0; + a.scalingCenterY = 0; + a.translationX = b.translateX; + a.translationY = b.translateY + }, + resetTransform: function(b) { + var a = this.attr; + a.matrix.reset(); + a.inverseMatrix.reset(); + if (!b) { + this.updateTransformAttributes() + } + a.dirtyTransform = false; + a.bbox.transform.dirty = true; + this.setDirty(true); + return this + }, + setTransform: function(a, b) { + this.resetTransform(true); + this.transform.call(this, a, b); + return this + }, + preRender: Ext.emptyFn, + render: Ext.emptyFn, + hitTest: function(b, c) { + if (this.isVisible()) { + var a = b[0], + f = b[1], + e = this.getBBox(), + d = e && a >= e.x && a <= (e.x + e.width) && f >= e.y && f <= (e.y + e.height); + if (d) { + return { + sprite: this + } + } + } + return null + }, + isVisible: function() { + var e = this.attr, + f = this.getParent(), + g = f && (f.isSurface || f.isVisible()), + d = g && !e.hidden && e.globalAlpha, + b = Ext.draw.Color.NONE, + a = Ext.draw.Color.RGBA_NONE, + c = e.fillOpacity && e.fillStyle !== b && e.fillStyle !== a, + i = e.strokeOpacity && e.strokeStyle !== b && e.strokeStyle !== a, + h = d && (c || i); + return !!h + }, + repaint: function() { + var a = this.getSurface(); + if (a) { + a.renderFrame() + } + }, + remove: function() { + var a = this.getSurface(); + if (a && a.isSurface) { + return a.remove(this) + } + return null + }, + destroy: function() { + var b = this, + a = b.topModifier, + c; + while (a) { + c = a; + a = a.getPrevious(); + c.destroy() + } + delete b.attr; + b.remove(); + if (b.fireEvent("beforedestroy", b) !== false) { + b.fireEvent("destroy", b) + } + b.callParent() + } +}, function() { + this.def = new Ext.draw.sprite.AttributeDefinition(this.def); + this.def.spriteClass = this +}); +Ext.define("Ext.draw.Path", { + requires: ["Ext.draw.Draw"], + statics: { + pathRe: /,?([achlmqrstvxz]),?/gi, + pathRe2: /-/gi, + pathSplitRe: /\s|,/g + }, + svgString: "", + constructor: function(a) { + var b = this; + b.commands = []; + b.params = []; + b.cursor = null; + b.startX = 0; + b.startY = 0; + if (a) { + b.fromSvgString(a) + } + }, + clear: function() { + var a = this; + a.params.length = 0; + a.commands.length = 0; + a.cursor = null; + a.startX = 0; + a.startY = 0; + a.dirt() + }, + dirt: function() { + this.svgString = "" + }, + moveTo: function(a, c) { + var b = this; + if (!b.cursor) { + b.cursor = [a, c] + } + b.params.push(a, c); + b.commands.push("M"); + b.startX = a; + b.startY = c; + b.cursor[0] = a; + b.cursor[1] = c; + b.dirt() + }, + lineTo: function(a, c) { + var b = this; + if (!b.cursor) { + b.cursor = [a, c]; + b.params.push(a, c); + b.commands.push("M") + } else { + b.params.push(a, c); + b.commands.push("L") + } + b.cursor[0] = a; + b.cursor[1] = c; + b.dirt() + }, + bezierCurveTo: function(c, e, b, d, a, g) { + var f = this; + if (!f.cursor) { + f.moveTo(c, e) + } + f.params.push(c, e, b, d, a, g); + f.commands.push("C"); + f.cursor[0] = a; + f.cursor[1] = g; + f.dirt() + }, + quadraticCurveTo: function(b, e, a, d) { + var c = this; + if (!c.cursor) { + c.moveTo(b, e) + } + c.bezierCurveTo((2 * b + c.cursor[0]) / 3, (2 * e + c.cursor[1]) / 3, (2 * b + a) / 3, (2 * e + d) / 3, a, d) + }, + closePath: function() { + var a = this; + if (a.cursor) { + a.cursor = null; + a.commands.push("Z"); + a.dirt() + } + }, + arcTo: function(A, f, z, d, j, i, v) { + var E = this; + if (i === undefined) { + i = j + } + if (v === undefined) { + v = 0 + } + if (!E.cursor) { + E.moveTo(A, f); + return + } + if (j === 0 || i === 0) { + E.lineTo(A, f); + return + } + z -= A; + d -= f; + var B = E.cursor[0] - A, + g = E.cursor[1] - f, + C = z * g - d * B, + b, a, l, r, k, q, x = Math.sqrt(B * B + g * g), + u = Math.sqrt(z * z + d * d), + t, e, c; + if (C === 0) { + E.lineTo(A, f); + return + } + if (i !== j) { + b = Math.cos(v); + a = Math.sin(v); + l = b / j; + r = a / i; + k = -a / j; + q = b / i; + var D = l * B + r * g; + g = k * B + q * g; + B = D; + D = l * z + r * d; + d = k * z + q * d; + z = D + } else { + B /= j; + g /= i; + z /= j; + d /= i + } + e = B * u + z * x; + c = g * u + d * x; + t = 1 / (Math.sin(Math.asin(Math.abs(C) / (x * u)) * 0.5) * Math.sqrt(e * e + c * c)); + e *= t; + c *= t; + var o = (e * B + c * g) / (B * B + g * g), + m = (e * z + c * d) / (z * z + d * d); + var n = B * o - e, + p = g * o - c, + h = z * m - e, + y = d * m - c, + w = Math.atan2(p, n), + s = Math.atan2(y, h); + if (C > 0) { + if (s < w) { + s += Math.PI * 2 + } + } else { + if (w < s) { + w += Math.PI * 2 + } + } + if (i !== j) { + e = b * e * j - a * c * i + A; + c = a * c * i + b * c * i + f; + E.lineTo(b * j * n - a * i * p + e, a * j * n + b * i * p + c); + E.ellipse(e, c, j, i, v, w, s, C < 0) + } else { + e = e * j + A; + c = c * i + f; + E.lineTo(j * n + e, i * p + c); + E.ellipse(e, c, j, i, v, w, s, C < 0) + } + }, + ellipse: function(h, f, c, a, q, n, d, e) { + var o = this, + g = o.params, + b = g.length, + m, l, k; + if (d - n >= Math.PI * 2) { + o.ellipse(h, f, c, a, q, n, n + Math.PI, e); + o.ellipse(h, f, c, a, q, n + Math.PI, d, e); + return + } + if (!e) { + if (d < n) { + d += Math.PI * 2 + } + m = o.approximateArc(g, h, f, c, a, q, n, d) + } else { + if (n < d) { + n += Math.PI * 2 + } + m = o.approximateArc(g, h, f, c, a, q, d, n); + for (l = b, k = g.length - 2; l < k; l += 2, k -= 2) { + var p = g[l]; + g[l] = g[k]; + g[k] = p; + p = g[l + 1]; + g[l + 1] = g[k + 1]; + g[k + 1] = p + } + } + if (!o.cursor) { + o.cursor = [g[g.length - 2], g[g.length - 1]]; + o.commands.push("M") + } else { + o.cursor[0] = g[g.length - 2]; + o.cursor[1] = g[g.length - 1]; + o.commands.push("L") + } + for (l = 2; l < m; l += 6) { + o.commands.push("C") + } + o.dirt() + }, + arc: function(b, f, a, d, c, e) { + this.ellipse(b, f, a, a, 0, d, c, e) + }, + rect: function(b, e, c, a) { + if (c == 0 || a == 0) { + return + } + var d = this; + d.moveTo(b, e); + d.lineTo(b + c, e); + d.lineTo(b + c, e + a); + d.lineTo(b, e + a); + d.closePath() + }, + approximateArc: function(s, i, f, o, n, d, x, v) { + var e = Math.cos(d), + z = Math.sin(d), + k = Math.cos(x), + l = Math.sin(x), + q = e * k * o - z * l * n, + y = -e * l * o - z * k * n, + p = z * k * o + e * l * n, + w = -z * l * o + e * k * n, + m = Math.PI / 2, + r = 2, + j = q, + u = y, + h = p, + t = w, + b = 0.547443256150549, + C, g, A, a, B, c; + v -= x; + if (v < 0) { + v += Math.PI * 2 + } + s.push(q + i, p + f); + while (v >= m) { + s.push(j + u * b + i, h + t * b + f, j * b + u + i, h * b + t + f, u + i, t + f); + r += 6; + v -= m; + C = j; + j = u; + u = -C; + C = h; + h = t; + t = -C + } + if (v) { + g = (0.3294738052815987 + 0.012120855841304373 * v) * v; + A = Math.cos(v); + a = Math.sin(v); + B = A + g * a; + c = a - g * A; + s.push(j + u * g + i, h + t * g + f, j * B + u * c + i, h * B + t * c + f, j * A + u * a + i, h * A + t * a + f); + r += 6 + } + return r + }, + arcSvg: function(j, h, r, m, w, t, c) { + if (j < 0) { + j = -j + } + if (h < 0) { + h = -h + } + var x = this, + u = x.cursor[0], + f = x.cursor[1], + a = (u - t) / 2, + y = (f - c) / 2, + d = Math.cos(r), + s = Math.sin(r), + o = a * d + y * s, + v = -a * s + y * d, + i = o / j, + g = v / h, + p = i * i + g * g, + e = (u + t) * 0.5, + b = (f + c) * 0.5, + l = 0, + k = 0; + if (p >= 1) { + p = Math.sqrt(p); + j *= p; + h *= p + } else { + p = Math.sqrt(1 / p - 1); + if (m === w) { + p = -p + } + l = p * j * g; + k = -p * h * i; + e += d * l - s * k; + b += s * l + d * k + } + var q = Math.atan2((v - k) / h, (o - l) / j), + n = Math.atan2((-v - k) / h, (-o - l) / j) - q; + if (w) { + if (n <= 0) { + n += Math.PI * 2 + } + } else { + if (n >= 0) { + n -= Math.PI * 2 + } + } + x.ellipse(e, b, j, h, r, q, q + n, 1 - w) + }, + fromSvgString: function(e) { + if (!e) { + return + } + var m = this, + h, l = { + a: 7, + c: 6, + h: 1, + l: 2, + m: 2, + q: 4, + s: 4, + t: 2, + v: 1, + z: 0, + A: 7, + C: 6, + H: 1, + L: 2, + M: 2, + Q: 4, + S: 4, + T: 2, + V: 1, + Z: 0 + }, + k = "", + g, f, c = 0, + b = 0, + d = false, + j, n, a; + if (Ext.isString(e)) { + h = e.replace(Ext.draw.Path.pathRe, " $1 ").replace(Ext.draw.Path.pathRe2, " -").split(Ext.draw.Path.pathSplitRe) + } else { + if (Ext.isArray(e)) { + h = e.join(",").split(Ext.draw.Path.pathSplitRe) + } + } + for (j = 0, n = 0; j < h.length; j++) { + if (h[j] !== "") { + h[n++] = h[j] + } + } + h.length = n; + m.clear(); + for (j = 0; j < h.length;) { + k = d; + d = h[j]; + a = (d.toUpperCase() !== d); + j++; + switch (d) { + case "M": + m.moveTo(c = +h[j], b = +h[j + 1]); + j += 2; + while (j < n && !l.hasOwnProperty(h[j])) { + m.lineTo(c = +h[j], b = +h[j + 1]); + j += 2 + } + break; + case "L": + m.lineTo(c = +h[j], b = +h[j + 1]); + j += 2; + while (j < n && !l.hasOwnProperty(h[j])) { + m.lineTo(c = +h[j], b = +h[j + 1]); + j += 2 + } + break; + case "A": + while (j < n && !l.hasOwnProperty(h[j])) { + m.arcSvg(+h[j], +h[j + 1], +h[j + 2] * Math.PI / 180, +h[j + 3], +h[j + 4], c = +h[j + 5], b = +h[j + 6]); + j += 7 + } + break; + case "C": + while (j < n && !l.hasOwnProperty(h[j])) { + m.bezierCurveTo(+h[j], +h[j + 1], g = +h[j + 2], f = +h[j + 3], c = +h[j + 4], b = +h[j + 5]); + j += 6 + } + break; + case "Z": + m.closePath(); + break; + case "m": + m.moveTo(c += +h[j], b += +h[j + 1]); + j += 2; + while (j < n && !l.hasOwnProperty(h[j])) { + m.lineTo(c += +h[j], b += +h[j + 1]); + j += 2 + } + break; + case "l": + m.lineTo(c += +h[j], b += +h[j + 1]); + j += 2; + while (j < n && !l.hasOwnProperty(h[j])) { + m.lineTo(c += +h[j], b += +h[j + 1]); + j += 2 + } + break; + case "a": + while (j < n && !l.hasOwnProperty(h[j])) { + m.arcSvg(+h[j], +h[j + 1], +h[j + 2] * Math.PI / 180, +h[j + 3], +h[j + 4], c += +h[j + 5], b += +h[j + 6]); + j += 7 + } + break; + case "c": + while (j < n && !l.hasOwnProperty(h[j])) { + m.bezierCurveTo(c + (+h[j]), b + (+h[j + 1]), g = c + (+h[j + 2]), f = b + (+h[j + 3]), c += +h[j + 4], b += +h[j + 5]); + j += 6 + } + break; + case "z": + m.closePath(); + break; + case "s": + if (!(k === "c" || k === "C" || k === "s" || k === "S")) { + g = c; + f = b + } + while (j < n && !l.hasOwnProperty(h[j])) { + m.bezierCurveTo(c + c - g, b + b - f, g = c + (+h[j]), f = b + (+h[j + 1]), c += +h[j + 2], b += +h[j + 3]); + j += 4 + } + break; + case "S": + if (!(k === "c" || k === "C" || k === "s" || k === "S")) { + g = c; + f = b + } + while (j < n && !l.hasOwnProperty(h[j])) { + m.bezierCurveTo(c + c - g, b + b - f, g = +h[j], f = +h[j + 1], c = (+h[j + 2]), b = (+h[j + 3])); + j += 4 + } + break; + case "q": + while (j < n && !l.hasOwnProperty(h[j])) { + m.quadraticCurveTo(g = c + (+h[j]), f = b + (+h[j + 1]), c += +h[j + 2], b += +h[j + 3]); + j += 4 + } + break; + case "Q": + while (j < n && !l.hasOwnProperty(h[j])) { + m.quadraticCurveTo(g = +h[j], f = +h[j + 1], c = +h[j + 2], b = +h[j + 3]); + j += 4 + } + break; + case "t": + if (!(k === "q" || k === "Q" || k === "t" || k === "T")) { + g = c; + f = b + } + while (j < n && !l.hasOwnProperty(h[j])) { + m.quadraticCurveTo(g = c + c - g, f = b + b - f, c += +h[j + 1], b += +h[j + 2]); + j += 2 + } + break; + case "T": + if (!(k === "q" || k === "Q" || k === "t" || k === "T")) { + g = c; + f = b + } + while (j < n && !l.hasOwnProperty(h[j])) { + m.quadraticCurveTo(g = c + c - g, f = b + b - f, c = (+h[j + 1]), b = (+h[j + 2])); + j += 2 + } + break; + case "h": + while (j < n && !l.hasOwnProperty(h[j])) { + m.lineTo(c += +h[j], b); + j++ + } + break; + case "H": + while (j < n && !l.hasOwnProperty(h[j])) { + m.lineTo(c = +h[j], b); + j++ + } + break; + case "v": + while (j < n && !l.hasOwnProperty(h[j])) { + m.lineTo(c, b += +h[j]); + j++ + } + break; + case "V": + while (j < n && !l.hasOwnProperty(h[j])) { + m.lineTo(c, b = +h[j]); + j++ + } + break + } + } + }, + clone: function() { + var a = this, + b = new Ext.draw.Path(); + b.params = a.params.slice(0); + b.commands = a.commands.slice(0); + b.cursor = a.cursor ? a.cursor.slice(0) : null; + b.startX = a.startX; + b.startY = a.startY; + b.svgString = a.svgString; + return b + }, + transform: function(j) { + if (j.isIdentity()) { + return + } + var a = j.getXX(), + f = j.getYX(), + m = j.getDX(), + l = j.getXY(), + e = j.getYY(), + k = j.getDY(), + b = this.params, + c = 0, + d = b.length, + h, g; + for (; c < d; c += 2) { + h = b[c]; + g = b[c + 1]; + b[c] = h * a + g * f + m; + b[c + 1] = h * l + g * e + k + } + this.dirt() + }, + getDimension: function(f) { + if (!f) { + f = {} + } + if (!this.commands || !this.commands.length) { + f.x = 0; + f.y = 0; + f.width = 0; + f.height = 0; + return f + } + f.left = Infinity; + f.top = Infinity; + f.right = -Infinity; + f.bottom = -Infinity; + var d = 0, + c = 0, + b = this.commands, + g = this.params, + e = b.length, + a, h; + for (; d < e; d++) { + switch (b[d]) { + case "M": + case "L": + a = g[c]; + h = g[c + 1]; + f.left = Math.min(a, f.left); + f.top = Math.min(h, f.top); + f.right = Math.max(a, f.right); + f.bottom = Math.max(h, f.bottom); + c += 2; + break; + case "C": + this.expandDimension(f, a, h, g[c], g[c + 1], g[c + 2], g[c + 3], a = g[c + 4], h = g[c + 5]); + c += 6; + break + } + } + f.x = f.left; + f.y = f.top; + f.width = f.right - f.left; + f.height = f.bottom - f.top; + return f + }, + getDimensionWithTransform: function(n, f) { + if (!this.commands || !this.commands.length) { + if (!f) { + f = {} + } + f.x = 0; + f.y = 0; + f.width = 0; + f.height = 0; + return f + } + f.left = Infinity; + f.top = Infinity; + f.right = -Infinity; + f.bottom = -Infinity; + var a = n.getXX(), + k = n.getYX(), + q = n.getDX(), + p = n.getXY(), + h = n.getYY(), + o = n.getDY(), + e = 0, + d = 0, + b = this.commands, + c = this.params, + g = b.length, + m, l; + for (; e < g; e++) { + switch (b[e]) { + case "M": + case "L": + m = c[d] * a + c[d + 1] * k + q; + l = c[d] * p + c[d + 1] * h + o; + f.left = Math.min(m, f.left); + f.top = Math.min(l, f.top); + f.right = Math.max(m, f.right); + f.bottom = Math.max(l, f.bottom); + d += 2; + break; + case "C": + this.expandDimension(f, m, l, c[d] * a + c[d + 1] * k + q, c[d] * p + c[d + 1] * h + o, c[d + 2] * a + c[d + 3] * k + q, c[d + 2] * p + c[d + 3] * h + o, m = c[d + 4] * a + c[d + 5] * k + q, l = c[d + 4] * p + c[d + 5] * h + o); + d += 6; + break + } + } + if (!f) { + f = {} + } + f.x = f.left; + f.y = f.top; + f.width = f.right - f.left; + f.height = f.bottom - f.top; + return f + }, + expandDimension: function(i, d, p, k, g, j, e, c, o) { + var m = this, + f = i.left, + a = i.right, + q = i.top, + n = i.bottom, + h = m.dim || (m.dim = []); + m.curveDimension(d, k, j, c, h); + f = Math.min(f, h[0]); + a = Math.max(a, h[1]); + m.curveDimension(p, g, e, o, h); + q = Math.min(q, h[0]); + n = Math.max(n, h[1]); + i.left = f; + i.right = a; + i.top = q; + i.bottom = n + }, + curveDimension: function(p, n, k, j, h) { + var i = 3 * (-p + 3 * (n - k) + j), + g = 6 * (p - 2 * n + k), + f = -3 * (p - n), + o, m, e = Math.min(p, j), + l = Math.max(p, j), + q; + if (i === 0) { + if (g === 0) { + h[0] = e; + h[1] = l; + return + } else { + o = -f / g; + if (0 < o && o < 1) { + m = this.interpolate(p, n, k, j, o); + e = Math.min(e, m); + l = Math.max(l, m) + } + } + } else { + q = g * g - 4 * i * f; + if (q >= 0) { + q = Math.sqrt(q); + o = (q - g) / 2 / i; + if (0 < o && o < 1) { + m = this.interpolate(p, n, k, j, o); + e = Math.min(e, m); + l = Math.max(l, m) + } + if (q > 0) { + o -= q / i; + if (0 < o && o < 1) { + m = this.interpolate(p, n, k, j, o); + e = Math.min(e, m); + l = Math.max(l, m) + } + } + } + } + h[0] = e; + h[1] = l + }, + interpolate: function(f, e, j, i, g) { + if (g === 0) { + return f + } + if (g === 1) { + return i + } + var h = (1 - g) / g; + return g * g * g * (i + h * (3 * j + h * (3 * e + h * f))) + }, + fromStripes: function(g) { + var e = this, + c = 0, + d = g.length, + b, a, f; + e.clear(); + for (; c < d; c++) { + f = g[c]; + e.params.push.apply(e.params, f); + e.commands.push("M"); + for (b = 2, a = f.length; b < a; b += 6) { + e.commands.push("C") + } + } + if (!e.cursor) { + e.cursor = [] + } + e.cursor[0] = e.params[e.params.length - 2]; + e.cursor[1] = e.params[e.params.length - 1]; + e.dirt() + }, + toStripes: function(k) { + var o = k || [], + p, n, m, b, a, h, g, f, e, c = this.commands, + d = this.params, + l = c.length; + for (f = 0, e = 0; f < l; f++) { + switch (c[f]) { + case "M": + p = [h = b = d[e++], g = a = d[e++]]; + o.push(p); + break; + case "L": + n = d[e++]; + m = d[e++]; + p.push((b + b + n) / 3, (a + a + m) / 3, (b + n + n) / 3, (a + m + m) / 3, b = n, a = m); + break; + case "C": + p.push(d[e++], d[e++], d[e++], d[e++], b = d[e++], a = d[e++]); + break; + case "Z": + n = h; + m = g; + p.push((b + b + n) / 3, (a + a + m) / 3, (b + n + n) / 3, (a + m + m) / 3, b = n, a = m); + break + } + } + return o + }, + updateSvgString: function() { + var b = [], + a = this.commands, + f = this.params, + e = a.length, + d = 0, + c = 0; + for (; d < e; d++) { + switch (a[d]) { + case "M": + b.push("M" + f[c] + "," + f[c + 1]); + c += 2; + break; + case "L": + b.push("L" + f[c] + "," + f[c + 1]); + c += 2; + break; + case "C": + b.push("C" + f[c] + "," + f[c + 1] + " " + f[c + 2] + "," + f[c + 3] + " " + f[c + 4] + "," + f[c + 5]); + c += 6; + break; + case "Z": + b.push("Z"); + break + } + } + this.svgString = b.join("") + }, + toString: function() { + if (!this.svgString) { + this.updateSvgString() + } + return this.svgString + } +}); +Ext.define("Ext.draw.overrides.Path", { + override: "Ext.draw.Path", + rayOrigin: { + x: -10000, + y: -10000 + }, + isPointInPath: function(o, n) { + var m = this, + c = m.commands, + q = Ext.draw.PathUtil, + p = m.rayOrigin, + f = m.params, + l = c.length, + e = null, + d = null, + b = 0, + a = 0, + k = 0, + h, g; + for (h = 0, g = 0; h < l; h++) { + switch (c[h]) { + case "M": + if (e !== null) { + if (q.linesIntersection(e, d, b, a, p.x, p.y, o, n)) { + k += 1 + } + } + e = b = f[g]; + d = a = f[g + 1]; + g += 2; + break; + case "L": + if (q.linesIntersection(b, a, f[g], f[g + 1], p.x, p.y, o, n)) { + k += 1 + } + b = f[g]; + a = f[g + 1]; + g += 2; + break; + case "C": + k += q.cubicLineIntersections(b, f[g], f[g + 2], f[g + 4], a, f[g + 1], f[g + 3], f[g + 5], p.x, p.y, o, n).length; + b = f[g + 4]; + a = f[g + 5]; + g += 6; + break; + case "Z": + if (e !== null) { + if (q.linesIntersection(e, d, b, a, p.x, p.y, o, n)) { + k += 1 + } + } + break + } + } + return k % 2 === 1 + }, + isPointOnPath: function(n, m) { + var l = this, + c = l.commands, + o = Ext.draw.PathUtil, + f = l.params, + k = c.length, + e = null, + d = null, + b = 0, + a = 0, + h, g; + for (h = 0, g = 0; h < k; h++) { + switch (c[h]) { + case "M": + if (e !== null) { + if (o.pointOnLine(e, d, b, a, n, m)) { + return true + } + } + e = b = f[g]; + d = a = f[g + 1]; + g += 2; + break; + case "L": + if (o.pointOnLine(b, a, f[g], f[g + 1], n, m)) { + return true + } + b = f[g]; + a = f[g + 1]; + g += 2; + break; + case "C": + if (o.pointOnCubic(b, f[g], f[g + 2], f[g + 4], a, f[g + 1], f[g + 3], f[g + 5], n, m)) { + return true + } + b = f[g + 4]; + a = f[g + 5]; + g += 6; + break; + case "Z": + if (e !== null) { + if (o.pointOnLine(e, d, b, a, n, m)) { + return true + } + } + break + } + } + return false + }, + getSegmentIntersections: function(t, d, s, c, r, b, o, a) { + var w = this, + g = arguments.length, + v = Ext.draw.PathUtil, + f = w.commands, + u = w.params, + k = f.length, + m = null, + l = null, + h = 0, + e = 0, + x = [], + q, n, p; + for (q = 0, n = 0; q < k; q++) { + switch (f[q]) { + case "M": + if (m !== null) { + switch (g) { + case 4: + p = v.linesIntersection(m, l, h, e, t, d, s, c); + if (p) { + x.push(p) + } + break; + case 8: + p = v.cubicLineIntersections(t, s, r, o, d, c, b, a, m, l, h, e); + x.push.apply(x, p); + break + } + } + m = h = u[n]; + l = e = u[n + 1]; + n += 2; + break; + case "L": + switch (g) { + case 4: + p = v.linesIntersection(h, e, u[n], u[n + 1], t, d, s, c); + if (p) { + x.push(p) + } + break; + case 8: + p = v.cubicLineIntersections(t, s, r, o, d, c, b, a, h, e, u[n], u[n + 1]); + x.push.apply(x, p); + break + } + h = u[n]; + e = u[n + 1]; + n += 2; + break; + case "C": + switch (g) { + case 4: + p = v.cubicLineIntersections(h, u[n], u[n + 2], u[n + 4], e, u[n + 1], u[n + 3], u[n + 5], t, d, s, c); + x.push.apply(x, p); + break; + case 8: + p = v.cubicsIntersections(h, u[n], u[n + 2], u[n + 4], e, u[n + 1], u[n + 3], u[n + 5], t, s, r, o, d, c, b, a); + x.push.apply(x, p); + break + } + h = u[n + 4]; + e = u[n + 5]; + n += 6; + break; + case "Z": + if (m !== null) { + switch (g) { + case 4: + p = v.linesIntersection(m, l, h, e, t, d, s, c); + if (p) { + x.push(p) + } + break; + case 8: + p = v.cubicLineIntersections(t, s, r, o, d, c, b, a, m, l, h, e); + x.push.apply(x, p); + break + } + } + break + } + } + return x + }, + getIntersections: function(o) { + var m = this, + c = m.commands, + g = m.params, + l = c.length, + f = null, + e = null, + b = 0, + a = 0, + d = [], + k, h, n; + for (k = 0, h = 0; k < l; k++) { + switch (c[k]) { + case "M": + if (f !== null) { + n = o.getSegmentIntersections.call(o, f, e, b, a); + d.push.apply(d, n) + } + f = b = g[h]; + e = a = g[h + 1]; + h += 2; + break; + case "L": + n = o.getSegmentIntersections.call(o, b, a, g[h], g[h + 1]); + d.push.apply(d, n); + b = g[h]; + a = g[h + 1]; + h += 2; + break; + case "C": + n = o.getSegmentIntersections.call(o, b, a, g[h], g[h + 1], g[h + 2], g[h + 3], g[h + 4], g[h + 5]); + d.push.apply(d, n); + b = g[h + 4]; + a = g[h + 5]; + h += 6; + break; + case "Z": + if (f !== null) { + n = o.getSegmentIntersections.call(o, f, e, b, a); + d.push.apply(d, n) + } + break + } + } + return d + } +}); +Ext.define("Ext.draw.sprite.Path", { + extend: "Ext.draw.sprite.Sprite", + requires: ["Ext.draw.Draw", "Ext.draw.Path"], + alias: ["sprite.path", "Ext.draw.Sprite"], + type: "path", + isPath: true, + inheritableStatics: { + def: { + processors: { + path: function(b, a) { + if (!(b instanceof Ext.draw.Path)) { + b = new Ext.draw.Path(b) + } + return b + } + }, + aliases: { + d: "path" + }, + triggers: { + path: "bbox" + }, + updaters: { + path: function(a) { + var b = a.path; + if (!b || b.bindAttr !== a) { + b = new Ext.draw.Path(); + b.bindAttr = a; + a.path = b + } + b.clear(); + this.updatePath(b, a); + this.scheduleUpdater(a, "bbox", ["path"]) + } + } + } + }, + updatePlainBBox: function(a) { + if (this.attr.path) { + this.attr.path.getDimension(a) + } + }, + updateTransformedBBox: function(a) { + if (this.attr.path) { + this.attr.path.getDimensionWithTransform(this.attr.matrix, a) + } + }, + render: function(b, c) { + var d = this.attr.matrix, + a = this.attr; + if (!a.path || a.path.params.length === 0) { + return + } + d.toContext(c); + c.appendPath(a.path); + c.fillStroke(a) + }, + updatePath: function(b, a) {} +}); +Ext.define("Ext.draw.overrides.sprite.Path", { + override: "Ext.draw.sprite.Path", + requires: ["Ext.draw.Color"], + isPointInPath: function(c, g) { + var b = this.attr; + if (b.fillStyle === Ext.draw.Color.RGBA_NONE) { + return this.isPointOnPath(c, g) + } + var e = b.path, + d = b.matrix, + f, a; + if (!d.isIdentity()) { + f = e.params.slice(0); + e.transform(b.matrix) + } + a = e.isPointInPath(c, g); + if (f) { + e.params = f + } + return a + }, + isPointOnPath: function(c, g) { + var b = this.attr, + e = b.path, + d = b.matrix, + f, a; + if (!d.isIdentity()) { + f = e.params.slice(0); + e.transform(b.matrix) + } + a = e.isPointOnPath(c, g); + if (f) { + e.params = f + } + return a + }, + hitTest: function(i, l) { + var e = this, + c = e.attr, + k = c.path, + g = c.matrix, + h = i[0], + f = i[1], + d = e.callParent([i, l]), + j = null, + a, b; + if (!d) { + return j + } + l = l || Ext.draw.sprite.Sprite.defaultHitTestOptions; + if (!g.isIdentity()) { + a = k.params.slice(0); + k.transform(c.matrix) + } + if (l.fill && l.stroke) { + b = c.fillStyle !== Ext.draw.Color.NONE && c.fillStyle !== Ext.draw.Color.RGBA_NONE; + if (b) { + if (k.isPointInPath(h, f)) { + j = { + sprite: e + } + } + } else { + if (k.isPointInPath(h, f) || k.isPointOnPath(h, f)) { + j = { + sprite: e + } + } + } + } else { + if (l.stroke && !l.fill) { + if (k.isPointOnPath(h, f)) { + j = { + sprite: e + } + } + } else { + if (l.fill && !l.stroke) { + if (k.isPointInPath(h, f)) { + j = { + sprite: e + } + } + } + } + } + if (a) { + k.params = a + } + return j + }, + getIntersections: function(j) { + if (!(j.isSprite && j.isPath)) { + return [] + } + var e = this.attr, + d = j.attr, + i = e.path, + h = d.path, + g = e.matrix, + a = d.matrix, + c, f, b; + if (!g.isIdentity()) { + c = i.params.slice(0); + i.transform(e.matrix) + } + if (!a.isIdentity()) { + f = h.params.slice(0); + h.transform(d.matrix) + } + b = i.getIntersections(h); + if (c) { + i.params = c + } + if (f) { + h.params = f + } + return b + } +}); +Ext.define("Ext.draw.sprite.Circle", { + extend: "Ext.draw.sprite.Path", + alias: "sprite.circle", + type: "circle", + inheritableStatics: { + def: { + processors: { + cx: "number", + cy: "number", + r: "number" + }, + aliases: { + radius: "r", + x: "cx", + y: "cy", + centerX: "cx", + centerY: "cy" + }, + defaults: { + cx: 0, + cy: 0, + r: 4 + }, + triggers: { + cx: "path", + cy: "path", + r: "path" + } + } + }, + updatePlainBBox: function(c) { + var b = this.attr, + a = b.cx, + e = b.cy, + d = b.r; + c.x = a - d; + c.y = e - d; + c.width = d + d; + c.height = d + d + }, + updateTransformedBBox: function(d) { + var g = this.attr, + f = g.cx, + e = g.cy, + a = g.r, + h = g.matrix, + j = h.getScaleX(), + i = h.getScaleY(), + c, b; + c = j * a; + b = i * a; + d.x = h.x(f, e) - c; + d.y = h.y(f, e) - b; + d.width = c + c; + d.height = b + b + }, + updatePath: function(b, a) { + b.arc(a.cx, a.cy, a.r, 0, Math.PI * 2, false) + } +}); +Ext.define("Ext.draw.sprite.Arc", { + extend: "Ext.draw.sprite.Circle", + alias: "sprite.arc", + type: "arc", + inheritableStatics: { + def: { + processors: { + startAngle: "number", + endAngle: "number", + anticlockwise: "bool" + }, + aliases: { + from: "startAngle", + to: "endAngle", + start: "startAngle", + end: "endAngle" + }, + defaults: { + startAngle: 0, + endAngle: Math.PI * 2, + anticlockwise: false + }, + triggers: { + startAngle: "path", + endAngle: "path", + anticlockwise: "path" + } + } + }, + updatePath: function(b, a) { + b.arc(a.cx, a.cy, a.r, a.startAngle, a.endAngle, a.anticlockwise) + } +}); +Ext.define("Ext.draw.sprite.Arrow", { + extend: "Ext.draw.sprite.Path", + alias: "sprite.arrow", + inheritableStatics: { + def: { + processors: { + x: "number", + y: "number", + size: "number" + }, + defaults: { + x: 0, + y: 0, + size: 4 + }, + triggers: { + x: "path", + y: "path", + size: "path" + } + } + }, + updatePath: function(d, b) { + var c = b.size * 1.5, + a = b.x - b.lineWidth / 2, + e = b.y; + d.fromSvgString("M".concat(a - c * 0.7, ",", e - c * 0.4, "l", [c * 0.6, 0, 0, -c * 0.4, c, c * 0.8, -c, c * 0.8, 0, -c * 0.4, -c * 0.6, 0], "z")) + } +}); +Ext.define("Ext.draw.sprite.Composite", { + extend: "Ext.draw.sprite.Sprite", + alias: "sprite.composite", + type: "composite", + isComposite: true, + config: { + sprites: [] + }, + constructor: function() { + this.sprites = []; + this.sprites.map = {}; + this.callParent(arguments) + }, + add: function(c) { + if (!c) { + return null + } + if (!c.isSprite) { + c = Ext.create("sprite." + c.type, c); + c.setParent(this); + c.setSurface(this.getSurface()) + } + var d = this, + a = d.attr, + b = c.applyTransformations; + c.applyTransformations = function() { + if (c.attr.dirtyTransform) { + a.dirtyTransform = true; + a.bbox.plain.dirty = true; + a.bbox.transform.dirty = true + } + b.call(c) + }; + d.sprites.push(c); + d.sprites.map[c.id] = c.getId(); + a.bbox.plain.dirty = true; + a.bbox.transform.dirty = true; + return c + }, + updateSurface: function(a) { + for (var b = 0, c = this.sprites.length; b < c; b++) { + this.sprites[b].setSurface(a) + } + }, + addAll: function(b) { + if (b.isSprite || b.type) { + this.add(b) + } else { + if (Ext.isArray(b)) { + var a = 0; + while (a < b.length) { + this.add(b[a++]) + } + } + } + }, + updatePlainBBox: function(g) { + var e = this, + b = Infinity, + h = -Infinity, + f = Infinity, + a = -Infinity, + j, k, c, d; + for (c = 0, d = e.sprites.length; c < d; c++) { + j = e.sprites[c]; + j.applyTransformations(); + k = j.getBBox(); + if (b > k.x) { + b = k.x + } + if (h < k.x + k.width) { + h = k.x + k.width + } + if (f > k.y) { + f = k.y + } + if (a < k.y + k.height) { + a = k.y + k.height + } + } + g.x = b; + g.y = f; + g.width = h - b; + g.height = a - f + }, + render: function(a, b, f) { + var d = this.attr.matrix, + c, e; + d.toContext(b); + for (c = 0, e = this.sprites.length; c < e; c++) { + a.renderSprite(this.sprites[c], f) + } + }, + destroy: function() { + var c = this, + d = c.sprites, + b = d.length, + a; + c.callParent(); + for (a = 0; a < b; a++) { + d[a].destroy() + } + d.length = 0 + } +}); +Ext.define("Ext.draw.sprite.Cross", { + extend: "Ext.draw.sprite.Path", + alias: "sprite.cross", + inheritableStatics: { + def: { + processors: { + x: "number", + y: "number", + size: "number" + }, + defaults: { + x: 0, + y: 0, + size: 4 + }, + triggers: { + x: "path", + y: "path", + size: "path" + } + } + }, + updatePath: function(d, b) { + var c = b.size / 1.7, + a = b.x - b.lineWidth / 2, + e = b.y; + d.fromSvgString("M".concat(a - c, ",", e, "l", [-c, -c, c, -c, c, c, c, -c, c, c, -c, c, c, c, -c, c, -c, -c, -c, c, -c, -c, "z"])) + } +}); +Ext.define("Ext.draw.sprite.Diamond", { + extend: "Ext.draw.sprite.Path", + alias: "sprite.diamond", + inheritableStatics: { + def: { + processors: { + x: "number", + y: "number", + size: "number" + }, + defaults: { + x: 0, + y: 0, + size: 4 + }, + triggers: { + x: "path", + y: "path", + size: "path" + } + } + }, + updatePath: function(d, b) { + var c = b.size * 1.25, + a = b.x - b.lineWidth / 2, + e = b.y; + d.fromSvgString(["M", a, e - c, "l", c, c, -c, c, -c, -c, c, -c, "z"]) + } +}); +Ext.define("Ext.draw.sprite.Ellipse", { + extend: "Ext.draw.sprite.Path", + alias: "sprite.ellipse", + type: "ellipse", + inheritableStatics: { + def: { + processors: { + cx: "number", + cy: "number", + rx: "number", + ry: "number", + axisRotation: "number" + }, + aliases: { + radius: "r", + x: "cx", + y: "cy", + centerX: "cx", + centerY: "cy", + radiusX: "rx", + radiusY: "ry" + }, + defaults: { + cx: 0, + cy: 0, + rx: 1, + ry: 1, + axisRotation: 0 + }, + triggers: { + cx: "path", + cy: "path", + rx: "path", + ry: "path", + axisRotation: "path" + } + } + }, + updatePlainBBox: function(c) { + var b = this.attr, + a = b.cx, + f = b.cy, + e = b.rx, + d = b.ry; + c.x = a - e; + c.y = f - d; + c.width = e + e; + c.height = d + d + }, + updateTransformedBBox: function(d) { + var i = this.attr, + f = i.cx, + e = i.cy, + c = i.rx, + b = i.ry, + l = b / c, + m = i.matrix.clone(), + a, q, k, j, p, o, n, g; + m.append(1, 0, 0, l, 0, e * (1 - l)); + a = m.getXX(); + k = m.getYX(); + p = m.getDX(); + q = m.getXY(); + j = m.getYY(); + o = m.getDY(); + n = Math.sqrt(a * a + k * k) * c; + g = Math.sqrt(q * q + j * j) * c; + d.x = f * a + e * k + p - n; + d.y = f * q + e * j + o - g; + d.width = n + n; + d.height = g + g + }, + updatePath: function(b, a) { + b.ellipse(a.cx, a.cy, a.rx, a.ry, a.axisRotation, 0, Math.PI * 2, false) + } +}); +Ext.define("Ext.draw.sprite.EllipticalArc", { + extend: "Ext.draw.sprite.Ellipse", + alias: "sprite.ellipticalArc", + type: "ellipticalArc", + inheritableStatics: { + def: { + processors: { + startAngle: "number", + endAngle: "number", + anticlockwise: "bool" + }, + aliases: { + from: "startAngle", + to: "endAngle", + start: "startAngle", + end: "endAngle" + }, + defaults: { + startAngle: 0, + endAngle: Math.PI * 2, + anticlockwise: false + }, + triggers: { + startAngle: "path", + endAngle: "path", + anticlockwise: "path" + } + } + }, + updatePath: function(b, a) { + b.ellipse(a.cx, a.cy, a.rx, a.ry, a.axisRotation, a.startAngle, a.endAngle, a.anticlockwise) + } +}); +Ext.define("Ext.draw.sprite.Rect", { + extend: "Ext.draw.sprite.Path", + alias: "sprite.rect", + type: "rect", + inheritableStatics: { + def: { + processors: { + x: "number", + y: "number", + width: "number", + height: "number", + radius: "number" + }, + aliases: {}, + triggers: { + x: "path", + y: "path", + width: "path", + height: "path", + radius: "path" + }, + defaults: { + x: 0, + y: 0, + width: 8, + height: 8, + radius: 0 + } + } + }, + updatePlainBBox: function(b) { + var a = this.attr; + b.x = a.x; + b.y = a.y; + b.width = a.width; + b.height = a.height + }, + updateTransformedBBox: function(a, b) { + this.attr.matrix.transformBBox(b, this.attr.radius, a) + }, + updatePath: function(f, d) { + var c = d.x, + g = d.y, + e = d.width, + b = d.height, + a = Math.min(d.radius, Math.abs(d.height) * 0.5, Math.abs(d.width) * 0.5); + if (a === 0) { + f.rect(c, g, e, b) + } else { + f.moveTo(c + a, g); + f.arcTo(c + e, g, c + e, g + b, a); + f.arcTo(c + e, g + b, c, g + b, a); + f.arcTo(c, g + b, c, g, a); + f.arcTo(c, g, c + a, g, a) + } + } +}); +Ext.define("Ext.draw.sprite.Image", { + extend: "Ext.draw.sprite.Rect", + alias: "sprite.image", + type: "image", + statics: { + imageLoaders: {} + }, + inheritableStatics: { + def: { + processors: { + src: "string" + }, + defaults: { + src: "", + width: null, + height: null + } + } + }, + render: function(c, o) { + var j = this, + h = j.attr, + n = h.matrix, + a = h.src, + l = h.x, + k = h.y, + b = h.width, + m = h.height, + g = Ext.draw.sprite.Image.imageLoaders[a], + f, d, e; + if (g && g.done) { + n.toContext(o); + d = g.image; + o.drawImage(d, l, k, b || (d.naturalWidth || d.width) / c.devicePixelRatio, m || (d.naturalHeight || d.height) / c.devicePixelRatio) + } else { + if (!g) { + f = new Image(); + g = Ext.draw.sprite.Image.imageLoaders[a] = { + image: f, + done: false, + pendingSprites: [j], + pendingSurfaces: [c] + }; + f.width = b; + f.height = m; + f.onload = function() { + if (!g.done) { + g.done = true; + for (e = 0; e < g.pendingSprites.length; e++) { + g.pendingSprites[e].setDirty(true) + } + for (e in g.pendingSurfaces) { + g.pendingSurfaces[e].renderFrame() + } + } + }; + f.src = a + } else { + Ext.Array.include(g.pendingSprites, j); + Ext.Array.include(g.pendingSurfaces, c) + } + } + } +}); +Ext.define("Ext.draw.sprite.Instancing", { + extend: "Ext.draw.sprite.Sprite", + alias: "sprite.instancing", + type: "instancing", + isInstancing: true, + config: { + template: null + }, + instances: null, + applyTemplate: function(a) { + if (!a.isSprite) { + if (!a.xclass && !a.type) { + a.type = "circle" + } + a = Ext.create(a.xclass || "sprite." + a.type, a) + } + a.setParent(this); + return a + }, + updateTemplate: function(a, b) { + if (b) { + delete b.ownAttr + } + a.setSurface(this.getSurface()); + a.ownAttr = a.attr; + this.clearAll() + }, + updateSurface: function(a) { + var b = this.getTemplate(); + if (b) { + b.setSurface(a) + } + }, + get: function(a) { + return this.instances[a] + }, + getCount: function() { + return this.instances.length + }, + clearAll: function() { + var a = this.getTemplate(); + a.attr.children = this.instances = []; + this.position = 0 + }, + createInstance: function(d, f, c) { + var e = this.getTemplate(), + b = e.attr, + a = Ext.Object.chain(b); + e.topModifier.prepareAttributes(a); + e.attr = a; + e.setAttributes(d, f, c); + a.template = e; + this.instances.push(a); + e.attr = b; + this.position++; + return a + }, + getBBox: function() { + return null + }, + getBBoxFor: function(b, d) { + var c = this.getTemplate(), + a = c.attr, + e; + c.attr = this.instances[b]; + e = c.getBBox(d); + c.attr = a; + return e + }, + isVisible: function() { + var b = this.attr, + c = this.getParent(), + a; + a = c && c.isSurface && !b.hidden && b.globalAlpha; + return !!a + }, + isInstanceVisible: function(c) { + var e = this, + d = e.getTemplate(), + b = d.attr, + f = e.instances, + a = false; + if (!Ext.isNumber(c) || c < 0 || c >= f.length || !e.isVisible()) { + return a + } + d.attr = f[c]; + a = d.isVisible(point, options); + d.attr = b; + return a + }, + render: function(b, l, d, h) { + var g = this, + j = g.getTemplate(), + k = g.attr.matrix, + c = j.attr, + a = g.instances, + e, f = g.position; + k.toContext(l); + j.preRender(b, l, d, h); + j.useAttributes(l, h); + for (e = 0; e < f; e++) { + if (a[e].dirtyZIndex) { + break + } + } + for (e = 0; e < f; e++) { + if (a[e].hidden) { + continue + } + l.save(); + j.attr = a[e]; + j.useAttributes(l, h); + j.render(b, l, d, h); + l.restore() + } + j.attr = c + }, + setAttributesFor: function(c, e, f) { + var d = this.getTemplate(), + b = d.attr, + a = this.instances[c]; + if (!a) { + return + } + d.attr = a; + if (f) { + e = Ext.apply({}, e) + } else { + e = d.self.def.normalize(e) + } + d.topModifier.pushDown(a, e); + d.attr = b + }, + destroy: function() { + var b = this, + a = b.getTemplate(); + b.instances = null; + if (a) { + a.destroy() + } + b.callParent() + } +}); +Ext.define("Ext.draw.overrides.sprite.Instancing", { + override: "Ext.draw.sprite.Instancing", + hitTest: function(f, j) { + var e = this, + g = e.getTemplate(), + b = g.attr, + a = e.instances, + d = a.length, + c = 0, + h = null; + if (!e.isVisible()) { + return h + } + for (; c < d; c++) { + g.attr = a[c]; + h = g.hitTest(f, j); + if (h) { + h.isInstance = true; + h.template = h.sprite; + h.sprite = this; + h.instance = a[c]; + h.index = c; + return h + } + } + g.attr = b; + return h + } +}); +Ext.define("Ext.draw.sprite.Line", { + extend: "Ext.draw.sprite.Sprite", + alias: "sprite.line", + type: "line", + inheritableStatics: { + def: { + processors: { + fromX: "number", + fromY: "number", + toX: "number", + toY: "number" + }, + defaults: { + fromX: 0, + fromY: 0, + toX: 1, + toY: 1, + strokeStyle: "black" + }, + aliases: { + x1: "fromX", + y1: "fromY", + x2: "toX", + y2: "toY" + } + } + }, + updateLineBBox: function(b, i, s, g, r, f) { + var o = this.attr, + q = o.matrix, + h = o.lineWidth / 2, + m, l, d, c, k, j, n; + if (i) { + n = q.transformPoint([s, g]); + s = n[0]; + g = n[1]; + n = q.transformPoint([r, f]); + r = n[0]; + f = n[1] + } + m = Math.min(s, r); + d = Math.max(s, r); + l = Math.min(g, f); + c = Math.max(g, f); + var t = Math.atan2(d - m, c - l), + a = Math.sin(t), + e = Math.cos(t), + k = h * e, + j = h * a; + m -= k; + l -= j; + d += k; + c += j; + b.x = m; + b.y = l; + b.width = d - m; + b.height = c - l + }, + updatePlainBBox: function(b) { + var a = this.attr; + this.updateLineBBox(b, false, a.fromX, a.fromY, a.toX, a.toY) + }, + updateTransformedBBox: function(b, c) { + var a = this.attr; + this.updateLineBBox(b, true, a.fromX, a.fromY, a.toX, a.toY) + }, + render: function(b, c) { + var a = this.attr, + d = this.attr.matrix; + d.toContext(c); + c.beginPath(); + c.moveTo(a.fromX, a.fromY); + c.lineTo(a.toX, a.toY); + c.stroke() + } +}); +Ext.define("Ext.draw.sprite.Plus", { + extend: "Ext.draw.sprite.Path", + alias: "sprite.plus", + inheritableStatics: { + def: { + processors: { + x: "number", + y: "number", + size: "number" + }, + defaults: { + x: 0, + y: 0, + size: 4 + }, + triggers: { + x: "path", + y: "path", + size: "path" + } + } + }, + updatePath: function(d, b) { + var c = b.size / 1.3, + a = b.x - b.lineWidth / 2, + e = b.y; + d.fromSvgString("M".concat(a - c / 2, ",", e - c / 2, "l", [0, -c, c, 0, 0, c, c, 0, 0, c, -c, 0, 0, c, -c, 0, 0, -c, -c, 0, 0, -c, "z"])) + } +}); +Ext.define("Ext.draw.sprite.Sector", { + extend: "Ext.draw.sprite.Path", + alias: "sprite.sector", + type: "sector", + inheritableStatics: { + def: { + processors: { + centerX: "number", + centerY: "number", + startAngle: "number", + endAngle: "number", + startRho: "number", + endRho: "number", + margin: "number" + }, + aliases: { + rho: "endRho" + }, + triggers: { + centerX: "path,bbox", + centerY: "path,bbox", + startAngle: "path,bbox", + endAngle: "path,bbox", + startRho: "path,bbox", + endRho: "path,bbox", + margin: "path,bbox" + }, + defaults: { + centerX: 0, + centerY: 0, + startAngle: 0, + endAngle: 0, + startRho: 0, + endRho: 150, + margin: 0, + path: "M 0,0" + } + } + }, + getMidAngle: function() { + return this.midAngle || 0 + }, + updatePath: function(j, h) { + var g = Math.min(h.startAngle, h.endAngle), + c = Math.max(h.startAngle, h.endAngle), + b = this.midAngle = (g + c) * 0.5, + d = h.margin, + f = h.centerX, + e = h.centerY, + i = Math.min(h.startRho, h.endRho), + a = Math.max(h.startRho, h.endRho); + if (d) { + f += d * Math.cos(b); + e += d * Math.sin(b) + } + j.moveTo(f + i * Math.cos(g), e + i * Math.sin(g)); + j.lineTo(f + a * Math.cos(g), e + a * Math.sin(g)); + j.arc(f, e, a, g, c, false); + j.lineTo(f + i * Math.cos(c), e + i * Math.sin(c)); + j.arc(f, e, i, c, g, true) + } +}); +Ext.define("Ext.draw.sprite.Square", { + extend: "Ext.draw.sprite.Rect", + alias: "sprite.square", + inheritableStatics: { + def: { + processors: { + size: "number" + }, + defaults: { + size: 4 + }, + triggers: { + size: "size" + }, + updaters: { + size: function(a) { + var c = a.size, + b = a.lineWidth / 2; + this.setAttributes({ + x: a.x - c - b, + y: a.y - c, + height: 2 * c, + width: 2 * c + }) + } + } + } + } +}); +Ext.define("Ext.draw.TextMeasurer", { + singleton: true, + requires: ["Ext.util.TextMetrics"], + measureDiv: null, + measureCache: {}, + precise: Ext.isIE8, + measureDivTpl: { + tag: "div", + style: { + overflow: "hidden", + position: "relative", + "float": "left", + width: 0, + height: 0 + }, + children: { + tag: "div", + style: { + display: "block", + position: "absolute", + x: -100000, + y: -100000, + padding: 0, + margin: 0, + "z-index": -100000, + "white-space": "nowrap" + } + } + }, + actualMeasureText: function(g, b) { + var e = Ext.draw.TextMeasurer, + f = e.measureDiv, + a = 100000, + c; + if (!f) { + var d = Ext.Element.create({ + style: { + overflow: "hidden", + position: "relative", + "float": "left", + width: 0, + height: 0 + } + }); + e.measureDiv = f = Ext.Element.create({ + style: { + position: "absolute", + x: a, + y: a, + "z-index": -a, + "white-space": "nowrap", + display: "block", + padding: 0, + margin: 0 + } + }); + Ext.getBody().appendChild(d); + d.appendChild(f) + } + if (b) { + f.setStyle({ + font: b, + lineHeight: "normal" + }) + } + f.setText("(" + g + ")"); + c = f.getSize(); + f.setText("()"); + c.width -= f.getSize().width; + return c + }, + measureTextSingleLine: function(h, d) { + if (this.precise) { + return this.preciseMeasureTextSingleLine(h, d) + } + h = h.toString(); + var a = this.measureCache, + g = h.split(""), + c = 0, + j = 0, + l, b, e, f, k; + if (!a[d]) { + a[d] = {} + } + a = a[d]; + if (a[h]) { + return a[h] + } + for (e = 0, f = g.length; e < f; e++) { + b = g[e]; + if (!(l = a[b])) { + k = this.actualMeasureText(b, d); + l = a[b] = k + } + c += l.width; + j = Math.max(j, l.height) + } + return a[h] = { + width: c, + height: j + } + }, + preciseMeasureTextSingleLine: function(c, a) { + c = c.toString(); + var b = this.measureDiv || (this.measureDiv = Ext.getBody().createChild(this.measureDivTpl).down("div")); + b.setStyle({ + font: a || "" + }); + return Ext.util.TextMetrics.measure(b, c) + }, + measureText: function(e, b) { + var h = e.split("\n"), + d = h.length, + f = 0, + a = 0, + j, c, g; + if (d === 1) { + return this.measureTextSingleLine(e, b) + } + g = []; + for (c = 0; c < d; c++) { + j = this.measureTextSingleLine(h[c], b); + g.push(j); + f += j.height; + a = Math.max(a, j.width) + } + return { + width: a, + height: f, + sizes: g + } + } +}); +Ext.define("Ext.draw.sprite.Text", function() { + var d = { + "xx-small": true, + "x-small": true, + small: true, + medium: true, + large: true, + "x-large": true, + "xx-large": true + }; + var b = { + normal: true, + bold: true, + bolder: true, + lighter: true, + 100: true, + 200: true, + 300: true, + 400: true, + 500: true, + 600: true, + 700: true, + 800: true, + 900: true + }; + var a = { + start: "start", + left: "start", + center: "center", + middle: "center", + end: "end", + right: "end" + }; + var c = { + top: "top", + hanging: "hanging", + middle: "middle", + center: "middle", + alphabetic: "alphabetic", + ideographic: "ideographic", + bottom: "bottom" + }; + return { + extend: "Ext.draw.sprite.Sprite", + requires: ["Ext.draw.TextMeasurer", "Ext.draw.Color"], + alias: "sprite.text", + type: "text", + lineBreakRe: /\r?\n/g, + inheritableStatics: { + def: { + animationProcessors: { + text: "text" + }, + processors: { + x: "number", + y: "number", + text: "string", + fontSize: function(e) { + if (Ext.isNumber(+e)) { + return e + "px" + } else { + if (e.match(Ext.dom.Element.unitRe)) { + return e + } else { + if (e in d) { + return e + } + } + } + }, + fontStyle: "enums(,italic,oblique)", + fontVariant: "enums(,small-caps)", + fontWeight: function(e) { + if (e in b) { + return String(e) + } else { + return "" + } + }, + fontFamily: "string", + textAlign: function(e) { + return a[e] || "center" + }, + textBaseline: function(e) { + return c[e] || "alphabetic" + }, + font: "string" + }, + aliases: { + "font-size": "fontSize", + "font-family": "fontFamily", + "font-weight": "fontWeight", + "font-variant": "fontVariant", + "text-anchor": "textAlign" + }, + defaults: { + fontStyle: "", + fontVariant: "", + fontWeight: "", + fontSize: "10px", + fontFamily: "sans-serif", + font: "10px sans-serif", + textBaseline: "alphabetic", + textAlign: "start", + strokeStyle: "rgba(0, 0, 0, 0)", + fillStyle: "#000", + x: 0, + y: 0, + text: "" + }, + triggers: { + fontStyle: "fontX,bbox", + fontVariant: "fontX,bbox", + fontWeight: "fontX,bbox", + fontSize: "fontX,bbox", + fontFamily: "fontX,bbox", + font: "font,bbox,canvas", + textBaseline: "bbox", + textAlign: "bbox", + x: "bbox", + y: "bbox", + text: "bbox" + }, + updaters: { + fontX: "makeFontShorthand", + font: "parseFontShorthand" + } + } + }, + constructor: function(e) { + if (e && e.font) { + e = Ext.clone(e); + for (var f in e) { + if (f !== "font" && f.indexOf("font") === 0) { + delete e[f] + } + } + } + Ext.draw.sprite.Sprite.prototype.constructor.call(this, e) + }, + fontValuesMap: { + italic: "fontStyle", + oblique: "fontStyle", + "small-caps": "fontVariant", + bold: "fontWeight", + bolder: "fontWeight", + lighter: "fontWeight", + "100": "fontWeight", + "200": "fontWeight", + "300": "fontWeight", + "400": "fontWeight", + "500": "fontWeight", + "600": "fontWeight", + "700": "fontWeight", + "800": "fontWeight", + "900": "fontWeight", + "xx-small": "fontSize", + "x-small": "fontSize", + small: "fontSize", + medium: "fontSize", + large: "fontSize", + "x-large": "fontSize", + "xx-large": "fontSize" + }, + makeFontShorthand: function(e) { + var f = []; + if (e.fontStyle) { + f.push(e.fontStyle) + } + if (e.fontVariant) { + f.push(e.fontVariant) + } + if (e.fontWeight) { + f.push(e.fontWeight) + } + if (e.fontSize) { + f.push(e.fontSize) + } + if (e.fontFamily) { + f.push(e.fontFamily) + } + this.setAttributes({ + font: f.join(" ") + }, true) + }, + parseFontShorthand: function(j) { + var m = j.font, + k = m.length, + l = {}, + n = this.fontValuesMap, + e = 0, + i, g, f, h; + while (e < k && i !== -1) { + i = m.indexOf(" ", e); + if (i < 0) { + f = m.substr(e) + } else { + if (i > e) { + f = m.substr(e, i - e) + } else { + continue + } + } + g = f.indexOf("/"); + if (g > 0) { + f = f.substr(0, g) + } else { + if (g === 0) { + continue + } + } + if (f !== "normal" && f !== "inherit") { + h = n[f]; + if (h) { + l[h] = f + } else { + if (f.match(Ext.dom.Element.unitRe)) { + l.fontSize = f + } else { + l.fontFamily = m.substr(e); + break + } + } + } + e = i + 1 + } + if (!l.fontStyle) { + l.fontStyle = "" + } + if (!l.fontVariant) { + l.fontVariant = "" + } + if (!l.fontWeight) { + l.fontWeight = "" + } + this.setAttributes(l, true) + }, + fontProperties: { + fontStyle: true, + fontVariant: true, + fontWeight: true, + fontSize: true, + fontFamily: true + }, + setAttributes: function(g, i, e) { + var f, h; + if (g && g.font) { + h = {}; + for (f in g) { + if (!(f in this.fontProperties)) { + h[f] = g[f] + } + } + g = h + } + this.callParent([g, i, e]) + }, + getBBox: function(g) { + var h = this, + f = h.attr.bbox.plain, + e = h.getSurface(); + if (f.dirty) { + h.updatePlainBBox(f); + f.dirty = false + } + if (e.getInherited().rtl && e.getFlipRtlText()) { + h.updatePlainBBox(f, true) + } + return h.callParent([g]) + }, + rtlAlignments: { + start: "end", + center: "center", + end: "start" + }, + updatePlainBBox: function(k, B) { + var C = this, + w = C.attr, + o = w.x, + n = w.y, + q = [], + t = w.font, + r = w.text, + s = w.textBaseline, + l = w.textAlign, + u = (B && C.oldSize) ? C.oldSize : (C.oldSize = Ext.draw.TextMeasurer.measureText(r, t)), + z = C.getSurface(), + p = z.getInherited().rtl, + v = p && z.getFlipRtlText(), + h = z.getRect(), + f = u.sizes, + g = u.height, + j = u.width, + m = f ? f.length : 0, + e, A = 0; + switch (s) { + case "hanging": + case "top": + break; + case "ideographic": + case "bottom": + n -= g; + break; + case "alphabetic": + n -= g * 0.8; + break; + case "middle": + n -= g * 0.5; + break + } + if (v) { + o = h[2] - h[0] - o; + l = C.rtlAlignments[l] + } + switch (l) { + case "start": + if (p) { + for (; A < m; A++) { + e = f[A].width; + q.push(-(j - e)) + } + } + break; + case "end": + o -= j; + if (p) { + break + } + for (; A < m; A++) { + e = f[A].width; + q.push(j - e) + } + break; + case "center": + o -= j * 0.5; + for (; A < m; A++) { + e = f[A].width; + q.push((p ? -1 : 1) * (j - e) * 0.5) + } + break + } + w.textAlignOffsets = q; + k.x = o; + k.y = n; + k.width = j; + k.height = g + }, + setText: function(e) { + this.setAttributes({ + text: e + }, true) + }, + render: function(e, q, k) { + var h = this, + g = h.attr, + p = Ext.draw.Matrix.fly(g.matrix.elements.slice(0)), + o = h.getBBox(true), + s = g.textAlignOffsets, + m = Ext.draw.Color.RGBA_NONE, + l, j, f, r, n; + if (g.text.length === 0) { + return + } + r = g.text.split(h.lineBreakRe); + n = o.height / r.length; + l = g.bbox.plain.x; + j = g.bbox.plain.y + n * 0.78; + p.toContext(q); + if (e.getInherited().rtl) { + l += g.bbox.plain.width + } + for (f = 0; f < r.length; f++) { + if (q.fillStyle !== m) { + q.fillText(r[f], l + (s[f] || 0), j + n * f) + } + if (q.strokeStyle !== m) { + q.strokeText(r[f], l + (s[f] || 0), j + n * f) + } + } + } + } +}); +Ext.define("Ext.draw.sprite.Tick", { + extend: "Ext.draw.sprite.Line", + alias: "sprite.tick", + inheritableStatics: { + def: { + processors: { + x: "number", + y: "number", + size: "number" + }, + defaults: { + x: 0, + y: 0, + size: 4 + }, + triggers: { + x: "tick", + y: "tick", + size: "tick" + }, + updaters: { + tick: function(b) { + var d = b.size * 1.5, + c = b.lineWidth / 2, + a = b.x, + e = b.y; + this.setAttributes({ + fromX: a - c, + fromY: e - d, + toX: a - c, + toY: e + d + }) + } + } + } + } +}); +Ext.define("Ext.draw.sprite.Triangle", { + extend: "Ext.draw.sprite.Path", + alias: "sprite.triangle", + inheritableStatics: { + def: { + processors: { + x: "number", + y: "number", + size: "number" + }, + defaults: { + x: 0, + y: 0, + size: 4 + }, + triggers: { + x: "path", + y: "path", + size: "path" + } + } + }, + updatePath: function(d, b) { + var c = b.size * 2.2, + a = b.x, + e = b.y; + d.fromSvgString("M".concat(a, ",", e, "m0-", c * 0.58, "l", c * 0.5, ",", c * 0.87, "-", c, ",0z")) + } +}); +Ext.define("Ext.draw.gradient.Linear", { + extend: "Ext.draw.gradient.Gradient", + requires: ["Ext.draw.Color"], + type: "linear", + config: { + degrees: 0, + radians: 0 + }, + applyRadians: function(b, a) { + if (Ext.isNumber(b)) { + return b + } + return a + }, + applyDegrees: function(b, a) { + if (Ext.isNumber(b)) { + return b + } + return a + }, + updateRadians: function(a) { + this.setDegrees(Ext.draw.Draw.degrees(a)) + }, + updateDegrees: function(a) { + this.setRadians(Ext.draw.Draw.rad(a)) + }, + generateGradient: function(q, o) { + var c = this.getRadians(), + p = Math.cos(c), + j = Math.sin(c), + m = o.width, + f = o.height, + d = o.x + m * 0.5, + b = o.y + f * 0.5, + n = this.getStops(), + g = n.length, + k, a, e; + if (Ext.isNumber(d + b) && f > 0 && m > 0) { + a = (Math.sqrt(f * f + m * m) * Math.abs(Math.cos(c - Math.atan(f / m)))) / 2; + k = q.createLinearGradient(d + p * a, b + j * a, d - p * a, b - j * a); + for (e = 0; e < g; e++) { + k.addColorStop(n[e].offset, n[e].color) + } + return k + } + return Ext.draw.Color.NONE + } +}); +Ext.define("Ext.draw.gradient.Radial", { + extend: "Ext.draw.gradient.Gradient", + type: "radial", + config: { + start: { + x: 0, + y: 0, + r: 0 + }, + end: { + x: 0, + y: 0, + r: 1 + } + }, + applyStart: function(a, b) { + if (!b) { + return a + } + var c = { + x: b.x, + y: b.y, + r: b.r + }; + if ("x" in a) { + c.x = a.x + } else { + if ("centerX" in a) { + c.x = a.centerX + } + } + if ("y" in a) { + c.y = a.y + } else { + if ("centerY" in a) { + c.y = a.centerY + } + } + if ("r" in a) { + c.r = a.r + } else { + if ("radius" in a) { + c.r = a.radius + } + } + return c + }, + applyEnd: function(b, a) { + if (!a) { + return b + } + var c = { + x: a.x, + y: a.y, + r: a.r + }; + if ("x" in b) { + c.x = b.x + } else { + if ("centerX" in b) { + c.x = b.centerX + } + } + if ("y" in b) { + c.y = b.y + } else { + if ("centerY" in b) { + c.y = b.centerY + } + } + if ("r" in b) { + c.r = b.r + } else { + if ("radius" in b) { + c.r = b.radius + } + } + return c + }, + generateGradient: function(n, m) { + var a = this.getStart(), + b = this.getEnd(), + k = m.width * 0.5, + d = m.height * 0.5, + j = m.x + k, + f = m.y + d, + g = n.createRadialGradient(j + a.x * k, f + a.y * d, a.r * Math.max(k, d), j + b.x * k, f + b.y * d, b.r * Math.max(k, d)), + l = this.getStops(), + e = l.length, + c; + for (c = 0; c < e; c++) { + g.addColorStop(l[c].offset, l[c].color) + } + return g + } +}); +Ext.define("Ext.draw.Surface", { + extend: "Ext.draw.SurfaceBase", + xtype: "surface", + requires: ["Ext.draw.sprite.*", "Ext.draw.gradient.*", "Ext.draw.sprite.AttributeDefinition", "Ext.draw.Matrix", "Ext.draw.Draw"], + uses: ["Ext.draw.engine.Canvas"], + devicePixelRatio: window.devicePixelRatio || window.screen.deviceXDPI / window.screen.logicalXDPI, + deprecated: { + "5.1.0": { + statics: { + methods: { + stableSort: function(a) { + return Ext.Array.sort(a, function(d, c) { + return d.attr.zIndex - c.attr.zIndex + }) + } + } + } + } + }, + config: { + cls: Ext.baseCSSPrefix + "surface", + rect: null, + background: null, + items: [], + dirty: false, + flipRtlText: false + }, + isSurface: true, + isPendingRenderFrame: false, + dirtyPredecessorCount: 0, + constructor: function(a) { + var b = this; + b.predecessors = []; + b.successors = []; + b.map = {}; + b.callParent([a]); + b.matrix = new Ext.draw.Matrix(); + b.inverseMatrix = b.matrix.inverse() + }, + roundPixel: function(a) { + return Math.round(this.devicePixelRatio * a) / this.devicePixelRatio + }, + waitFor: function(a) { + var b = this, + c = b.predecessors; + if (!Ext.Array.contains(c, a)) { + c.push(a); + a.successors.push(b); + if (a.getDirty()) { + b.dirtyPredecessorCount++ + } + } + }, + updateDirty: function(d) { + var c = this.successors, + e = c.length, + b = 0, + a; + for (; b < e; b++) { + a = c[b]; + if (d) { + a.dirtyPredecessorCount++; + a.setDirty(true) + } else { + a.dirtyPredecessorCount--; + if (a.dirtyPredecessorCount === 0 && a.isPendingRenderFrame) { + a.renderFrame() + } + } + } + }, + applyBackground: function(a, b) { + this.setDirty(true); + if (Ext.isString(a)) { + a = { + fillStyle: a + } + } + return Ext.factory(a, Ext.draw.sprite.Rect, b) + }, + applyRect: function(a, b) { + if (b && a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]) { + return + } + if (Ext.isArray(a)) { + return [a[0], a[1], a[2], a[3]] + } else { + if (Ext.isObject(a)) { + return [a.x || a.left, a.y || a.top, a.width || (a.right - a.left), a.height || (a.bottom - a.top)] + } + } + }, + updateRect: function(i) { + var h = this, + c = i[0], + f = i[1], + g = c + i[2], + a = f + i[3], + e = h.getBackground(), + d = h.element; + d.setLocalXY(Math.floor(c), Math.floor(f)); + d.setSize(Math.ceil(g - Math.floor(c)), Math.ceil(a - Math.floor(f))); + if (e) { + e.setAttributes({ + x: 0, + y: 0, + width: Math.ceil(g - Math.floor(c)), + height: Math.ceil(a - Math.floor(f)) + }) + } + h.setDirty(true) + }, + resetTransform: function() { + this.matrix.set(1, 0, 0, 1, 0, 0); + this.inverseMatrix.set(1, 0, 0, 1, 0, 0); + this.setDirty(true) + }, + get: function(a) { + return this.map[a] || this.getItems()[a] + }, + add: function() { + var g = this, + e = Array.prototype.slice.call(arguments), + j = Ext.isArray(e[0]), + a = g.map, + c = [], + f, k, h, b, d; + f = Ext.Array.clean(j ? e[0] : e); + if (!f.length) { + return c + } + for (b = 0, d = f.length; b < d; b++) { + k = f[b]; + h = null; + if (k.isSprite && !a[k.getId()]) { + h = k + } else { + if (!a[k.id]) { + h = this.createItem(k) + } + } + if (h) { + a[h.getId()] = h; + c.push(h); + h.setParent(g); + h.setSurface(g); + g.onAdd(h) + } + } + f = g.getItems(); + if (f) { + f.push.apply(f, c) + } + g.dirtyZIndex = true; + g.setDirty(true); + if (!j && c.length === 1) { + return c[0] + } else { + return c + } + }, + onAdd: Ext.emptyFn, + remove: function(a, c) { + var b = this, + e, d; + if (a) { + if (a.charAt) { + a = b.map[a] + } + if (!a || !a.isSprite) { + return null + } + if (a.isDestroyed || a.isDestroying) { + return a + } + e = a.getId(); + d = b.map[e]; + delete b.map[e]; + if (c) { + a.destroy() + } + if (!d) { + return a + } + a.setParent(null); + a.setSurface(null); + Ext.Array.remove(b.getItems(), a); + b.dirtyZIndex = true; + b.setDirty(true) + } + return a || null + }, + removeAll: function(d) { + var a = this.getItems(), + b = a.length - 1, + c; + if (d) { + for (; b >= 0; b--) { + a[b].destroy() + } + } else { + for (; b >= 0; b--) { + c = a[b]; + c.setParent(null); + c.setSurface(null) + } + } + a.length = 0; + this.map = {}; + this.dirtyZIndex = true + }, + applyItems: function(a) { + if (this.getItems()) { + this.removeAll(true) + } + return Ext.Array.from(this.add(a)) + }, + createItem: function(a) { + return Ext.create(a.xclass || "sprite." + a.type, a) + }, + getBBox: function(f, b) { + var f = Ext.Array.from(f), + c = Infinity, + h = -Infinity, + g = Infinity, + a = -Infinity, + j, k, d, e; + for (d = 0, e = f.length; d < e; d++) { + j = f[d]; + k = j.getBBox(b); + if (c > k.x) { + c = k.x + } + if (h < k.x + k.width) { + h = k.x + k.width + } + if (g > k.y) { + g = k.y + } + if (a < k.y + k.height) { + a = k.y + k.height + } + } + return { + x: c, + y: g, + width: h - c, + height: a - g + } + }, + emptyRect: [0, 0, 0, 0], + getEventXY: function(d) { + var g = this, + f = g.getInherited().rtl, + c = d.getXY(), + a = g.getOwnerBody(), + i = a.getXY(), + h = g.getRect() || g.emptyRect, + j = [], + b; + if (f) { + b = a.getWidth(); + j[0] = i[0] - c[0] - h[0] + b + } else { + j[0] = c[0] - i[0] - h[0] + } + j[1] = c[1] - i[1] - h[1]; + return j + }, + clear: Ext.emptyFn, + orderByZIndex: function() { + var d = this, + a = d.getItems(), + e = false, + b, c; + if (d.getDirty()) { + for (b = 0, c = a.length; b < c; b++) { + if (a[b].attr.dirtyZIndex) { + e = true; + break + } + } + if (e) { + Ext.Array.sort(a, function(g, f) { + return g.attr.zIndex - f.attr.zIndex + }); + this.setDirty(true) + } + for (b = 0, c = a.length; b < c; b++) { + a[b].attr.dirtyZIndex = false + } + } + }, + repaint: function() { + var a = this; + a.repaint = Ext.emptyFn; + Ext.defer(function() { + delete a.repaint; + a.element.repaint() + }, 1) + }, + renderFrame: function() { + var g = this; + if (!g.element) { + return + } + if (g.dirtyPredecessorCount > 0) { + g.isPendingRenderFrame = true; + return + } + var f = g.getRect(), + c = g.getBackground(), + a = g.getItems(), + e, b, d; + if (!f) { + return + } + g.orderByZIndex(); + if (g.getDirty()) { + g.clear(); + g.clearTransform(); + if (c) { + g.renderSprite(c) + } + for (b = 0, d = a.length; b < d; b++) { + e = a[b]; + if (g.renderSprite(e) === false) { + return + } + e.attr.textPositionCount = g.textPosition + } + g.setDirty(false) + } + }, + renderSprite: Ext.emptyFn, + clearTransform: Ext.emptyFn, + destroy: function() { + var a = this; + a.removeAll(true); + a.predecessors = null; + a.successors = null; + a.callParent() + } +}); +Ext.define("Ext.draw.overrides.Surface", { + override: "Ext.draw.Surface", + hitTest: function(b, c) { + var f = this, + g = f.getItems(), + e, d, a; + c = c || Ext.draw.sprite.Sprite.defaultHitTestOptions; + for (e = g.length - 1; e >= 0; e--) { + d = g[e]; + if (d.hitTest) { + a = d.hitTest(b, c); + if (a) { + return a + } + } + } + return null + }, + hitTestEvent: function(b, a) { + var c = this.getEventXY(b); + return this.hitTest(c, a) + } +}); +Ext.define("Ext.draw.engine.SvgContext", { + requires: ["Ext.draw.Color"], + toSave: ["strokeOpacity", "strokeStyle", "fillOpacity", "fillStyle", "globalAlpha", "lineWidth", "lineCap", "lineJoin", "lineDash", "lineDashOffset", "miterLimit", "shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor", "globalCompositeOperation", "position", "fillGradient", "strokeGradient"], + strokeOpacity: 1, + strokeStyle: "none", + fillOpacity: 1, + fillStyle: "none", + lineDash: [], + lineDashOffset: 0, + globalAlpha: 1, + lineWidth: 1, + lineCap: "butt", + lineJoin: "miter", + miterLimit: 10, + shadowOffsetX: 0, + shadowOffsetY: 0, + shadowBlur: 0, + shadowColor: "none", + globalCompositeOperation: "src", + urlStringRe: /^url\(#([\w\-]+)\)$/, + constructor: function(a) { + this.surface = a; + this.state = []; + this.matrix = new Ext.draw.Matrix(); + this.path = null; + this.clear() + }, + clear: function() { + this.group = this.surface.mainGroup; + this.position = 0; + this.path = null + }, + getElement: function(a) { + return this.surface.getSvgElement(this.group, a, this.position++) + }, + removeElement: function(d) { + var d = Ext.fly(d), + h, g, b, f, a, e, c; + if (!d) { + return + } + if (d.dom.tagName === "g") { + a = d.dom.gradients; + for (c in a) { + a[c].destroy() + } + } else { + h = d.getAttribute("fill"); + g = d.getAttribute("stroke"); + b = h && h.match(this.urlStringRe); + f = g && g.match(this.urlStringRe); + if (b && b[1]) { + e = Ext.fly(b[1]); + if (e) { + e.destroy() + } + } + if (f && f[1]) { + e = Ext.fly(f[1]); + if (e) { + e.destroy() + } + } + } + d.destroy() + }, + save: function() { + var c = this.toSave, + e = {}, + d = this.getElement("g"), + b, a; + for (a = 0; a < c.length; a++) { + b = c[a]; + if (b in this) { + e[b] = this[b] + } + } + this.position = 0; + e.matrix = this.matrix.clone(); + this.state.push(e); + this.group = d; + return d + }, + restore: function() { + var d = this.toSave, + e = this.state.pop(), + c = this.group.dom.childNodes, + b, a; + while (c.length > this.position) { + this.removeElement(c[c.length - 1]) + } + for (a = 0; a < d.length; a++) { + b = d[a]; + if (b in e) { + this[b] = e[b] + } else { + delete this[b] + } + } + this.setTransform.apply(this, e.matrix.elements); + this.group = this.group.getParent() + }, + transform: function(f, b, e, g, d, c) { + if (this.path) { + var a = Ext.draw.Matrix.fly([f, b, e, g, d, c]).inverse(); + this.path.transform(a) + } + this.matrix.append(f, b, e, g, d, c) + }, + setTransform: function(e, a, d, f, c, b) { + if (this.path) { + this.path.transform(this.matrix) + } + this.matrix.reset(); + this.transform(e, a, d, f, c, b) + }, + scale: function(a, b) { + this.transform(a, 0, 0, b, 0, 0) + }, + rotate: function(d) { + var c = Math.cos(d), + a = Math.sin(d), + b = -Math.sin(d), + e = Math.cos(d); + this.transform(c, a, b, e, 0, 0) + }, + translate: function(a, b) { + this.transform(1, 0, 0, 1, a, b) + }, + setGradientBBox: function(a) { + this.bbox = a + }, + beginPath: function() { + this.path = new Ext.draw.Path() + }, + moveTo: function(a, b) { + if (!this.path) { + this.beginPath() + } + this.path.moveTo(a, b); + this.path.element = null + }, + lineTo: function(a, b) { + if (!this.path) { + this.beginPath() + } + this.path.lineTo(a, b); + this.path.element = null + }, + rect: function(b, d, c, a) { + this.moveTo(b, d); + this.lineTo(b + c, d); + this.lineTo(b + c, d + a); + this.lineTo(b, d + a); + this.closePath() + }, + strokeRect: function(b, d, c, a) { + this.beginPath(); + this.rect(b, d, c, a); + this.stroke() + }, + fillRect: function(b, d, c, a) { + this.beginPath(); + this.rect(b, d, c, a); + this.fill() + }, + closePath: function() { + if (!this.path) { + this.beginPath() + } + this.path.closePath(); + this.path.element = null + }, + arcSvg: function(d, a, f, g, c, b, e) { + if (!this.path) { + this.beginPath() + } + this.path.arcSvg(d, a, f, g, c, b, e); + this.path.element = null + }, + arc: function(b, f, a, d, c, e) { + if (!this.path) { + this.beginPath() + } + this.path.arc(b, f, a, d, c, e); + this.path.element = null + }, + ellipse: function(a, h, g, f, d, c, b, e) { + if (!this.path) { + this.beginPath() + } + this.path.ellipse(a, h, g, f, d, c, b, e); + this.path.element = null + }, + arcTo: function(b, e, a, d, g, f, c) { + if (!this.path) { + this.beginPath() + } + this.path.arcTo(b, e, a, d, g, f, c); + this.path.element = null + }, + bezierCurveTo: function(d, f, b, e, a, c) { + if (!this.path) { + this.beginPath() + } + this.path.bezierCurveTo(d, f, b, e, a, c); + this.path.element = null + }, + strokeText: function(d, a, e) { + d = String(d); + if (this.strokeStyle) { + var b = this.getElement("text"), + c = this.surface.getSvgElement(b, "tspan", 0); + this.surface.setElementAttributes(b, { + x: a, + y: e, + transform: this.matrix.toSvg(), + stroke: this.strokeStyle, + fill: "none", + opacity: this.globalAlpha, + "stroke-opacity": this.strokeOpacity, + style: "font: " + this.font, + "stroke-dasharray": this.lineDash.join(","), + "stroke-dashoffset": this.lineDashOffset + }); + if (this.lineDash.length) { + this.surface.setElementAttributes(b, { + "stroke-dasharray": this.lineDash.join(","), + "stroke-dashoffset": this.lineDashOffset + }) + } + if (c.dom.firstChild) { + c.dom.removeChild(c.dom.firstChild) + } + this.surface.setElementAttributes(c, { + "alignment-baseline": "alphabetic" + }); + c.dom.appendChild(document.createTextNode(Ext.String.htmlDecode(d))) + } + }, + fillText: function(d, a, e) { + d = String(d); + if (this.fillStyle) { + var b = this.getElement("text"), + c = this.surface.getSvgElement(b, "tspan", 0); + this.surface.setElementAttributes(b, { + x: a, + y: e, + transform: this.matrix.toSvg(), + fill: this.fillStyle, + opacity: this.globalAlpha, + "fill-opacity": this.fillOpacity, + style: "font: " + this.font + }); + if (c.dom.firstChild) { + c.dom.removeChild(c.dom.firstChild) + } + this.surface.setElementAttributes(c, { + "alignment-baseline": "alphabetic" + }); + c.dom.appendChild(document.createTextNode(Ext.String.htmlDecode(d))) + } + }, + drawImage: function(c, k, i, l, e, p, n, a, g) { + var f = this, + d = f.getElement("image"), + j = k, + h = i, + b = typeof l === "undefined" ? c.width : l, + m = typeof e === "undefined" ? c.height : e, + o = null; + if (typeof g !== "undefined") { + o = k + " " + i + " " + l + " " + e; + j = p; + h = n; + b = a; + m = g + } + d.dom.setAttributeNS("http://www.w3.org/1999/xlink", "href", c.src); + f.surface.setElementAttributes(d, { + viewBox: o, + x: j, + y: h, + width: b, + height: m, + opacity: f.globalAlpha, + transform: f.matrix.toSvg() + }) + }, + fill: function() { + if (!this.path) { + return + } + if (this.fillStyle) { + var c, a = this.fillGradient, + d = this.bbox, + b = this.path.element; + if (!b) { + c = this.path.toString(); + b = this.path.element = this.getElement("path"); + this.surface.setElementAttributes(b, { + d: c, + transform: this.matrix.toSvg() + }) + } + this.surface.setElementAttributes(b, { + fill: a && d ? a.generateGradient(this, d) : this.fillStyle, + "fill-opacity": this.fillOpacity * this.globalAlpha + }) + } + }, + stroke: function() { + if (!this.path) { + return + } + if (this.strokeStyle) { + var c, b = this.strokeGradient, + d = this.bbox, + a = this.path.element; + if (!a || !this.path.svgString) { + c = this.path.toString(); + if (!c) { + return + } + a = this.path.element = this.getElement("path"); + this.surface.setElementAttributes(a, { + fill: "none", + d: c, + transform: this.matrix.toSvg() + }) + } + this.surface.setElementAttributes(a, { + stroke: b && d ? b.generateGradient(this, d) : this.strokeStyle, + "stroke-linecap": this.lineCap, + "stroke-linejoin": this.lineJoin, + "stroke-width": this.lineWidth, + "stroke-opacity": this.strokeOpacity * this.globalAlpha, + "stroke-dasharray": this.lineDash.join(","), + "stroke-dashoffset": this.lineDashOffset + }); + if (this.lineDash.length) { + this.surface.setElementAttributes(a, { + "stroke-dasharray": this.lineDash.join(","), + "stroke-dashoffset": this.lineDashOffset + }) + } + } + }, + fillStroke: function(a, e) { + var b = this, + d = b.fillStyle, + g = b.strokeStyle, + c = b.fillOpacity, + f = b.strokeOpacity; + if (e === undefined) { + e = a.transformFillStroke + } + if (!e) { + a.inverseMatrix.toContext(b) + } + if (d && c !== 0) { + b.fill() + } + if (g && f !== 0) { + b.stroke() + } + }, + appendPath: function(a) { + this.path = a.clone() + }, + setLineDash: function(a) { + this.lineDash = a + }, + getLineDash: function() { + return this.lineDash + }, + createLinearGradient: function(d, g, b, e) { + var f = this, + c = f.surface.getNextDef("linearGradient"), + a = f.group.dom.gradients || (f.group.dom.gradients = {}), + h; + f.surface.setElementAttributes(c, { + x1: d, + y1: g, + x2: b, + y2: e, + gradientUnits: "userSpaceOnUse" + }); + h = new Ext.draw.engine.SvgContext.Gradient(f, f.surface, c); + a[c.dom.id] = h; + return h + }, + createRadialGradient: function(b, j, d, a, i, c) { + var g = this, + e = g.surface.getNextDef("radialGradient"), + f = g.group.dom.gradients || (g.group.dom.gradients = {}), + h; + g.surface.setElementAttributes(e, { + fx: b, + fy: j, + cx: a, + cy: i, + r: c, + gradientUnits: "userSpaceOnUse" + }); + h = new Ext.draw.engine.SvgContext.Gradient(g, g.surface, e, d / c); + f[e.dom.id] = h; + return h + } +}); +Ext.define("Ext.draw.engine.SvgContext.Gradient", { + statics: { + map: {} + }, + constructor: function(c, a, d, b) { + var f = this.statics().map, + e; + e = f[d.dom.id]; + if (e) { + e.element = null + } + f[d.dom.id] = this; + this.ctx = c; + this.surface = a; + this.element = d; + this.position = 0; + this.compression = b || 0 + }, + addColorStop: function(d, b) { + var c = this.surface.getSvgElement(this.element, "stop", this.position++), + a = this.compression; + this.surface.setElementAttributes(c, { + offset: (((1 - a) * d + a) * 100).toFixed(2) + "%", + "stop-color": b, + "stop-opacity": Ext.draw.Color.fly(b).a.toFixed(15) + }) + }, + toString: function() { + var a = this.element.dom.childNodes; + while (a.length > this.position) { + Ext.fly(a[a.length - 1]).destroy() + } + return "url(#" + this.element.getId() + ")" + }, + destroy: function() { + var b = this.statics().map, + a = this.element; + if (a && a.dom) { + delete b[a.dom.id]; + a.destroy() + } + this.callParent() + } +}); +Ext.define("Ext.draw.engine.Svg", { + extend: "Ext.draw.Surface", + requires: ["Ext.draw.engine.SvgContext"], + statics: { + BBoxTextCache: {} + }, + config: { + highPrecision: false + }, + getElementConfig: function() { + return { + reference: "element", + style: { + position: "absolute" + }, + children: [{ + reference: "innerElement", + style: { + width: "100%", + height: "100%", + position: "relative" + }, + children: [{ + tag: "svg", + reference: "svgElement", + namespace: "http://www.w3.org/2000/svg", + width: "100%", + height: "100%", + version: 1.1 + }] + }] + } + }, + constructor: function(a) { + var b = this; + b.callParent([a]); + b.mainGroup = b.createSvgNode("g"); + b.defElement = b.createSvgNode("defs"); + b.svgElement.appendChild(b.mainGroup); + b.svgElement.appendChild(b.defElement); + b.ctx = new Ext.draw.engine.SvgContext(b) + }, + createSvgNode: function(a) { + var b = document.createElementNS("http://www.w3.org/2000/svg", a); + return Ext.get(b) + }, + getSvgElement: function(d, b, a) { + var c; + if (d.dom.childNodes.length > a) { + c = d.dom.childNodes[a]; + if (c.tagName === b) { + return Ext.get(c) + } else { + Ext.destroy(c) + } + } + c = Ext.get(this.createSvgNode(b)); + if (a === 0) { + d.insertFirst(c) + } else { + c.insertAfter(Ext.fly(d.dom.childNodes[a - 1])) + } + c.cache = {}; + return c + }, + setElementAttributes: function(d, b) { + var f = d.dom, + a = d.cache, + c, e; + for (c in b) { + e = b[c]; + if (a[c] !== e) { + a[c] = e; + f.setAttribute(c, e) + } + } + }, + getNextDef: function(a) { + return this.getSvgElement(this.defElement, a, this.defPosition++) + }, + clearTransform: function() { + var a = this; + a.mainGroup.set({ + transform: a.matrix.toSvg() + }) + }, + clear: function() { + this.ctx.clear(); + this.defPosition = 0 + }, + renderSprite: function(b) { + var d = this, + c = d.getRect(), + a = d.ctx; + if (b.attr.hidden || b.attr.globalAlpha === 0) { + a.save(); + a.restore(); + return + } + b.element = a.save(); + b.preRender(this); + b.useAttributes(a, c); + if (false === b.render(this, a, [0, 0, c[2], c[3]])) { + return false + } + b.setDirty(false); + a.restore() + }, + flatten: function(e, b) { + var c = '', + f = Ext.getClassName(this), + a, g, d; + c += ''; + for (d = 0; d < b.length; d++) { + a = b[d]; + if (Ext.getClassName(a) !== f) { + continue + } + g = a.getRect(); + c += ''; + c += this.serializeNode(a.svgElement.dom); + c += "" + } + c += ""; + return { + data: "data:image/svg+xml;utf8," + encodeURIComponent(c), + type: "svg" + } + }, + serializeNode: function(d) { + var b = "", + c, f, a, e; + if (d.nodeType === document.TEXT_NODE) { + return d.nodeValue + } + b += "<" + d.nodeName; + if (d.attributes.length) { + for (c = 0, f = d.attributes.length; c < f; c++) { + a = d.attributes[c]; + b += " " + a.name + '="' + a.value + '"' + } + } + b += ">"; + if (d.childNodes && d.childNodes.length) { + for (c = 0, f = d.childNodes.length; c < f; c++) { + e = d.childNodes[c]; + b += this.serializeNode(e) + } + } + b += ""; + return b + }, + destroy: function() { + var a = this; + a.ctx.destroy(); + a.mainGroup.destroy(); + delete a.mainGroup; + delete a.ctx; + a.callParent() + }, + remove: function(a, b) { + if (a && a.element) { + if (this.ctx) { + this.ctx.removeElement(a.element) + } else { + a.element.destroy() + } + a.element = null + } + this.callParent(arguments) + } +}); +Ext.draw || (Ext.draw = {}); +Ext.draw.engine || (Ext.draw.engine = {}); +Ext.draw.engine.excanvas = true; +if (!document.createElement("canvas").getContext) { + (function() { + var ab = Math; + var n = ab.round; + var l = ab.sin; + var A = ab.cos; + var H = ab.abs; + var N = ab.sqrt; + var d = 10; + var f = d / 2; + var z = +navigator.userAgent.match(/MSIE ([\d.]+)?/)[1]; + + function y() { + return this.context_ || (this.context_ = new D(this)) + } + var t = Array.prototype.slice; + + function g(j, m, p) { + var i = t.call(arguments, 2); + return function() { + return j.apply(m, i.concat(t.call(arguments))) + } + } + + function af(i) { + return String(i).replace(/&/g, "&").replace(/"/g, """) + } + + function Y(m, j, i) { + Ext.onReady(function() { + if (!m.namespaces[j]) { + m.namespaces.add(j, i, "#default#VML") + } + }) + } + + function R(j) { + Y(j, "g_vml_", "urn:schemas-microsoft-com:vml"); + Y(j, "g_o_", "urn:schemas-microsoft-com:office:office"); + if (!j.styleSheets.ex_canvas_) { + var i = j.createStyleSheet(); + i.owningElement.id = "ex_canvas_"; + i.cssText = "canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}" + } + } + R(document); + var e = { + init: function(i) { + var j = i || document; + j.createElement("canvas"); + j.attachEvent("onreadystatechange", g(this.init_, this, j)) + }, + init_: function(p) { + var m = p.getElementsByTagName("canvas"); + for (var j = 0; j < m.length; j++) { + this.initElement(m[j]) + } + }, + initElement: function(j) { + if (!j.getContext) { + j.getContext = y; + R(j.ownerDocument); + j.innerHTML = ""; + j.attachEvent("onpropertychange", x); + j.attachEvent("onresize", W); + var i = j.attributes; + if (i.width && i.width.specified) { + j.style.width = i.width.nodeValue + "px" + } else { + j.width = j.clientWidth + } + if (i.height && i.height.specified) { + j.style.height = i.height.nodeValue + "px" + } else { + j.height = j.clientHeight + } + } + return j + } + }; + + function x(j) { + var i = j.srcElement; + switch (j.propertyName) { + case "width": + i.getContext().clearRect(); + i.style.width = i.attributes.width.nodeValue + "px"; + i.firstChild.style.width = i.clientWidth + "px"; + break; + case "height": + i.getContext().clearRect(); + i.style.height = i.attributes.height.nodeValue + "px"; + i.firstChild.style.height = i.clientHeight + "px"; + break + } + } + + function W(j) { + var i = j.srcElement; + if (i.firstChild) { + i.firstChild.style.width = i.clientWidth + "px"; + i.firstChild.style.height = i.clientHeight + "px" + } + } + e.init(); + var k = []; + for (var ae = 0; ae < 16; ae++) { + for (var ad = 0; ad < 16; ad++) { + k[ae * 16 + ad] = ae.toString(16) + ad.toString(16) + } + } + + function B() { + return [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ] + } + + function J(p, m) { + var j = B(); + for (var i = 0; i < 3; i++) { + for (var ah = 0; ah < 3; ah++) { + var Z = 0; + for (var ag = 0; ag < 3; ag++) { + Z += p[i][ag] * m[ag][ah] + } + j[i][ah] = Z + } + } + return j + } + + function v(j, i) { + i.fillStyle = j.fillStyle; + i.lineCap = j.lineCap; + i.lineJoin = j.lineJoin; + i.lineDash = j.lineDash; + i.lineWidth = j.lineWidth; + i.miterLimit = j.miterLimit; + i.shadowBlur = j.shadowBlur; + i.shadowColor = j.shadowColor; + i.shadowOffsetX = j.shadowOffsetX; + i.shadowOffsetY = j.shadowOffsetY; + i.strokeStyle = j.strokeStyle; + i.globalAlpha = j.globalAlpha; + i.font = j.font; + i.textAlign = j.textAlign; + i.textBaseline = j.textBaseline; + i.arcScaleX_ = j.arcScaleX_; + i.arcScaleY_ = j.arcScaleY_; + i.lineScale_ = j.lineScale_ + } + var b = { + aliceblue: "#F0F8FF", + antiquewhite: "#FAEBD7", + aquamarine: "#7FFFD4", + azure: "#F0FFFF", + beige: "#F5F5DC", + bisque: "#FFE4C4", + black: "#000000", + blanchedalmond: "#FFEBCD", + blueviolet: "#8A2BE2", + brown: "#A52A2A", + burlywood: "#DEB887", + cadetblue: "#5F9EA0", + chartreuse: "#7FFF00", + chocolate: "#D2691E", + coral: "#FF7F50", + cornflowerblue: "#6495ED", + cornsilk: "#FFF8DC", + crimson: "#DC143C", + cyan: "#00FFFF", + darkblue: "#00008B", + darkcyan: "#008B8B", + darkgoldenrod: "#B8860B", + darkgray: "#A9A9A9", + darkgreen: "#006400", + darkgrey: "#A9A9A9", + darkkhaki: "#BDB76B", + darkmagenta: "#8B008B", + darkolivegreen: "#556B2F", + darkorange: "#FF8C00", + darkorchid: "#9932CC", + darkred: "#8B0000", + darksalmon: "#E9967A", + darkseagreen: "#8FBC8F", + darkslateblue: "#483D8B", + darkslategray: "#2F4F4F", + darkslategrey: "#2F4F4F", + darkturquoise: "#00CED1", + darkviolet: "#9400D3", + deeppink: "#FF1493", + deepskyblue: "#00BFFF", + dimgray: "#696969", + dimgrey: "#696969", + dodgerblue: "#1E90FF", + firebrick: "#B22222", + floralwhite: "#FFFAF0", + forestgreen: "#228B22", + gainsboro: "#DCDCDC", + ghostwhite: "#F8F8FF", + gold: "#FFD700", + goldenrod: "#DAA520", + grey: "#808080", + greenyellow: "#ADFF2F", + honeydew: "#F0FFF0", + hotpink: "#FF69B4", + indianred: "#CD5C5C", + indigo: "#4B0082", + ivory: "#FFFFF0", + khaki: "#F0E68C", + lavender: "#E6E6FA", + lavenderblush: "#FFF0F5", + lawngreen: "#7CFC00", + lemonchiffon: "#FFFACD", + lightblue: "#ADD8E6", + lightcoral: "#F08080", + lightcyan: "#E0FFFF", + lightgoldenrodyellow: "#FAFAD2", + lightgreen: "#90EE90", + lightgrey: "#D3D3D3", + lightpink: "#FFB6C1", + lightsalmon: "#FFA07A", + lightseagreen: "#20B2AA", + lightskyblue: "#87CEFA", + lightslategray: "#778899", + lightslategrey: "#778899", + lightsteelblue: "#B0C4DE", + lightyellow: "#FFFFE0", + limegreen: "#32CD32", + linen: "#FAF0E6", + magenta: "#FF00FF", + mediumaquamarine: "#66CDAA", + mediumblue: "#0000CD", + mediumorchid: "#BA55D3", + mediumpurple: "#9370DB", + mediumseagreen: "#3CB371", + mediumslateblue: "#7B68EE", + mediumspringgreen: "#00FA9A", + mediumturquoise: "#48D1CC", + mediumvioletred: "#C71585", + midnightblue: "#191970", + mintcream: "#F5FFFA", + mistyrose: "#FFE4E1", + moccasin: "#FFE4B5", + navajowhite: "#FFDEAD", + oldlace: "#FDF5E6", + olivedrab: "#6B8E23", + orange: "#FFA500", + orangered: "#FF4500", + orchid: "#DA70D6", + palegoldenrod: "#EEE8AA", + palegreen: "#98FB98", + paleturquoise: "#AFEEEE", + palevioletred: "#DB7093", + papayawhip: "#FFEFD5", + peachpuff: "#FFDAB9", + peru: "#CD853F", + pink: "#FFC0CB", + plum: "#DDA0DD", + powderblue: "#B0E0E6", + rosybrown: "#BC8F8F", + royalblue: "#4169E1", + saddlebrown: "#8B4513", + salmon: "#FA8072", + sandybrown: "#F4A460", + seagreen: "#2E8B57", + seashell: "#FFF5EE", + sienna: "#A0522D", + skyblue: "#87CEEB", + slateblue: "#6A5ACD", + slategray: "#708090", + slategrey: "#708090", + snow: "#FFFAFA", + springgreen: "#00FF7F", + steelblue: "#4682B4", + tan: "#D2B48C", + thistle: "#D8BFD8", + tomato: "#FF6347", + turquoise: "#40E0D0", + violet: "#EE82EE", + wheat: "#F5DEB3", + whitesmoke: "#F5F5F5", + yellowgreen: "#9ACD32" + }; + + function M(j) { + var p = j.indexOf("(", 3); + var i = j.indexOf(")", p + 1); + var m = j.substring(p + 1, i).split(","); + if (m.length != 4 || j.charAt(3) != "a") { + m[3] = 1 + } + return m + } + + function c(i) { + return parseFloat(i) / 100 + } + + function r(j, m, i) { + return Math.min(i, Math.max(m, j)) + } + + function I(ag) { + var i, ai, aj, ah, ak, Z; + ah = parseFloat(ag[0]) / 360 % 360; + if (ah < 0) { + ah++ + } + ak = r(c(ag[1]), 0, 1); + Z = r(c(ag[2]), 0, 1); + if (ak == 0) { + i = ai = aj = Z + } else { + var j = Z < 0.5 ? Z * (1 + ak) : Z + ak - Z * ak; + var m = 2 * Z - j; + i = a(m, j, ah + 1 / 3); + ai = a(m, j, ah); + aj = a(m, j, ah - 1 / 3) + } + return "#" + k[Math.floor(i * 255)] + k[Math.floor(ai * 255)] + k[Math.floor(aj * 255)] + } + + function a(j, i, m) { + if (m < 0) { + m++ + } + if (m > 1) { + m-- + } + if (6 * m < 1) { + return j + (i - j) * 6 * m + } else { + if (2 * m < 1) { + return i + } else { + if (3 * m < 2) { + return j + (i - j) * (2 / 3 - m) * 6 + } else { + return j + } + } + } + } + var C = {}; + + function F(j) { + if (j in C) { + return C[j] + } + var ag, Z = 1; + j = String(j); + if (j.charAt(0) == "#") { + ag = j + } else { + if (/^rgb/.test(j)) { + var p = M(j); + var ag = "#", + ah; + for (var m = 0; m < 3; m++) { + if (p[m].indexOf("%") != -1) { + ah = Math.floor(c(p[m]) * 255) + } else { + ah = +p[m] + } + ag += k[r(ah, 0, 255)] + } + Z = +p[3] + } else { + if (/^hsl/.test(j)) { + var p = M(j); + ag = I(p); + Z = p[3] + } else { + ag = b[j] || j + } + } + } + return C[j] = { + color: ag, + alpha: Z + } + } + var o = { + style: "normal", + variant: "normal", + weight: "normal", + size: 10, + family: "sans-serif" + }; + var L = {}; + + function E(i) { + if (L[i]) { + return L[i] + } + var p = document.createElement("div"); + var m = p.style; + try { + m.font = i + } catch (j) {} + return L[i] = { + style: m.fontStyle || o.style, + variant: m.fontVariant || o.variant, + weight: m.fontWeight || o.weight, + size: m.fontSize || o.size, + family: m.fontFamily || o.family + } + } + + function u(m, j) { + var i = {}; + for (var ah in m) { + i[ah] = m[ah] + } + var ag = parseFloat(j.currentStyle.fontSize), + Z = parseFloat(m.size); + if (typeof m.size == "number") { + i.size = m.size + } else { + if (m.size.indexOf("px") != -1) { + i.size = Z + } else { + if (m.size.indexOf("em") != -1) { + i.size = ag * Z + } else { + if (m.size.indexOf("%") != -1) { + i.size = (ag / 100) * Z + } else { + if (m.size.indexOf("pt") != -1) { + i.size = Z / 0.75 + } else { + i.size = ag + } + } + } + } + } + i.size *= 0.981; + return i + } + + function ac(i) { + return i.style + " " + i.variant + " " + i.weight + " " + i.size + "px " + i.family + } + var s = { + butt: "flat", + round: "round" + }; + + function S(i) { + return s[i] || "square" + } + + function D(i) { + this.m_ = B(); + this.mStack_ = []; + this.aStack_ = []; + this.currentPath_ = []; + this.strokeStyle = "#000"; + this.fillStyle = "#000"; + this.lineWidth = 1; + this.lineJoin = "miter"; + this.lineDash = []; + this.lineCap = "butt"; + this.miterLimit = d * 1; + this.globalAlpha = 1; + this.font = "10px sans-serif"; + this.textAlign = "left"; + this.textBaseline = "alphabetic"; + this.canvas = i; + var m = "width:" + i.clientWidth + "px;height:" + i.clientHeight + "px;overflow:hidden;position:absolute"; + var j = i.ownerDocument.createElement("div"); + j.style.cssText = m; + i.appendChild(j); + var p = j.cloneNode(false); + p.style.backgroundColor = "red"; + p.style.filter = "alpha(opacity=0)"; + i.appendChild(p); + this.element_ = j; + this.arcScaleX_ = 1; + this.arcScaleY_ = 1; + this.lineScale_ = 1 + } + var q = D.prototype; + q.clearRect = function() { + if (this.textMeasureEl_) { + this.textMeasureEl_.removeNode(true); + this.textMeasureEl_ = null + } + this.element_.innerHTML = "" + }; + q.beginPath = function() { + this.currentPath_ = [] + }; + q.moveTo = function(j, i) { + var m = V(this, j, i); + this.currentPath_.push({ + type: "moveTo", + x: m.x, + y: m.y + }); + this.currentX_ = m.x; + this.currentY_ = m.y + }; + q.lineTo = function(j, i) { + var m = V(this, j, i); + this.currentPath_.push({ + type: "lineTo", + x: m.x, + y: m.y + }); + this.currentX_ = m.x; + this.currentY_ = m.y + }; + q.bezierCurveTo = function(m, j, ak, aj, ai, ag) { + var i = V(this, ai, ag); + var ah = V(this, m, j); + var Z = V(this, ak, aj); + K(this, ah, Z, i) + }; + + function K(i, Z, m, j) { + i.currentPath_.push({ + type: "bezierCurveTo", + cp1x: Z.x, + cp1y: Z.y, + cp2x: m.x, + cp2y: m.y, + x: j.x, + y: j.y + }); + i.currentX_ = j.x; + i.currentY_ = j.y + } + q.quadraticCurveTo = function(ai, m, j, i) { + var ah = V(this, ai, m); + var ag = V(this, j, i); + var aj = { + x: this.currentX_ + 2 / 3 * (ah.x - this.currentX_), + y: this.currentY_ + 2 / 3 * (ah.y - this.currentY_) + }; + var Z = { + x: aj.x + (ag.x - this.currentX_) / 3, + y: aj.y + (ag.y - this.currentY_) / 3 + }; + K(this, aj, Z, ag) + }; + q.arc = function(al, aj, ak, ag, j, m) { + ak *= d; + var ap = m ? "at" : "wa"; + var am = al + A(ag) * ak - f; + var ao = aj + l(ag) * ak - f; + var i = al + A(j) * ak - f; + var an = aj + l(j) * ak - f; + if (am == i && !m) { + am += 0.125 + } + var Z = V(this, al, aj); + var ai = V(this, am, ao); + var ah = V(this, i, an); + this.currentPath_.push({ + type: ap, + x: Z.x, + y: Z.y, + radius: ak, + xStart: ai.x, + yStart: ai.y, + xEnd: ah.x, + yEnd: ah.y + }) + }; + q.rect = function(m, j, i, p) { + this.moveTo(m, j); + this.lineTo(m + i, j); + this.lineTo(m + i, j + p); + this.lineTo(m, j + p); + this.closePath() + }; + q.strokeRect = function(m, j, i, p) { + var Z = this.currentPath_; + this.beginPath(); + this.moveTo(m, j); + this.lineTo(m + i, j); + this.lineTo(m + i, j + p); + this.lineTo(m, j + p); + this.closePath(); + this.stroke(); + this.currentPath_ = Z + }; + q.fillRect = function(m, j, i, p) { + var Z = this.currentPath_; + this.beginPath(); + this.moveTo(m, j); + this.lineTo(m + i, j); + this.lineTo(m + i, j + p); + this.lineTo(m, j + p); + this.closePath(); + this.fill(); + this.currentPath_ = Z + }; + q.createLinearGradient = function(j, p, i, m) { + var Z = new U("gradient"); + Z.x0_ = j; + Z.y0_ = p; + Z.x1_ = i; + Z.y1_ = m; + return Z + }; + q.createRadialGradient = function(p, ag, m, j, Z, i) { + var ah = new U("gradientradial"); + ah.x0_ = p; + ah.y0_ = ag; + ah.r0_ = m; + ah.x1_ = j; + ah.y1_ = Z; + ah.r1_ = i; + return ah + }; + q.drawImage = function(an, j) { + var ah, Z, aj, ar, al, ak, ao, av; + var ai = an.runtimeStyle.width; + var am = an.runtimeStyle.height; + an.runtimeStyle.width = "auto"; + an.runtimeStyle.height = "auto"; + var ag = an.width; + var aq = an.height; + an.runtimeStyle.width = ai; + an.runtimeStyle.height = am; + if (arguments.length == 3) { + ah = arguments[1]; + Z = arguments[2]; + al = ak = 0; + ao = aj = ag; + av = ar = aq + } else { + if (arguments.length == 5) { + ah = arguments[1]; + Z = arguments[2]; + aj = arguments[3]; + ar = arguments[4]; + al = ak = 0; + ao = ag; + av = aq + } else { + if (arguments.length == 9) { + al = arguments[1]; + ak = arguments[2]; + ao = arguments[3]; + av = arguments[4]; + ah = arguments[5]; + Z = arguments[6]; + aj = arguments[7]; + ar = arguments[8] + } else { + throw Error("Invalid number of arguments") + } + } + } + var au = V(this, ah, Z); + var at = []; + var i = 10; + var p = 10; + var ap = this.m_; + at.push(" ', '", ""); + this.element_.insertAdjacentHTML("BeforeEnd", at.join("")) + }; + q.setLineDash = function(i) { + if (i.length === 1) { + i = i.slice(); + i[1] = i[0] + } + this.lineDash = i + }; + q.getLineDash = function() { + return this.lineDash + }; + q.stroke = function(ak) { + var ai = []; + var m = 10; + var al = 10; + ai.push(" aj.x) { + aj.x = j.x + } + if (Z.y == null || j.y < Z.y) { + Z.y = j.y + } + if (aj.y == null || j.y > aj.y) { + aj.y = j.y + } + } + } + ai.push(' ">'); + if (!ak) { + w(this, ai) + } else { + G(this, ai, Z, aj) + } + ai.push(""); + this.element_.insertAdjacentHTML("beforeEnd", ai.join("")) + }; + + function w(m, ag) { + var j = F(m.strokeStyle); + var p = j.color; + var Z = j.alpha * m.globalAlpha; + var i = m.lineScale_ * m.lineWidth; + if (i < 1) { + Z *= i + } + ag.push("') + } + + function G(aq, ai, aK, ar) { + var aj = aq.fillStyle; + var aB = aq.arcScaleX_; + var aA = aq.arcScaleY_; + var j = ar.x - aK.x; + var p = ar.y - aK.y; + if (aj instanceof U) { + var an = 0; + var aF = { + x: 0, + y: 0 + }; + var ax = 0; + var am = 1; + if (aj.type_ == "gradient") { + var al = aj.x0_ / aB; + var m = aj.y0_ / aA; + var ak = aj.x1_ / aB; + var aM = aj.y1_ / aA; + var aJ = V(aq, al, m); + var aI = V(aq, ak, aM); + var ag = aI.x - aJ.x; + var Z = aI.y - aJ.y; + an = Math.atan2(ag, Z) * 180 / Math.PI; + if (an < 0) { + an += 360 + } + if (an < 0.000001) { + an = 0 + } + } else { + var aJ = V(aq, aj.x0_, aj.y0_); + aF = { + x: (aJ.x - aK.x) / j, + y: (aJ.y - aK.y) / p + }; + j /= aB * d; + p /= aA * d; + var aD = ab.max(j, p); + ax = 2 * aj.r0_ / aD; + am = 2 * aj.r1_ / aD - ax + } + var av = aj.colors_; + av.sort(function(aN, i) { + return aN.offset - i.offset + }); + var ap = av.length; + var au = av[0].color; + var at = av[ap - 1].color; + var az = av[0].alpha * aq.globalAlpha; + var ay = av[ap - 1].alpha * aq.globalAlpha; + var aE = []; + for (var aH = 0; aH < ap; aH++) { + var ao = av[aH]; + aE.push(ao.offset * am + ax + " " + ao.color) + } + ai.push('') + } else { + if (aj instanceof T) { + if (j && p) { + var ah = -aK.x; + var aC = -aK.y; + ai.push("') + } + } else { + var aL = F(aq.fillStyle); + var aw = aL.color; + var aG = aL.alpha * aq.globalAlpha; + ai.push('') + } + } + } + q.fill = function() { + this.$stroke(true) + }; + q.closePath = function() { + this.currentPath_.push({ + type: "close" + }) + }; + + function V(j, Z, p) { + var i = j.m_; + return { + x: d * (Z * i[0][0] + p * i[1][0] + i[2][0]) - f, + y: d * (Z * i[0][1] + p * i[1][1] + i[2][1]) - f + } + } + q.save = function() { + var i = {}; + v(this, i); + this.aStack_.push(i); + this.mStack_.push(this.m_); + this.m_ = J(B(), this.m_) + }; + q.restore = function() { + if (this.aStack_.length) { + v(this.aStack_.pop(), this); + this.m_ = this.mStack_.pop() + } + }; + + function h(i) { + return isFinite(i[0][0]) && isFinite(i[0][1]) && isFinite(i[1][0]) && isFinite(i[1][1]) && isFinite(i[2][0]) && isFinite(i[2][1]) + } + + function aa(j, i, p) { + if (!h(i)) { + return + } + j.m_ = i; + if (p) { + var Z = i[0][0] * i[1][1] - i[0][1] * i[1][0]; + j.lineScale_ = N(H(Z)) + } + } + q.translate = function(m, j) { + var i = [ + [1, 0, 0], + [0, 1, 0], + [m, j, 1] + ]; + aa(this, J(i, this.m_), false) + }; + q.rotate = function(j) { + var p = A(j); + var m = l(j); + var i = [ + [p, m, 0], + [-m, p, 0], + [0, 0, 1] + ]; + aa(this, J(i, this.m_), false) + }; + q.scale = function(m, j) { + this.arcScaleX_ *= m; + this.arcScaleY_ *= j; + var i = [ + [m, 0, 0], + [0, j, 0], + [0, 0, 1] + ]; + aa(this, J(i, this.m_), true) + }; + q.transform = function(Z, p, ah, ag, j, i) { + var m = [ + [Z, p, 0], + [ah, ag, 0], + [j, i, 1] + ]; + aa(this, J(m, this.m_), true) + }; + q.setTransform = function(ag, Z, ai, ah, p, j) { + var i = [ + [ag, Z, 0], + [ai, ah, 0], + [p, j, 1] + ]; + aa(this, i, true) + }; + q.drawText_ = function(am, ak, aj, ap, ai) { + var ao = this.m_, + at = 1000, + j = 0, + ar = at, + ah = { + x: 0, + y: 0 + }, + ag = []; + var i = u(E(this.font), this.element_); + var p = ac(i); + var au = this.element_.currentStyle; + var Z = this.textAlign.toLowerCase(); + switch (Z) { + case "left": + case "center": + case "right": + break; + case "end": + Z = au.direction == "ltr" ? "right" : "left"; + break; + case "start": + Z = au.direction == "rtl" ? "right" : "left"; + break; + default: + Z = "left" + } + switch (this.textBaseline) { + case "hanging": + case "top": + ah.y = i.size / 1.75; + break; + case "middle": + break; + default: + case null: + case "alphabetic": + case "ideographic": + case "bottom": + ah.y = -i.size / 3; + break + } + switch (Z) { + case "right": + j = at; + ar = 0.05; + break; + case "center": + j = ar = at / 2; + break + } + var aq = V(this, ak + ah.x, aj + ah.y); + ag.push(''); + if (ai) { + w(this, ag) + } else { + G(this, ag, { + x: -j, + y: 0 + }, { + x: ar, + y: i.size + }) + } + var an = ao[0][0].toFixed(3) + "," + ao[1][0].toFixed(3) + "," + ao[0][1].toFixed(3) + "," + ao[1][1].toFixed(3) + ",0,0"; + var al = n(aq.x / d) + "," + n(aq.y / d); + ag.push('', '', ''); + this.element_.insertAdjacentHTML("beforeEnd", ag.join("")) + }; + q.fillText = function(m, i, p, j) { + this.drawText_(m, i, p, j, false) + }; + q.strokeText = function(m, i, p, j) { + this.drawText_(m, i, p, j, true) + }; + q.measureText = function(m) { + if (!this.textMeasureEl_) { + var i = ''; + this.element_.insertAdjacentHTML("beforeEnd", i); + this.textMeasureEl_ = this.element_.lastChild + } + var j = this.element_.ownerDocument; + this.textMeasureEl_.innerHTML = ""; + this.textMeasureEl_.style.font = this.font; + this.textMeasureEl_.appendChild(j.createTextNode(m)); + return { + width: this.textMeasureEl_.offsetWidth + } + }; + q.clip = function() {}; + q.arcTo = function() {}; + q.createPattern = function(j, i) { + return new T(j, i) + }; + + function U(i) { + this.type_ = i; + this.x0_ = 0; + this.y0_ = 0; + this.r0_ = 0; + this.x1_ = 0; + this.y1_ = 0; + this.r1_ = 0; + this.colors_ = [] + } + U.prototype.addColorStop = function(j, i) { + i = F(i); + this.colors_.push({ + offset: j, + color: i.color, + alpha: i.alpha + }) + }; + + function T(j, i) { + Q(j); + switch (i) { + case "repeat": + case null: + case "": + this.repetition_ = "repeat"; + break; + case "repeat-x": + case "repeat-y": + case "no-repeat": + this.repetition_ = i; + break; + default: + O("SYNTAX_ERR") + } + this.src_ = j.src; + this.width_ = j.width; + this.height_ = j.height + } + + function O(i) { + throw new P(i) + } + + function Q(i) { + if (!i || i.nodeType != 1 || i.tagName != "IMG") { + O("TYPE_MISMATCH_ERR") + } + if (i.readyState != "complete") { + O("INVALID_STATE_ERR") + } + } + + function P(i) { + this.code = this[i]; + this.message = i + ": DOM Exception " + this.code + } + var X = P.prototype = new Error(); + X.INDEX_SIZE_ERR = 1; + X.DOMSTRING_SIZE_ERR = 2; + X.HIERARCHY_REQUEST_ERR = 3; + X.WRONG_DOCUMENT_ERR = 4; + X.INVALID_CHARACTER_ERR = 5; + X.NO_DATA_ALLOWED_ERR = 6; + X.NO_MODIFICATION_ALLOWED_ERR = 7; + X.NOT_FOUND_ERR = 8; + X.NOT_SUPPORTED_ERR = 9; + X.INUSE_ATTRIBUTE_ERR = 10; + X.INVALID_STATE_ERR = 11; + X.SYNTAX_ERR = 12; + X.INVALID_MODIFICATION_ERR = 13; + X.NAMESPACE_ERR = 14; + X.INVALID_ACCESS_ERR = 15; + X.VALIDATION_ERR = 16; + X.TYPE_MISMATCH_ERR = 17; + G_vmlCanvasManager = e; + CanvasRenderingContext2D = D; + CanvasGradient = U; + CanvasPattern = T; + DOMException = P + })() +} +Ext.define("Ext.draw.engine.Canvas", { + extend: "Ext.draw.Surface", + requires: ["Ext.draw.engine.excanvas", "Ext.draw.Animator", "Ext.draw.Color"], + config: { + highPrecision: false + }, + statics: { + contextOverrides: { + setGradientBBox: function(a) { + this.bbox = a + }, + fill: function() { + var c = this.fillStyle, + a = this.fillGradient, + b = this.fillOpacity, + d = this.globalAlpha, + e = this.bbox; + if (c !== Ext.draw.Color.RGBA_NONE && b !== 0) { + if (a && e) { + this.fillStyle = a.generateGradient(this, e) + } + if (b !== 1) { + this.globalAlpha = d * b + } + this.$fill(); + if (b !== 1) { + this.globalAlpha = d + } + if (a && e) { + this.fillStyle = c + } + } + }, + stroke: function() { + var e = this.strokeStyle, + c = this.strokeGradient, + a = this.strokeOpacity, + b = this.globalAlpha, + d = this.bbox; + if (e !== Ext.draw.Color.RGBA_NONE && a !== 0) { + if (c && d) { + this.strokeStyle = c.generateGradient(this, d) + } + if (a !== 1) { + this.globalAlpha = b * a + } + this.$stroke(); + if (a !== 1) { + this.globalAlpha = b + } + if (c && d) { + this.strokeStyle = e + } + } + }, + fillStroke: function(d, e) { + var j = this, + i = this.fillStyle, + h = this.fillOpacity, + f = this.strokeStyle, + c = this.strokeOpacity, + b = j.shadowColor, + a = j.shadowBlur, + g = Ext.draw.Color.RGBA_NONE; + if (e === undefined) { + e = d.transformFillStroke + } + if (!e) { + d.inverseMatrix.toContext(j) + } + if (i !== g && h !== 0) { + j.fill(); + j.shadowColor = g; + j.shadowBlur = 0 + } + if (f !== g && c !== 0) { + j.stroke() + } + j.shadowColor = b; + j.shadowBlur = a + }, + setLineDash: function(a) { + if (this.$setLineDash) { + this.$setLineDash(a) + } + }, + getLineDash: function() { + if (this.$getLineDash) { + return this.$getLineDash() + } + }, + ellipse: function(g, e, c, a, j, b, f, d) { + var i = Math.cos(j), + h = Math.sin(j); + this.transform(i * c, h * c, -h * a, i * a, g, e); + this.arc(0, 0, 1, b, f, d); + this.transform(i / c, -h / a, h / c, i / a, -(i * g + h * e) / c, (h * g - i * e) / a) + }, + appendPath: function(f) { + var e = this, + c = 0, + b = 0, + a = f.commands, + g = f.params, + d = a.length; + e.beginPath(); + for (; c < d; c++) { + switch (a[c]) { + case "M": + e.moveTo(g[b], g[b + 1]); + b += 2; + break; + case "L": + e.lineTo(g[b], g[b + 1]); + b += 2; + break; + case "C": + e.bezierCurveTo(g[b], g[b + 1], g[b + 2], g[b + 3], g[b + 4], g[b + 5]); + b += 6; + break; + case "Z": + e.closePath(); + break + } + } + }, + save: function() { + var c = this.toSave, + d = c.length, + e = d && {}, + b = 0, + a; + for (; b < d; b++) { + a = c[b]; + if (a in this) { + e[a] = this[a] + } + } + this.state.push(e); + this.$save() + }, + restore: function() { + var b = this.state.pop(), + a; + if (b) { + for (a in b) { + this[a] = b[a] + } + } + this.$restore() + } + } + }, + splitThreshold: 3000, + toSave: ["fillGradient", "strokeGradient"], + element: { + reference: "element", + style: { + position: "absolute" + }, + children: [{ + reference: "innerElement", + style: { + width: "100%", + height: "100%", + position: "relative" + } + }] + }, + createCanvas: function() { + var c = Ext.Element.create({ + tag: "canvas", + cls: Ext.baseCSSPrefix + "surface-canvas" + }); + window.G_vmlCanvasManager && G_vmlCanvasManager.initElement(c.dom); + var d = Ext.draw.engine.Canvas.contextOverrides, + a = c.dom.getContext("2d"), + b; + if (a.ellipse) { + delete d.ellipse + } + a.state = []; + a.toSave = this.toSave; + for (b in d) { + a["$" + b] = a[b] + } + Ext.apply(a, d); + if (this.getHighPrecision()) { + this.enablePrecisionCompensation(a) + } else { + this.disablePrecisionCompensation(a) + } + this.innerElement.appendChild(c); + this.canvases.push(c); + this.contexts.push(a) + }, + updateHighPrecision: function(d) { + var e = this.contexts, + c = e.length, + b, a; + for (b = 0; b < c; b++) { + a = e[b]; + if (d) { + this.enablePrecisionCompensation(a) + } else { + this.disablePrecisionCompensation(a) + } + } + }, + precisionNames: ["rect", "fillRect", "strokeRect", "clearRect", "moveTo", "lineTo", "arc", "arcTo", "save", "restore", "updatePrecisionCompensate", "setTransform", "transform", "scale", "translate", "rotate", "quadraticCurveTo", "bezierCurveTo", "createLinearGradient", "createRadialGradient", "fillText", "strokeText", "drawImage"], + disablePrecisionCompensation: function(b) { + var a = Ext.draw.engine.Canvas.contextOverrides, + f = this.precisionNames, + e = f.length, + d, c; + for (d = 0; d < e; d++) { + c = f[d]; + if (!(c in a)) { + delete b[c] + } + } + this.setDirty(true) + }, + enablePrecisionCompensation: function(j) { + var c = this, + a = 1, + g = 1, + l = 0, + k = 0, + i = new Ext.draw.Matrix(), + b = [], + e = {}, + d = Ext.draw.engine.Canvas.contextOverrides, + h = j.constructor.prototype; + var f = { + toSave: c.toSave, + rect: function(m, p, n, o) { + return h.rect.call(this, m * a + l, p * g + k, n * a, o * g) + }, + fillRect: function(m, p, n, o) { + this.updatePrecisionCompensateRect(); + h.fillRect.call(this, m * a + l, p * g + k, n * a, o * g); + this.updatePrecisionCompensate() + }, + strokeRect: function(m, p, n, o) { + this.updatePrecisionCompensateRect(); + h.strokeRect.call(this, m * a + l, p * g + k, n * a, o * g); + this.updatePrecisionCompensate() + }, + clearRect: function(m, p, n, o) { + return h.clearRect.call(this, m * a + l, p * g + k, n * a, o * g) + }, + moveTo: function(m, n) { + return h.moveTo.call(this, m * a + l, n * g + k) + }, + lineTo: function(m, n) { + return h.lineTo.call(this, m * a + l, n * g + k) + }, + arc: function(n, r, m, p, o, q) { + this.updatePrecisionCompensateRect(); + h.arc.call(this, n * a + l, r * a + k, m * a, p, o, q); + this.updatePrecisionCompensate() + }, + arcTo: function(o, q, n, p, m) { + this.updatePrecisionCompensateRect(); + h.arcTo.call(this, o * a + l, q * g + k, n * a + l, p * g + k, m * a); + this.updatePrecisionCompensate() + }, + save: function() { + b.push(i); + i = i.clone(); + d.save.call(this); + h.save.call(this) + }, + restore: function() { + i = b.pop(); + d.restore.call(this); + h.restore.call(this); + this.updatePrecisionCompensate() + }, + updatePrecisionCompensate: function() { + i.precisionCompensate(c.devicePixelRatio, e); + a = e.xx; + g = e.yy; + l = e.dx; + k = e.dy; + h.setTransform.call(this, c.devicePixelRatio, e.b, e.c, e.d, 0, 0) + }, + updatePrecisionCompensateRect: function() { + i.precisionCompensateRect(c.devicePixelRatio, e); + a = e.xx; + g = e.yy; + l = e.dx; + k = e.dy; + h.setTransform.call(this, c.devicePixelRatio, e.b, e.c, e.d, 0, 0) + }, + setTransform: function(q, o, n, m, r, p) { + i.set(q, o, n, m, r, p); + this.updatePrecisionCompensate() + }, + transform: function(q, o, n, m, r, p) { + i.append(q, o, n, m, r, p); + this.updatePrecisionCompensate() + }, + scale: function(n, m) { + this.transform(n, 0, 0, m, 0, 0) + }, + translate: function(n, m) { + this.transform(1, 0, 0, 1, n, m) + }, + rotate: function(o) { + var n = Math.cos(o), + m = Math.sin(o); + this.transform(n, m, -m, n, 0, 0) + }, + quadraticCurveTo: function(n, p, m, o) { + h.quadraticCurveTo.call(this, n * a + l, p * g + k, m * a + l, o * g + k) + }, + bezierCurveTo: function(r, p, o, n, m, q) { + h.bezierCurveTo.call(this, r * a + l, p * g + k, o * a + l, n * g + k, m * a + l, q * g + k) + }, + createLinearGradient: function(n, p, m, o) { + this.updatePrecisionCompensateRect(); + var q = h.createLinearGradient.call(this, n * a + l, p * g + k, m * a + l, o * g + k); + this.updatePrecisionCompensate(); + return q + }, + createRadialGradient: function(p, r, o, n, q, m) { + this.updatePrecisionCompensateRect(); + var s = h.createLinearGradient.call(this, p * a + l, r * a + k, o * a, n * a + l, q * a + k, m * a); + this.updatePrecisionCompensate(); + return s + }, + fillText: function(o, m, p, n) { + h.setTransform.apply(this, i.elements); + if (typeof n === "undefined") { + h.fillText.call(this, o, m, p) + } else { + h.fillText.call(this, o, m, p, n) + } + this.updatePrecisionCompensate() + }, + strokeText: function(o, m, p, n) { + h.setTransform.apply(this, i.elements); + if (typeof n === "undefined") { + h.strokeText.call(this, o, m, p) + } else { + h.strokeText.call(this, o, m, p, n) + } + this.updatePrecisionCompensate() + }, + fill: function() { + var m = this.fillGradient, + n = this.bbox; + this.updatePrecisionCompensateRect(); + if (m && n) { + this.fillStyle = m.generateGradient(this, n) + } + h.fill.call(this); + this.updatePrecisionCompensate() + }, + stroke: function() { + var m = this.strokeGradient, + n = this.bbox; + this.updatePrecisionCompensateRect(); + if (m && n) { + this.strokeStyle = m.generateGradient(this, n) + } + h.stroke.call(this); + this.updatePrecisionCompensate() + }, + drawImage: function(u, s, r, q, p, o, n, m, t) { + switch (arguments.length) { + case 3: + return h.drawImage.call(this, u, s * a + l, r * g + k); + case 5: + return h.drawImage.call(this, u, s * a + l, r * g + k, q * a, p * g); + case 9: + return h.drawImage.call(this, u, s, r, q, p, o * a + l, n * g * k, m * a, t * g) + } + } + }; + Ext.apply(j, f); + this.setDirty(true) + }, + updateRect: function(a) { + this.callParent([a]); + var C = this, + p = Math.floor(a[0]), + e = Math.floor(a[1]), + g = Math.ceil(a[0] + a[2]), + B = Math.ceil(a[1] + a[3]), + u = C.devicePixelRatio, + D = C.canvases, + d = g - p, + y = B - e, + n = Math.round(C.splitThreshold / u), + c = C.xSplits = Math.ceil(d / n), + f = C.ySplits = Math.ceil(y / n), + v, s, q, A, z, x, o, m; + for (s = 0, z = 0; s < f; s++, z += n) { + for (v = 0, A = 0; v < c; v++, A += n) { + q = s * c + v; + if (q >= D.length) { + C.createCanvas() + } + x = D[q].dom; + x.style.left = A + "px"; + x.style.top = z + "px"; + m = Math.min(n, y - z); + if (m * u !== x.height) { + x.height = m * u; + x.style.height = m + "px" + } + o = Math.min(n, d - A); + if (o * u !== x.width) { + x.width = o * u; + x.style.width = o + "px" + } + C.applyDefaults(C.contexts[q]) + } + } + for (q += 1; q < D.length; q++) { + D[q].destroy() + } + C.activeCanvases = c * f; + D.length = C.activeCanvases; + C.clear() + }, + clearTransform: function() { + var f = this, + a = f.xSplits, + g = f.ySplits, + d = f.contexts, + h = f.splitThreshold, + l = f.devicePixelRatio, + e, c, b, m; + for (e = 0; e < a; e++) { + for (c = 0; c < g; c++) { + b = c * a + e; + m = d[b]; + m.translate(-h * e, -h * c); + m.scale(l, l); + f.matrix.toContext(m) + } + } + }, + renderSprite: function(q) { + var C = this, + b = C.getRect(), + e = C.matrix, + g = q.getParent(), + v = Ext.draw.Matrix.fly([1, 0, 0, 1, 0, 0]), + p = C.splitThreshold / C.devicePixelRatio, + c = C.xSplits, + m = C.ySplits, + A, z, s, a, r, o, d = 0, + B, n = 0, + f, l = b[2], + y = b[3], + x, u, t; + while (g && (g !== C)) { + v.prependMatrix(g.matrix || g.attr && g.attr.matrix); + g = g.getParent() + } + v.prependMatrix(e); + a = q.getBBox(); + if (a) { + a = v.transformBBox(a) + } + q.preRender(C); + if (q.attr.hidden || q.attr.globalAlpha === 0) { + q.setDirty(false); + return + } + for (u = 0, z = 0; u < m; u++, z += p) { + for (x = 0, A = 0; x < c; x++, A += p) { + t = u * c + x; + s = C.contexts[t]; + r = Math.min(p, l - A); + o = Math.min(p, y - z); + d = A; + B = d + r; + n = z; + f = n + o; + if (a) { + if (a.x > B || a.x + a.width < d || a.y > f || a.y + a.height < n) { + continue + } + } + s.save(); + q.useAttributes(s, b); + if (false === q.render(C, s, [d, n, r, o], b)) { + return false + } + s.restore() + } + } + q.setDirty(false) + }, + flatten: function(n, a) { + var k = document.createElement("canvas"), + f = Ext.getClassName(this), + g = this.devicePixelRatio, + l = k.getContext("2d"), + b, c, h, e, d, m; + k.width = Math.ceil(n.width * g); + k.height = Math.ceil(n.height * g); + for (e = 0; e < a.length; e++) { + b = a[e]; + if (Ext.getClassName(b) !== f) { + continue + } + h = b.getRect(); + for (d = 0; d < b.canvases.length; d++) { + c = b.canvases[d]; + m = c.getOffsetsTo(c.getParent()); + l.drawImage(c.dom, (h[0] + m[0]) * g, (h[1] + m[1]) * g) + } + } + return { + data: k.toDataURL(), + type: "png" + } + }, + applyDefaults: function(a) { + var b = Ext.draw.Color.RGBA_NONE; + a.strokeStyle = b; + a.fillStyle = b; + a.textAlign = "start"; + a.textBaseline = "alphabetic"; + a.miterLimit = 1 + }, + clear: function() { + var d = this, + e = d.activeCanvases, + c, b, a; + for (c = 0; c < e; c++) { + b = d.canvases[c].dom; + a = d.contexts[c]; + a.setTransform(1, 0, 0, 1, 0, 0); + a.clearRect(0, 0, b.width, b.height) + } + d.setDirty(true) + }, + destroy: function() { + var c = this, + a, b = c.canvases.length; + for (a = 0; a < b; a++) { + c.contexts[a] = null; + c.canvases[a].destroy(); + c.canvases[a] = null + } + delete c.contexts; + delete c.canvases; + c.callParent() + }, + privates: { + initElement: function() { + var a = this; + a.callParent(); + a.canvases = []; + a.contexts = []; + a.activeCanvases = (a.xSplits = 0) * (a.ySplits = 0) + } + } +}, function() { + var c = this, + b = c.prototype, + a = 10000000000; + if (Ext.os.is.Android4 && Ext.browser.is.Chrome) { + a = 3000 + } else { + if (Ext.is.iOS) { + a = 2200 + } + } + b.splitThreshold = a +}); +Ext.define("Ext.draw.Container", { + extend: "Ext.draw.ContainerBase", + alternateClassName: "Ext.draw.Component", + xtype: "draw", + defaultType: "surface", + isDrawContainer: true, + requires: ["Ext.draw.Surface", "Ext.draw.engine.Svg", "Ext.draw.engine.Canvas", "Ext.draw.gradient.GradientDefinition"], + engine: "Ext.draw.engine.Canvas", + config: { + cls: Ext.baseCSSPrefix + "draw-container", + resizeHandler: null, + sprites: null, + gradients: [] + }, + defaultDownloadServerUrl: "http://svg.sencha.io", + supportedFormats: ["png", "pdf", "jpeg", "gif"], + supportedOptions: { + version: Ext.isNumber, + data: Ext.isString, + format: function(a) { + return Ext.Array.indexOf(this.supportedFormats, a) >= 0 + }, + filename: Ext.isString, + width: Ext.isNumber, + height: Ext.isNumber, + scale: Ext.isNumber, + pdf: Ext.isObject, + jpeg: Ext.isObject + }, + initAnimator: function() { + this.frameCallbackId = Ext.draw.Animator.addFrameCallback("renderFrame", this) + }, + applyGradients: function(b) { + var a = [], + c, f, d, e; + if (!Ext.isArray(b)) { + return a + } + for (c = 0, f = b.length; c < f; c++) { + d = b[c]; + if (!Ext.isObject(d)) { + continue + } + if (typeof d.type !== "string") { + d.type = "linear" + } + if (d.angle) { + d.degrees = d.angle; + delete d.angle + } + if (Ext.isObject(d.stops)) { + d.stops = (function(i) { + var g = [], + h; + for (e in i) { + h = i[e]; + h.offset = e / 100; + g.push(h) + } + return g + })(d.stops) + } + a.push(d) + } + Ext.draw.gradient.GradientDefinition.add(a); + return a + }, + applySprites: function(f) { + if (!f) { + return + } + f = Ext.Array.from(f); + var e = f.length, + b = [], + d, a, c; + for (d = 0; d < e; d++) { + c = f[d]; + a = c.surface; + if (!(a && a.isSurface)) { + if (Ext.isString(a)) { + a = this.getSurface(a) + } else { + a = this.getSurface("main") + } + } + c = a.add(c); + b.push(c) + } + return b + }, + onBodyResize: function() { + var b = this.element, + a; + if (!b) { + return + } + a = b.getSize(); + if (a.width && a.height) { + this.setBodySize(a) + } + }, + setBodySize: function(c) { + var d = this, + b = d.getResizeHandler() || d.defaultResizeHandler, + a; + d.fireEvent("bodyresize", d, c); + a = b.call(d, c); + if (a !== false) { + d.renderFrame() + } + }, + defaultResizeHandler: function(a) { + this.getItems().each(function(b) { + b.setRect([0, 0, a.width, a.height]) + }) + }, + getSurface: function(d) { + d = this.getId() + "-" + (d || "main"); + var c = this, + b = c.getItems(), + a = b.get(d); + if (!a) { + a = c.add({ + xclass: c.engine, + id: d + }); + c.onBodyResize() + } + return a + }, + renderFrame: function() { + var e = this, + a = e.getItems(), + b, d, c; + for (b = 0, d = a.length; b < d; b++) { + c = a.items[b]; + if (c.isSurface) { + c.renderFrame() + } + } + }, + getImage: function(k) { + var l = this.innerElement.getSize(), + a = Array.prototype.slice.call(this.items.items), + d, g, c = this.surfaceZIndexes, + f, e, b, h; + for (e = 1; e < a.length; e++) { + b = a[e]; + h = c[b.type]; + f = e - 1; + while (f >= 0 && c[a[f].type] > h) { + a[f + 1] = a[f]; + f-- + } + a[f + 1] = b + } + d = a[0].flatten(l, a); + if (k === "image") { + g = new Image(); + g.src = d.data; + d.data = g; + return d + } + if (k === "stream") { + d.data = d.data.replace(/^data:image\/[^;]+/, "data:application/octet-stream"); + return d + } + return d + }, + download: function(d) { + var e = this, + a = [], + b, c, f; + d = Ext.apply({ + version: 2, + data: e.getImage().data + }, d); + for (c in d) { + if (d.hasOwnProperty(c)) { + f = d[c]; + if (c in e.supportedOptions) { + if (e.supportedOptions[c].call(e, f)) { + a.push({ + tag: "input", + type: "hidden", + name: c, + value: Ext.String.htmlEncode(Ext.isObject(f) ? Ext.JSON.encode(f) : f) + }) + } + } + } + } + b = Ext.dom.Helper.markup({ + tag: "html", + children: [{ + tag: "head" + }, { + tag: "body", + children: [{ + tag: "form", + method: "POST", + action: d.url || e.defaultDownloadServerUrl, + children: a + }, { + tag: "script", + type: "text/javascript", + children: 'document.getElementsByTagName("form")[0].submit();' + }] + }] + }); + window.open("", "ImageDownload_" + Date.now()).document.write(b) + }, + destroy: function() { + var a = this.frameCallbackId; + if (a) { + Ext.draw.Animator.removeFrameCallback(a) + } + this.callParent() + } +}, function() { + if (location.search.match("svg")) { + Ext.draw.Container.prototype.engine = "Ext.draw.engine.Svg" + } else { + if ((Ext.os.is.BlackBerry && Ext.os.version.getMajor() === 10) || (Ext.browser.is.AndroidStock4 && (Ext.os.version.getMinor() === 1 || Ext.os.version.getMinor() === 2 || Ext.os.version.getMinor() === 3))) { + Ext.draw.Container.prototype.engine = "Ext.draw.engine.Svg" + } + } +}); +Ext.define("Ext.chart.theme.Base", { + mixins: { + factoryable: "Ext.mixin.Factoryable" + }, + requires: ["Ext.draw.Color"], + factoryConfig: { + type: "chart.theme" + }, + isTheme: true, + config: { + baseColor: null, + colors: undefined, + gradients: null, + chart: { + defaults: { + background: "#23272a" + } + }, + axis: { + defaults: { + label: { + x: 0, + y: 0, + textBaseline: "middle", + textAlign: "center", + fontSize: "default", + fontFamily: "default", + fontWeight: "default", + fillStyle: "black", + color: "white" + }, + title: { + fillStyle: "black", + fontSize: "default*1.23", + fontFamily: "default", + fontWeight: "default", + color: "white" + }, + style: { + strokeStyle: "black" + }, + grid: { + strokeStyle: "rgba(44, 47, 51, 1)" + } + }, + top: { + style: { + textPadding: 5 + } + }, + bottom: { + style: { + textPadding: 5 + } + } + }, + series: { + defaults: { + label: { + fillStyle: "black", + strokeStyle: "none", + fontFamily: "default", + fontWeight: "default", + fontSize: "default*1.077", + textBaseline: "middle", + textAlign: "center" + }, + labelOverflowPadding: 5 + } + }, + sprites: { + text: { + fontSize: "default", + fontWeight: "default", + fontFamily: "default", + fillStyle: "black", + color: "white" + } + }, + seriesThemes: undefined, + markerThemes: { + type: ["circle", "cross", "plus", "square", "triangle", "diamond"] + }, + useGradients: false, + background: null + }, + colorDefaults: ["#94ae0a", "#115fa6", "#a61120", "#ff8809", "#ffd13e", "#a61187", "#24ad9a", "#7c7474", "#a66111"], + constructor: function(a) { + this.initConfig(a); + this.resolveDefaults() + }, + defaultRegEx: /^default([+\-/\*]\d+(?:\.\d+)?)?$/, + defaultOperators: { + "*": function(b, a) { + return b * a + }, + "+": function(b, a) { + return b + a + }, + "-": function(b, a) { + return b - a + } + }, + resolveDefaults: function() { + var a = this; + Ext.onReady(function() { + var f = Ext.clone(a.getSprites()), + e = Ext.clone(a.getAxis()), + d = Ext.clone(a.getSeries()), + g, c, b; + if (!a.superclass.defaults) { + g = Ext.getBody().createChild({ + tag: "div", + cls: "x-component" + }); + a.superclass.defaults = { + fontFamily: g.getStyle("fontFamily"), + fontWeight: g.getStyle("fontWeight"), + fontSize: parseFloat(g.getStyle("fontSize")), + fontVariant: g.getStyle("fontVariant"), + fontStyle: g.getStyle("fontStyle") + }; + g.destroy() + } + a.replaceDefaults(f.text); + a.setSprites(f); + for (c in e) { + b = e[c]; + a.replaceDefaults(b.label); + a.replaceDefaults(b.title) + } + a.setAxis(e); + for (c in d) { + b = d[c]; + a.replaceDefaults(b.label) + } + a.setSeries(d) + }) + }, + replaceDefaults: function(h) { + var e = this, + g = e.superclass.defaults, + a = e.defaultRegEx, + d, f, c, b; + if (Ext.isObject(h)) { + for (d in g) { + c = a.exec(h[d]); + if (c) { + f = g[d]; + c = c[1]; + if (c) { + b = e.defaultOperators[c.charAt(0)]; + f = Math.round(b(f, parseFloat(c.substr(1)))) + } + h[d] = f + } + } + } + }, + applyBaseColor: function(c) { + var a, b; + if (c) { + a = c.isColor ? c : Ext.draw.Color.fromString(c); + b = a.getHSL()[2]; + if (b < 0.15) { + a = a.createLighter(0.3) + } else { + if (b < 0.3) { + a = a.createLighter(0.15) + } else { + if (b > 0.85) { + a = a.createDarker(0.3) + } else { + if (b > 0.7) { + a = a.createDarker(0.15) + } + } + } + } + this.setColors([a.createDarker(0.3).toString(), a.createDarker(0.15).toString(), a.toString(), a.createLighter(0.12).toString(), a.createLighter(0.24).toString(), a.createLighter(0.31).toString()]) + } + return c + }, + applyColors: function(a) { + return a || this.colorDefaults + }, + updateUseGradients: function(a) { + if (a) { + this.updateGradients({ + type: "linear", + degrees: 90 + }) + } + }, + updateBackground: function(a) { + if (a) { + var b = this.getChart(); + b.defaults.background = a; + this.setChart(b) + } + }, + updateGradients: function(a) { + var c = this.getColors(), + e = [], + h, b, d, f, g; + if (Ext.isObject(a)) { + for (f = 0, g = c && c.length || 0; f < g; f++) { + b = Ext.draw.Color.fromString(c[f]); + if (b) { + d = b.createLighter(0.15).toString(); + h = Ext.apply(Ext.Object.chain(a), { + stops: [{ + offset: 1, + color: b.toString() + }, { + offset: 0, + color: d.toString() + }] + }); + e.push(h) + } + } + this.setColors(e) + } + }, + applySeriesThemes: function(a) { + this.getBaseColor(); + this.getUseGradients(); + this.getGradients(); + var b = this.getColors(); + if (!a) { + a = { + fillStyle: Ext.Array.clone(b), + strokeStyle: Ext.Array.map(b, function(d) { + var c = Ext.draw.Color.fromString(d.stops ? d.stops[0].color : d); + return c.createDarker(0.15).toString() + }) + } + } + return a + } +}); +Ext.define("Ext.chart.theme.Default", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.default", "chart.theme.Base"] +}); +Ext.define("Ext.chart.Markers", { + extend: "Ext.draw.sprite.Instancing", + isMarkers: true, + defaultCategory: "default", + constructor: function() { + this.callParent(arguments); + this.categories = {}; + this.revisions = {} + }, + destroy: function() { + this.categories = null; + this.revisions = null; + this.callParent() + }, + getMarkerFor: function(b, a) { + if (b in this.categories) { + var c = this.categories[b]; + if (a in c) { + return this.get(c[a]) + } + } + }, + clear: function(a) { + a = a || this.defaultCategory; + if (!(a in this.revisions)) { + this.revisions[a] = 1 + } else { + this.revisions[a]++ + } + }, + putMarkerFor: function(e, b, c, h, f) { + e = e || this.defaultCategory; + var d = this, + g = d.categories[e] || (d.categories[e] = {}), + a; + if (c in g) { + d.setAttributesFor(g[c], b, h) + } else { + g[c] = d.getCount(); + d.createInstance(b, h) + } + a = d.get(g[c]); + if (a) { + a.category = e; + if (!f) { + a.revision = d.revisions[e] || (d.revisions[e] = 1) + } + } + }, + getMarkerBBoxFor: function(c, a, b) { + if (c in this.categories) { + var d = this.categories[c]; + if (a in d) { + return this.getBBoxFor(d[a], b) + } + } + }, + getBBox: function() { + return null + }, + render: function(a, l, b) { + var f = this, + k = f.revisions, + j = f.attr.matrix, + h = f.getTemplate(), + d = h.attr, + g, c, e; + j.toContext(l); + h.preRender(a, l, b); + h.useAttributes(l, b); + for (c = 0, e = f.instances.length; c < e; c++) { + g = f.get(c); + if (g.hidden || g.revision !== k[g.category]) { + continue + } + l.save(); + h.attr = g; + h.useAttributes(l, b); + h.render(a, l, b); + l.restore() + } + h.attr = d + } +}); +Ext.define("Ext.chart.label.Callout", { + extend: "Ext.draw.modifier.Modifier", + prepareAttributes: function(a) { + if (!a.hasOwnProperty("calloutOriginal")) { + a.calloutOriginal = Ext.Object.chain(a); + a.calloutOriginal.prototype = a + } + if (this._previous) { + this._previous.prepareAttributes(a.calloutOriginal) + } + }, + setAttrs: function(e, h) { + var d = e.callout, + i = e.calloutOriginal, + l = e.bbox.plain, + c = (l.width || 0) + e.labelOverflowPadding, + m = (l.height || 0) + e.labelOverflowPadding, + p, o; + if ("callout" in h) { + d = h.callout + } + if ("callout" in h || "calloutPlaceX" in h || "calloutPlaceY" in h || "x" in h || "y" in h) { + var n = "rotationRads" in h ? i.rotationRads = h.rotationRads : i.rotationRads, + g = "x" in h ? (i.x = h.x) : i.x, + f = "y" in h ? (i.y = h.y) : i.y, + b = "calloutPlaceX" in h ? h.calloutPlaceX : e.calloutPlaceX, + a = "calloutPlaceY" in h ? h.calloutPlaceY : e.calloutPlaceY, + k = "calloutVertical" in h ? h.calloutVertical : e.calloutVertical, + j; + n %= Math.PI * 2; + if (Math.cos(n) < 0) { + n = (n + Math.PI) % (Math.PI * 2) + } + if (n > Math.PI) { + n -= Math.PI * 2 + } + if (k) { + n = n * (1 - d) - Math.PI / 2 * d; + j = c; + c = m; + m = j + } else { + n = n * (1 - d) + } + h.rotationRads = n; + h.x = g * (1 - d) + b * d; + h.y = f * (1 - d) + a * d; + p = b - g; + o = a - f; + if (Math.abs(o * c) > Math.abs(p * m)) { + if (o > 0) { + h.calloutEndX = h.x - (m / 2) * (p / o) * d; + h.calloutEndY = h.y - (m / 2) * d + } else { + h.calloutEndX = h.x + (m / 2) * (p / o) * d; + h.calloutEndY = h.y + (m / 2) * d + } + } else { + if (p > 0) { + h.calloutEndX = h.x - c / 2; + h.calloutEndY = h.y - (c / 2) * (o / p) * d + } else { + h.calloutEndX = h.x + c / 2; + h.calloutEndY = h.y + (c / 2) * (o / p) * d + } + } + if (h.calloutStartX && h.calloutStartY) { + h.calloutHasLine = (p > 0 && h.calloutStartX < h.calloutEndX) || (p <= 0 && h.calloutStartX > h.calloutEndX) || (o > 0 && h.calloutStartY < h.calloutEndY) || (o <= 0 && h.calloutStartY > h.calloutEndY) + } else { + h.calloutHasLine = true + } + } + return h + }, + pushDown: function(a, b) { + b = this.callParent([a.calloutOriginal, b]); + return this.setAttrs(a, b) + }, + popUp: function(a, b) { + a = a.prototype; + b = this.setAttrs(a, b); + if (this._next) { + return this._next.popUp(a, b) + } else { + return Ext.apply(a, b) + } + } +}); +Ext.define("Ext.chart.label.Label", { + extend: "Ext.draw.sprite.Text", + requires: ["Ext.chart.label.Callout"], + inheritableStatics: { + def: { + processors: { + callout: "limited01", + calloutHasLine: "bool", + calloutPlaceX: "number", + calloutPlaceY: "number", + calloutStartX: "number", + calloutStartY: "number", + calloutEndX: "number", + calloutEndY: "number", + calloutColor: "color", + calloutWidth: "number", + calloutVertical: "bool", + labelOverflowPadding: "number", + display: "enums(none,under,over,rotate,insideStart,insideEnd,inside,outside)", + orientation: "enums(horizontal,vertical)", + renderer: "default" + }, + defaults: { + callout: 0, + calloutHasLine: true, + calloutPlaceX: 0, + calloutPlaceY: 0, + calloutStartX: 0, + calloutStartY: 0, + calloutEndX: 0, + calloutEndY: 0, + calloutWidth: 1, + calloutVertical: false, + calloutColor: "black", + labelOverflowPadding: 5, + display: "none", + orientation: "", + renderer: null + }, + triggers: { + callout: "transform", + calloutPlaceX: "transform", + calloutPlaceY: "transform", + labelOverflowPadding: "transform", + calloutRotation: "transform", + display: "hidden" + }, + updaters: { + hidden: function(a) { + a.hidden = a.display === "none" + } + } + } + }, + config: { + fx: { + customDurations: { + callout: 200 + } + }, + field: null, + calloutLine: true + }, + applyCalloutLine: function(a) { + if (a) { + return Ext.apply({}, a) + } + }, + prepareModifiers: function() { + this.callParent(arguments); + this.calloutModifier = new Ext.chart.label.Callout({ + sprite: this + }); + this.fx.setNext(this.calloutModifier); + this.calloutModifier.setNext(this.topModifier) + }, + render: function(b, c) { + var e = this, + a = e.attr, + d = a.calloutColor; + c.save(); + c.globalAlpha *= a.callout; + if (c.globalAlpha > 0 && a.calloutHasLine) { + if (d && d.isGradient) { + d = d.getStops()[0].color + } + c.strokeStyle = d; + c.fillStyle = d; + c.lineWidth = a.calloutWidth; + c.beginPath(); + c.moveTo(e.attr.calloutStartX, e.attr.calloutStartY); + c.lineTo(e.attr.calloutEndX, e.attr.calloutEndY); + c.stroke(); + c.beginPath(); + c.arc(e.attr.calloutStartX, e.attr.calloutStartY, 1 * a.calloutWidth, 0, 2 * Math.PI, true); + c.fill(); + c.beginPath(); + c.arc(e.attr.calloutEndX, e.attr.calloutEndY, 1 * a.calloutWidth, 0, 2 * Math.PI, true); + c.fill() + } + c.restore(); + Ext.draw.sprite.Text.prototype.render.apply(e, arguments) + } +}); +Ext.define("Ext.chart.series.Series", { + requires: ["Ext.chart.Markers", "Ext.chart.label.Label", "Ext.tip.ToolTip"], + mixins: ["Ext.mixin.Observable", "Ext.mixin.Bindable"], + isSeries: true, + defaultBindProperty: "store", + type: null, + seriesType: "sprite", + identifiablePrefix: "ext-line-", + observableType: "series", + darkerStrokeRatio: 0.15, + config: { + chart: null, + title: null, + renderer: null, + showInLegend: true, + triggerAfterDraw: false, + style: {}, + subStyle: {}, + themeStyle: {}, + colors: null, + useDarkerStrokeColor: true, + store: null, + label: {}, + labelOverflowPadding: null, + showMarkers: true, + marker: null, + markerSubStyle: null, + itemInstancing: null, + background: null, + highlightItem: null, + surface: null, + overlaySurface: null, + hidden: false, + highlight: false, + highlightCfg: { + merge: function(a) { + return a + }, + $value: { + fillStyle: "yellow", + strokeStyle: "red" + } + }, + animation: null, + tooltip: null + }, + directions: [], + sprites: null, + themeColorCount: function() { + return 1 + }, + isStoreDependantColorCount: false, + themeMarkerCount: function() { + return 0 + }, + getFields: function(f) { + var e = this, + a = [], + c, b, d; + for (b = 0, d = f.length; b < d; b++) { + c = e["get" + f[b] + "Field"](); + if (Ext.isArray(c)) { + a.push.apply(a, c) + } else { + a.push(c) + } + } + return a + }, + applyAnimation: function(a, b) { + if (!a) { + a = { + duration: 0 + } + } else { + if (a === true) { + a = { + easing: "easeInOut", + duration: 500 + } + } + } + return b ? Ext.apply({}, a, b) : a + }, + getAnimation: function() { + var a = this.getChart(); + if (a && a.animationSuspendCount) { + return { + duration: 0 + } + } else { + return this.callParent() + } + }, + updateTitle: function(a) { + var j = this, + g = j.getChart(); + if (!g || g.isInitializing) { + return + } + a = Ext.Array.from(a); + var c = g.getSeries(), + b = Ext.Array.indexOf(c, j), + e = g.getLegendStore(), + h = j.getYField(), + d, l, k, f; + if (e.getCount() && b !== -1) { + f = h ? Math.min(a.length, h.length) : a.length; + for (d = 0; d < f; d++) { + k = a[d]; + l = e.getAt(b + d); + if (k && l) { + l.set("name", k) + } + } + } + }, + applyHighlight: function(a, b) { + if (Ext.isObject(a)) { + a = Ext.merge({}, this.config.highlightCfg, a) + } else { + if (a === true) { + a = this.config.highlightCfg + } + } + return Ext.apply(b || {}, a) + }, + updateHighlight: function(a) { + this.getStyle(); + if (!Ext.Object.isEmpty(a)) { + this.addItemHighlight() + } + }, + updateHighlightCfg: function(a) { + if (!Ext.Object.equals(a, this.defaultConfig.highlightCfg)) { + this.addItemHighlight() + } + }, + applyItemInstancing: function(a, b) { + return Ext.merge(b || {}, a) + }, + setAttributesForItem: function(c, d) { + var b = c && c.sprite, + a; + if (b) { + if (b.itemsMarker && c.category === "items") { + b.putMarker(c.category, d, c.index, false, true) + } + if (b.isMarkerHolder && c.category === "markers") { + b.putMarker(c.category, d, c.index, false, true) + } else { + if (b.isInstancing) { + b.setAttributesFor(c.index, d) + } else { + if (Ext.isArray(b)) { + for (a = 0; a < b.length; a++) { + b[a].setAttributes(d) + } + } else { + b.setAttributes(d) + } + } + } + } + }, + getBBoxForItem: function(a) { + if (a && a.sprite) { + if (a.sprite.itemsMarker && a.category === "items") { + return a.sprite.getMarkerBBox(a.category, a.index) + } else { + if (a.sprite instanceof Ext.draw.sprite.Instancing) { + return a.sprite.getBBoxFor(a.index) + } else { + return a.sprite.getBBox() + } + } + } + return null + }, + applyHighlightItem: function(d, a) { + if (d === a) { + return + } + if (Ext.isObject(d) && Ext.isObject(a)) { + var c = d.sprite === a.sprite, + b = d.index === a.index; + if (c && b) { + return + } + } + return d + }, + updateHighlightItem: function(b, a) { + this.setAttributesForItem(a, { + highlighted: false + }); + this.setAttributesForItem(b, { + highlighted: true + }) + }, + constructor: function(a) { + var b = this, + c; + a = a || {}; + if (a.tips) { + a = Ext.apply({ + tooltip: a.tips + }, a) + } + if (a.highlightCfg) { + a = Ext.apply({ + highlight: a.highlightCfg + }, a) + } + if ("id" in a) { + c = a.id + } else { + if ("id" in b.config) { + c = b.config.id + } else { + c = b.getId() + } + } + b.setId(c); + b.sprites = []; + b.dataRange = []; + b.mixins.observable.constructor.call(b, a); + b.initBindable() + }, + lookupViewModel: function(a) { + var b = this.getChart(); + return b ? b.lookupViewModel(a) : null + }, + applyTooltip: function(c, b) { + var a = Ext.apply({ + xtype: "tooltip", + renderer: Ext.emptyFn, + constrainPosition: true, + shrinkWrapDock: true, + autoHide: true, + offsetX: 10, + offsetY: 10 + }, c); + return Ext.create(a) + }, + updateTooltip: function() { + this.addItemHighlight() + }, + addItemHighlight: function() { + var d = this.getChart(); + if (!d) { + return + } + var e = d.getInteractions(), + c, a, b; + for (c = 0; c < e.length; c++) { + a = e[c]; + if (a.isItemHighlight || a.isItemEdit) { + b = true; + break + } + } + if (!b) { + e.push("itemhighlight"); + d.setInteractions(e) + } + }, + showTooltip: function(l, m) { + var d = this, + n = d.getTooltip(), + j, a, i, f, h, k, g, e, b, c; + if (!n) { + return + } + clearTimeout(d.tooltipTimeout); + b = n.config; + if (n.trackMouse) { + m[0] += b.offsetX; + m[1] += b.offsetY + } else { + j = l.sprite; + a = j.getSurface(); + i = Ext.get(a.getId()); + if (i) { + k = l.series.getBBoxForItem(l); + g = k.x + k.width / 2; + e = k.y + k.height / 2; + h = a.matrix.transformPoint([g, e]); + f = i.getXY(); + c = a.getInherited().rtl; + g = c ? f[0] + i.getWidth() - h[0] : f[0] + h[0]; + e = f[1] + h[1]; + m = [g, e] + } + } + Ext.callback(n.renderer, n.scope, [n, l.record, l], 0, d); + n.show(m) + }, + hideTooltip: function(b) { + var a = this, + c = a.getTooltip(); + if (!c) { + return + } + clearTimeout(a.tooltipTimeout); + a.tooltipTimeout = Ext.defer(function() { + c.hide() + }, 1) + }, + applyStore: function(a) { + return a && Ext.StoreManager.lookup(a) + }, + getStore: function() { + return this._store || this.getChart() && this.getChart().getStore() + }, + updateStore: function(b, a) { + var h = this, + g = h.getChart(), + c = g && g.getStore(), + f, j, e, d; + a = a || c; + if (a && a !== b) { + a.un({ + datachanged: "onDataChanged", + update: "onDataChanged", + scope: h + }) + } + if (b) { + b.on({ + datachanged: "onDataChanged", + update: "onDataChanged", + scope: h + }); + f = h.getSprites(); + for (d = 0, e = f.length; d < e; d++) { + j = f[d]; + if (j.setStore) { + j.setStore(b) + } + } + h.onDataChanged() + } + h.fireEvent("storechange", h, b, a) + }, + onStoreChange: function(b, a, c) { + if (!this._store) { + this.updateStore(a, c) + } + }, + coordinate: function(o, m, e) { + var l = this, + p = l.getStore(), + h = l.getHidden(), + k = p.getData().items, + b = l["get" + o + "Axis"](), + f = { + min: Infinity, + max: -Infinity + }, + q = l["fieldCategory" + o] || [o], + g = l.getFields(q), + d, n, c, a = {}, + j = l.getSprites(); + if (j.length > 0) { + if (!Ext.isBoolean(h) || !h) { + for (d = 0; d < q.length; d++) { + n = g[d]; + c = l.coordinateData(k, n, b); + l.getRangeOfData(c, f); + a["data" + q[d]] = c + } + } + l.dataRange[m] = f.min; + l.dataRange[m + e] = f.max; + a["dataMin" + o] = f.min; + a["dataMax" + o] = f.max; + if (b) { + b.range = null; + a["range" + o] = b.getRange() + } + for (d = 0; d < j.length; d++) { + j[d].setAttributes(a) + } + } + }, + coordinateData: function(b, h, d) { + var g = [], + f = b.length, + e = d && d.getLayout(), + c, a; + for (c = 0; c < f; c++) { + a = b[c].data[h]; + if (!Ext.isEmpty(a, true)) { + if (e) { + g[c] = e.getCoordFor(a, h, c, b) + } else { + g[c] = +a + } + } else { + g[c] = a + } + } + return g + }, + getRangeOfData: function(g, b) { + var e = g.length, + d = b.min, + a = b.max, + c, f; + for (c = 0; c < e; c++) { + f = g[c]; + if (f < d) { + d = f + } + if (f > a) { + a = f + } + } + b.min = d; + b.max = a + }, + updateLabelData: function() { + var h = this, + l = h.getStore(), + g = l.getData().items, + f = h.getSprites(), + a = h.getLabel().getTemplate(), + n = Ext.Array.from(a.getField()), + c, b, e, d, m, k; + if (!f.length || !n.length) { + return + } + for (c = 0; c < f.length; c++) { + d = []; + m = f[c]; + k = m.getField(); + if (Ext.Array.indexOf(n, k) < 0) { + k = n[c] + } + for (b = 0, e = g.length; b < e; b++) { + d.push(g[b].get(k)) + } + m.setAttributes({ + labels: d + }) + } + }, + processData: function() { + if (!this.getStore()) { + return + } + var d = this, + f = this.directions, + a, c = f.length, + e, b; + for (a = 0; a < c; a++) { + e = f[a]; + b = d["get" + e + "Axis"](); + if (b) { + b.processData(d); + continue + } + if (d["coordinate" + e]) { + d["coordinate" + e]() + } + } + d.updateLabelData() + }, + applyBackground: function(a) { + if (this.getChart()) { + this.getSurface().setBackground(a); + return this.getSurface().getBackground() + } else { + return a + } + }, + updateChart: function(d, a) { + var c = this, + b = c._store; + if (a) { + a.un("axeschange", "onAxesChange", c); + c.clearSprites(); + c.setSurface(null); + c.setOverlaySurface(null); + a.unregister(c); + c.onChartDetached(a); + if (!b) { + c.updateStore(null) + } + } + if (d) { + c.setSurface(d.getSurface("series")); + c.setOverlaySurface(d.getSurface("overlay")); + d.on("axeschange", "onAxesChange", c); + if (d.getAxes()) { + c.onAxesChange(d) + } + c.onChartAttached(d); + d.register(c); + if (!b) { + c.updateStore(d.getStore()) + } + } + }, + onAxesChange: function(h) { + var k = this, + g = h.getAxes(), + c, a = {}, + b = {}, + e = false, + j = this.directions, + l, d, f; + for (d = 0, f = j.length; d < f; d++) { + l = j[d]; + b[l] = k.getFields(k["fieldCategory" + l]) + } + for (d = 0, f = g.length; d < f; d++) { + c = g[d]; + if (!a[c.getDirection()]) { + a[c.getDirection()] = [c] + } else { + a[c.getDirection()].push(c) + } + } + for (d = 0, f = j.length; d < f; d++) { + l = j[d]; + if (k["get" + l + "Axis"]()) { + continue + } + if (a[l]) { + c = k.findMatchingAxis(a[l], b[l]); + if (c) { + k["set" + l + "Axis"](c); + if (c.getNeedHighPrecision()) { + e = true + } + } + } + } + this.getSurface().setHighPrecision(e) + }, + findMatchingAxis: function(f, e) { + var d, c, b, a; + for (b = 0; b < f.length; b++) { + d = f[b]; + c = d.getFields(); + if (!c.length) { + return d + } else { + if (e) { + for (a = 0; a < e.length; a++) { + if (Ext.Array.indexOf(c, e[a]) >= 0) { + return d + } + } + } + } + } + }, + onChartDetached: function(a) { + var b = this; + b.fireEvent("chartdetached", a, b); + a.un("storechange", "onStoreChange", b) + }, + onChartAttached: function(a) { + var b = this; + b.setBackground(b.getBackground()); + b.fireEvent("chartattached", a, b); + a.on("storechange", "onStoreChange", b); + b.processData() + }, + updateOverlaySurface: function(a) { + var b = this; + if (a) { + if (b.getLabel()) { + b.getOverlaySurface().add(b.getLabel()) + } + } + }, + applyLabel: function(a, b) { + if (!b) { + b = new Ext.chart.Markers({ + zIndex: 10 + }); + b.setTemplate(new Ext.chart.label.Label(a)) + } else { + b.getTemplate().setAttributes(a) + } + return b + }, + createItemInstancingSprite: function(c, b) { + var e = this, + f = new Ext.chart.Markers(), + a, d; + f.setAttributes({ + zIndex: Number.MAX_VALUE + }); + a = Ext.apply({}, b); + if (e.getHighlight()) { + a.highlight = e.getHighlight(); + a.modifiers = ["highlight"] + } + f.setTemplate(a); + d = f.getTemplate(); + d.setAttributes(e.getStyle()); + d.fx.on("animationstart", "onSpriteAnimationStart", this); + d.fx.on("animationend", "onSpriteAnimationEnd", this); + c.bindMarker("items", f); + e.getSurface().add(f); + return f + }, + getDefaultSpriteConfig: function() { + return { + type: this.seriesType, + renderer: this.getRenderer() + } + }, + updateRenderer: function(c) { + var b = this, + a = b.getChart(), + d; + if (a && a.isInitializing) { + return + } + d = b.getSprites(); + if (d.length) { + d[0].setAttributes({ + renderer: c || null + }); + if (a && !a.isInitializing) { + a.redraw() + } + } + }, + updateShowMarkers: function(a) { + var d = this.getSprites(), + b = d && d[0], + c = b && b.getMarker("markers"); + if (c) { + c.getTemplate().setAttributes({ + hidden: !a + }) + } + }, + createSprite: function() { + var f = this, + a = f.getSurface(), + e = f.getItemInstancing(), + d = a.add(f.getDefaultSpriteConfig()), + b = f.getMarker(), + g, c; + d.setAttributes(f.getStyle()); + d.setSeries(f); + if (e) { + d.itemsMarker = f.createItemInstancingSprite(d, e) + } + if (d.bindMarker) { + if (b) { + g = new Ext.chart.Markers(); + c = Ext.Object.merge({}, b); + if (f.getHighlight()) { + c.highlight = f.getHighlight(); + c.modifiers = ["highlight"] + } + g.setTemplate(c); + g.getTemplate().fx.setCustomDurations({ + translationX: 0, + translationY: 0 + }); + d.dataMarker = g; + d.bindMarker("markers", g); + f.getOverlaySurface().add(g) + } + if (f.getLabel().getTemplate().getField()) { + d.bindMarker("labels", f.getLabel()) + } + } + if (d.setStore) { + d.setStore(f.getStore()) + } + d.fx.on("animationstart", "onSpriteAnimationStart", f); + d.fx.on("animationend", "onSpriteAnimationEnd", f); + f.sprites.push(d); + return d + }, + getSprites: Ext.emptyFn, + onDataChanged: function() { + var d = this, + c = d.getChart(), + b = c && c.getStore(), + a = d.getStore(); + if (a !== b) { + d.processData() + } + }, + isXType: function(a) { + return a === "series" + }, + getItemId: function() { + return this.getId() + }, + applyThemeStyle: function(e, a) { + var b = this, + d, c; + d = e && e.subStyle && e.subStyle.fillStyle; + c = d && e.subStyle.strokeStyle; + if (d && !c) { + e.subStyle.strokeStyle = b.getStrokeColorsFromFillColors(d) + } + d = e && e.markerSubStyle && e.markerSubStyle.fillStyle; + c = d && e.markerSubStyle.strokeStyle; + if (d && !c) { + e.markerSubStyle.strokeStyle = b.getStrokeColorsFromFillColors(d) + } + return Ext.apply(a || {}, e) + }, + applyStyle: function(c, b) { + var a = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias("sprite." + this.seriesType)); + if (a && a.def) { + c = a.def.normalize(c) + } + return Ext.apply({}, c, b) + }, + applySubStyle: function(b, c) { + var a = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias("sprite." + this.seriesType)); + if (a && a.def) { + b = a.def.batchedNormalize(b, true) + } + return Ext.merge({}, c, b) + }, + applyMarker: function(c, a) { + var d = (c && c.type) || (a && a.type) || "circle", + b = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias("sprite." + d)); + if (b && b.def) { + c = b.def.normalize(Ext.isObject(c) ? c : {}, true); + c.type = d + } + return Ext.merge(a || {}, c) + }, + applyMarkerSubStyle: function(c, a) { + var d = (c && c.type) || (a && a.type) || "circle", + b = Ext.ClassManager.get(Ext.ClassManager.getNameByAlias("sprite." + d)); + if (b && b.def) { + c = b.def.batchedNormalize(c, true) + } + return Ext.merge(a || {}, c) + }, + updateHidden: function(b) { + var a = this; + a.getColors(); + a.getSubStyle(); + a.setSubStyle({ + hidden: b + }); + a.processData(); + a.doUpdateStyles(); + if (!Ext.isArray(b)) { + a.updateLegendStore(b) + } + }, + updateLegendStore: function(f, b) { + var e = this, + d = e.getChart(), + c = d.getLegendStore(), + g = e.getId(), + a; + if (c) { + if (arguments.length > 1) { + a = c.findBy(function(h) { + return h.get("series") === g && h.get("index") === b + }); + if (a !== -1) { + a = c.getAt(a) + } + } else { + a = c.findRecord("series", g) + } + if (a && a.get("disabled") !== f) { + a.set("disabled", f) + } + } + }, + setHiddenByIndex: function(a, c) { + var b = this; + if (Ext.isArray(b.getHidden())) { + b.getHidden()[a] = c; + b.updateHidden(b.getHidden()); + b.updateLegendStore(c, a) + } else { + b.setHidden(c) + } + }, + getStrokeColorsFromFillColors: function(a) { + var c = this, + e = c.getUseDarkerStrokeColor(), + b = (Ext.isNumber(e) ? e : c.darkerStrokeRatio), + d; + if (e) { + d = Ext.Array.map(a, function(f) { + f = Ext.isString(f) ? f : f.stops[0].color; + f = Ext.draw.Color.fromString(f); + return f.createDarker(b).toString() + }) + } else { + d = Ext.Array.clone(a) + } + return d + }, + updateThemeColors: function(b) { + var c = this, + d = c.getThemeStyle(), + a = Ext.Array.clone(b), + f = c.getStrokeColorsFromFillColors(b), + e = { + fillStyle: a, + strokeStyle: f + }; + d.subStyle = Ext.apply(d.subStyle || {}, e); + d.markerSubStyle = Ext.apply(d.markerSubStyle || {}, e); + c.doUpdateStyles() + }, + themeOnlyIfConfigured: {}, + updateTheme: function(d) { + var h = this, + a = d.getSeries(), + n = h.getInitialConfig(), + c = h.defaultConfig, + f = h.getConfigurator().configs, + j = a.defaults, + k = a[h.type], + g = h.themeOnlyIfConfigured, + l, i, o, b, m, e; + a = Ext.merge({}, j, k); + for (l in a) { + i = a[l]; + e = f[l]; + if (i !== null && i !== undefined && e) { + m = n[l]; + o = Ext.isObject(i); + b = m === c[l]; + if (o) { + if (b && g[l]) { + continue + } + i = Ext.merge({}, i, m) + } + if (b || o) { + h[e.names.set](i) + } + } + } + }, + updateChartColors: function(a) { + var b = this; + if (!b.getColors()) { + b.updateThemeColors(a) + } + }, + updateColors: function(a) { + this.updateThemeColors(a) + }, + updateStyle: function() { + this.doUpdateStyles() + }, + updateSubStyle: function() { + this.doUpdateStyles() + }, + updateThemeStyle: function() { + this.doUpdateStyles() + }, + doUpdateStyles: function() { + var g = this, + h = g.sprites, + d = g.getItemInstancing(), + c = 0, + f = h && h.length, + a = g.getConfig("showMarkers", true), + b = g.getMarker(), + e; + for (; c < f; c++) { + e = g.getStyleByIndex(c); + if (d) { + h[c].itemsMarker.getTemplate().setAttributes(e) + } + h[c].setAttributes(e); + if (b && h[c].dataMarker) { + h[c].dataMarker.getTemplate().setAttributes(g.getMarkerStyleByIndex(c)) + } + } + }, + getStyleWithTheme: function() { + var b = this, + c = b.getThemeStyle(), + d = (c && c.style) || {}, + a = Ext.applyIf(Ext.apply({}, b.getStyle()), d); + return a + }, + getSubStyleWithTheme: function() { + var c = this, + d = c.getThemeStyle(), + a = (d && d.subStyle) || {}, + b = Ext.applyIf(Ext.apply({}, c.getSubStyle()), a); + return b + }, + getStyleByIndex: function(b) { + var e = this, + h = e.getThemeStyle(), + d, g, c, f, a = {}; + d = e.getStyle(); + g = (h && h.style) || {}; + c = e.styleDataForIndex(e.getSubStyle(), b); + f = e.styleDataForIndex((h && h.subStyle), b); + Ext.apply(a, g); + Ext.apply(a, f); + Ext.apply(a, d); + Ext.apply(a, c); + return a + }, + getMarkerStyleByIndex: function(d) { + var g = this, + c = g.getThemeStyle(), + a, e, k, j, b, l, h, f, m = {}; + a = g.getStyle(); + e = (c && c.style) || {}; + k = g.styleDataForIndex(g.getSubStyle(), d); + if (k.hasOwnProperty("hidden")) { + k.hidden = k.hidden || !this.getConfig("showMarkers", true) + } + j = g.styleDataForIndex((c && c.subStyle), d); + b = g.getMarker(); + l = (c && c.marker) || {}; + h = g.getMarkerSubStyle(); + f = g.styleDataForIndex((c && c.markerSubStyle), d); + Ext.apply(m, e); + Ext.apply(m, j); + Ext.apply(m, l); + Ext.apply(m, f); + Ext.apply(m, a); + Ext.apply(m, k); + Ext.apply(m, b); + Ext.apply(m, h); + return m + }, + styleDataForIndex: function(d, c) { + var e, b, a = {}; + if (d) { + for (b in d) { + e = d[b]; + if (Ext.isArray(e)) { + a[b] = e[c % e.length] + } else { + a[b] = e + } + } + } + return a + }, + getItemForPoint: Ext.emptyFn, + getItemByIndex: function(a, e) { + var d = this, + f = d.getSprites(), + b = f && f[0], + c; + if (!b) { + return + } + if (e === undefined && b.isMarkerHolder) { + e = d.getItemInstancing() ? "items" : "markers" + } else { + if (!e || e === "" || e === "sprites") { + b = f[a] + } + } + if (b) { + c = { + series: d, + category: e, + index: a, + record: d.getStore().getData().items[a], + field: d.getYField(), + sprite: b + }; + return c + } + }, + onSpriteAnimationStart: function(a) { + this.fireEvent("animationstart", this, a) + }, + onSpriteAnimationEnd: function(a) { + this.fireEvent("animationend", this, a) + }, + resolveListenerScope: function(e) { + var d = this, + a = Ext._namedScopes[e], + c = d.getChart(), + b; + if (!a) { + b = c ? c.resolveListenerScope(e, false) : (e || d) + } else { + if (a.isThis) { + b = d + } else { + if (a.isController) { + b = c ? c.resolveListenerScope(e, false) : d + } else { + if (a.isSelf) { + b = c ? c.resolveListenerScope(e, false) : d; + if (b === c && !c.getInheritedConfig("defaultListenerScope")) { + b = d + } + } + } + } + } + return b + }, + provideLegendInfo: function(a) { + a.push({ + name: this.getTitle() || this.getId(), + mark: "black", + disabled: this.getHidden(), + series: this.getId(), + index: 0 + }) + }, + clearSprites: function() { + var d = this.sprites, + b, a, c; + for (a = 0, c = d.length; a < c; a++) { + b = d[a]; + if (b && b.isSprite) { + b.destroy() + } + } + this.sprites = [] + }, + destroy: function() { + var b = this, + a = b._store, + c = b.getConfig("tooltip", true); + if (a && a.getAutoDestroy()) { + Ext.destroy(a) + } + b.setChart(null); + b.clearListeners(); + if (c) { + Ext.destroy(c); + clearTimeout(b.tooltipTimeout) + } + b.callParent() + } +}); +Ext.define("Ext.chart.interactions.Abstract", { + xtype: "interaction", + mixins: { + observable: "Ext.mixin.Observable" + }, + config: { + gestures: { + tap: "onGesture" + }, + chart: null, + enabled: true + }, + throttleGap: 0, + stopAnimationBeforeSync: false, + constructor: function(a) { + var b = this, + c; + a = a || {}; + if ("id" in a) { + c = a.id + } else { + if ("id" in b.config) { + c = b.config.id + } else { + c = b.getId() + } + } + b.setId(c); + b.mixins.observable.constructor.call(b, a) + }, + initialize: Ext.emptyFn, + updateChart: function(c, a) { + var b = this; + if (a === c) { + return + } + if (a) { + a.unregister(b); + b.removeChartListener(a) + } + if (c) { + c.register(b); + b.addChartListener() + } + }, + updateEnabled: function(a) { + var c = this, + b = c.getChart(); + if (b) { + if (a) { + c.addChartListener() + } else { + c.removeChartListener(b) + } + } + }, + onGesture: Ext.emptyFn, + getItemForEvent: function(d) { + var b = this, + a = b.getChart(), + c = a.getEventXY(d); + return a.getItemForPoint(c[0], c[1]) + }, + getItemsForEvent: function(d) { + var b = this, + a = b.getChart(), + c = a.getEventXY(d); + return a.getItemsForPoint(c[0], c[1]) + }, + addChartListener: function() { + var c = this, + b = c.getChart(), + e = c.getGestures(), + a; + if (!c.getEnabled()) { + return + } + + function d(f, g) { + b.addElementListener(f, c.listeners[f] = function(j) { + var i = c.getLocks(), + h; + if (c.getEnabled() && (!(f in i) || i[f] === c)) { + h = (Ext.isFunction(g) ? g : c[g]).apply(this, arguments); + if (h === false && j && j.stopPropagation) { + j.stopPropagation() + } + return h + } + }, c) + } + c.listeners = c.listeners || {}; + for (a in e) { + d(a, e[a]) + } + }, + removeChartListener: function(c) { + var d = this, + e = d.getGestures(), + b; + + function a(f) { + var g = d.listeners[f]; + if (g) { + c.removeElementListener(f, g); + delete d.listeners[f] + } + } + if (d.listeners) { + for (b in e) { + a(b) + } + } + }, + lockEvents: function() { + var d = this, + c = d.getLocks(), + a = Array.prototype.slice.call(arguments), + b = a.length; + while (b--) { + c[a[b]] = d + } + }, + unlockEvents: function() { + var c = this.getLocks(), + a = Array.prototype.slice.call(arguments), + b = a.length; + while (b--) { + delete c[a[b]] + } + }, + getLocks: function() { + var a = this.getChart(); + return a.lockedEvents || (a.lockedEvents = {}) + }, + isMultiTouch: function() { + if (Ext.browser.is.IE10) { + return true + } + return !Ext.os.is.Desktop + }, + initializeDefaults: Ext.emptyFn, + doSync: function() { + var b = this, + a = b.getChart(); + if (b.syncTimer) { + clearTimeout(b.syncTimer); + b.syncTimer = null + } + if (b.stopAnimationBeforeSync) { + a.animationSuspendCount++ + } + a.redraw(); + if (b.stopAnimationBeforeSync) { + a.animationSuspendCount-- + } + b.syncThrottle = Date.now() + b.throttleGap + }, + sync: function() { + var a = this; + if (a.throttleGap && Ext.frameStartTime < a.syncThrottle) { + if (a.syncTimer) { + return + } + a.syncTimer = Ext.defer(function() { + a.doSync() + }, a.throttleGap) + } else { + a.doSync() + } + }, + getItemId: function() { + return this.getId() + }, + isXType: function(a) { + return a === "interaction" + }, + destroy: function() { + var a = this; + a.setChart(null); + delete a.listeners; + a.callParent() + } +}, function() { + if (Ext.os.is.Android4) { + this.prototype.throttleGap = 40 + } +}); +Ext.define("Ext.chart.MarkerHolder", { + extend: "Ext.Mixin", + mixinConfig: { + id: "markerHolder", + after: { + constructor: "constructor", + preRender: "preRender" + }, + before: { + destroy: "destroy" + } + }, + isMarkerHolder: true, + surfaceMatrix: null, + inverseSurfaceMatrix: null, + deprecated: { + 6: { + methods: { + getBoundMarker: { + message: "Please use the 'getMarker' method instead.", + fn: function(b) { + var a = this.boundMarkers[b]; + return a ? [a] : a + } + } + } + } + }, + constructor: function() { + this.boundMarkers = {}; + this.cleanRedraw = false + }, + bindMarker: function(b, a) { + var c = this, + d = c.boundMarkers; + if (a && a.isMarkers) { + c.releaseMarker(b); + d[b] = a; + a.on("destroy", c.onMarkerDestroy, c) + } + }, + onMarkerDestroy: function(a) { + this.releaseMarker(a) + }, + releaseMarker: function(a) { + var c = this.boundMarkers, + b; + if (a && a.isMarkers) { + for (b in c) { + if (c[b] === a) { + delete c[b]; + break + } + } + } else { + b = a; + a = c[b]; + delete c[b] + } + return a || null + }, + getMarker: function(a) { + return this.boundMarkers[a] || null + }, + preRender: function() { + var f = this, + g = f.getId(), + d = f.boundMarkers, + e = f.getParent(), + c, a, b; + if (f.surfaceMatrix) { + b = f.surfaceMatrix.set(1, 0, 0, 1, 0, 0) + } else { + b = f.surfaceMatrix = new Ext.draw.Matrix() + } + f.cleanRedraw = !f.attr.dirty; + if (!f.cleanRedraw) { + for (c in d) { + a = d[c]; + if (a) { + a.clear(g) + } + } + } + while (e && e.attr && e.attr.matrix) { + b.prependMatrix(e.attr.matrix); + e = e.getParent() + } + b.prependMatrix(e.matrix); + f.surfaceMatrix = b; + f.inverseSurfaceMatrix = b.inverse(f.inverseSurfaceMatrix) + }, + putMarker: function(d, a, c, g, e) { + var b = this.boundMarkers[d], + f = this.getId(); + if (b) { + b.putMarkerFor(f, a, c, g, e) + } + }, + getMarkerBBox: function(c, b, d) { + var a = this.boundMarkers[c], + e = this.getId(); + if (a) { + return a.getMarkerBBoxFor(e, b, d) + } + }, + destroy: function() { + var c = this.boundMarkers, + b, a; + for (b in c) { + a = c[b]; + a.destroy() + } + } +}); +Ext.define("Ext.chart.axis.sprite.Axis", { + extend: "Ext.draw.sprite.Sprite", + alias: "sprite.axis", + type: "axis", + mixins: { + markerHolder: "Ext.chart.MarkerHolder" + }, + requires: ["Ext.draw.sprite.Text"], + inheritableStatics: { + def: { + processors: { + grid: "bool", + axisLine: "bool", + minorTicks: "bool", + minorTickSize: "number", + majorTicks: "bool", + majorTickSize: "number", + length: "number", + startGap: "number", + endGap: "number", + dataMin: "number", + dataMax: "number", + visibleMin: "number", + visibleMax: "number", + position: "enums(left,right,top,bottom,angular,radial,gauge)", + minStepSize: "number", + estStepSize: "number", + titleOffset: "number", + textPadding: "number", + min: "number", + max: "number", + centerX: "number", + centerY: "number", + radius: "number", + totalAngle: "number", + baseRotation: "number", + data: "default", + enlargeEstStepSizeByText: "bool" + }, + defaults: { + grid: false, + axisLine: true, + minorTicks: false, + minorTickSize: 3, + majorTicks: true, + majorTickSize: 5, + length: 0, + startGap: 0, + endGap: 0, + visibleMin: 0, + visibleMax: 1, + dataMin: 0, + dataMax: 1, + position: "", + minStepSize: 0, + estStepSize: 20, + min: 0, + max: 1, + centerX: 0, + centerY: 0, + radius: 1, + baseRotation: 0, + data: null, + titleOffset: 0, + textPadding: 0, + scalingCenterY: 0, + scalingCenterX: 0, + strokeStyle: "black", + enlargeEstStepSizeByText: false + }, + triggers: { + minorTickSize: "bbox", + majorTickSize: "bbox", + position: "bbox,layout", + axisLine: "bbox,layout", + min: "layout", + max: "layout", + length: "layout", + minStepSize: "layout", + estStepSize: "layout", + data: "layout", + dataMin: "layout", + dataMax: "layout", + visibleMin: "layout", + visibleMax: "layout", + enlargeEstStepSizeByText: "layout" + }, + updaters: { + layout: "layoutUpdater" + } + } + }, + config: { + label: null, + layout: null, + segmenter: null, + renderer: null, + layoutContext: null, + axis: null + }, + thickness: 0, + stepSize: 0, + getBBox: function() { + return null + }, + defaultRenderer: function(a) { + return this.segmenter.renderer(a, this) + }, + layoutUpdater: function() { + var h = this, + f = h.getAxis().getChart(); + if (f.isInitializing) { + return + } + var e = h.attr, + d = h.getLayout(), + g = f.getInherited().rtl, + b = e.dataMin + (e.dataMax - e.dataMin) * e.visibleMin, + i = e.dataMin + (e.dataMax - e.dataMin) * e.visibleMax, + c = e.position, + a = { + attr: e, + segmenter: h.getSegmenter(), + renderer: h.defaultRenderer + }; + if (c === "left" || c === "right") { + e.translationX = 0; + e.translationY = i * e.length / (i - b); + e.scalingX = 1; + e.scalingY = -e.length / (i - b); + e.scalingCenterY = 0; + e.scalingCenterX = 0; + h.applyTransformations(true) + } else { + if (c === "top" || c === "bottom") { + if (g) { + e.translationX = e.length + b * e.length / (i - b) + 1 + } else { + e.translationX = -b * e.length / (i - b) + } + e.translationY = 0; + e.scalingX = (g ? -1 : 1) * e.length / (i - b); + e.scalingY = 1; + e.scalingCenterY = 0; + e.scalingCenterX = 0; + h.applyTransformations(true) + } + } + if (d) { + d.calculateLayout(a); + h.setLayoutContext(a) + } + }, + iterate: function(e, j) { + var c, g, a, b, h, d, k = Ext.Array.some, + m = Math.abs, + f; + if (e.getLabel) { + if (e.min < e.from) { + j.call(this, e.min, e.getLabel(e.min), -1, e) + } + for (c = 0; c <= e.steps; c++) { + j.call(this, e.get(c), e.getLabel(c), c, e) + } + if (e.max > e.to) { + j.call(this, e.max, e.getLabel(e.max), e.steps + 1, e) + } + } else { + b = this.getAxis(); + h = b.floatingAxes; + d = []; + f = (e.to - e.from) / (e.steps + 1); + if (b.getFloating()) { + for (a in h) { + d.push(h[a]) + } + } + + function l(i) { + return !d.length || k(d, function(n) { + return m(n - i) > f + }) + } + if (e.min < e.from && l(e.min)) { + j.call(this, e.min, e.min, -1, e) + } + for (c = 0; c <= e.steps; c++) { + g = e.get(c); + if (l(g)) { + j.call(this, g, g, c, e) + } + } + if (e.max > e.to && l(e.max)) { + j.call(this, e.max, e.max, e.steps + 1, e) + } + } + }, + renderTicks: function(l, m, s, p) { + var v = this, + k = v.attr, + u = k.position, + n = k.matrix, + e = 0.5 * k.lineWidth, + f = n.getXX(), + i = n.getDX(), + j = n.getYY(), + h = n.getDY(), + o = s.majorTicks, + d = k.majorTickSize, + a = s.minorTicks, + r = k.minorTickSize; + if (o) { + switch (u) { + case "right": + function q(w) { + return function(x, z, y) { + x = l.roundPixel(x * j + h) + e; + m.moveTo(0, x); + m.lineTo(w, x) + } + } + v.iterate(o, q(d)); + a && v.iterate(a, q(r)); + break; + case "left": + function t(w) { + return function(x, z, y) { + x = l.roundPixel(x * j + h) + e; + m.moveTo(p[2] - w, x); + m.lineTo(p[2], x) + } + } + v.iterate(o, t(d)); + a && v.iterate(a, t(r)); + break; + case "bottom": + function c(w) { + return function(x, z, y) { + x = l.roundPixel(x * f + i) - e; + m.moveTo(x, 0); + m.lineTo(x, w) + } + } + v.iterate(o, c(d)); + a && v.iterate(a, c(r)); + break; + case "top": + function b(w) { + return function(x, z, y) { + x = l.roundPixel(x * f + i) - e; + m.moveTo(x, p[3]); + m.lineTo(x, p[3] - w) + } + } + v.iterate(o, b(d)); + a && v.iterate(a, b(r)); + break; + case "angular": + v.iterate(o, function(w, y, x) { + w = w / (k.max + 1) * Math.PI * 2 + k.baseRotation; + m.moveTo(k.centerX + (k.length) * Math.cos(w), k.centerY + (k.length) * Math.sin(w)); + m.lineTo(k.centerX + (k.length + d) * Math.cos(w), k.centerY + (k.length + d) * Math.sin(w)) + }); + break; + case "gauge": + var g = v.getGaugeAngles(); + v.iterate(o, function(w, y, x) { + w = (w - k.min) / (k.max - k.min + 1) * k.totalAngle - k.totalAngle + g.start; + m.moveTo(k.centerX + (k.length) * Math.cos(w), k.centerY + (k.length) * Math.sin(w)); + m.lineTo(k.centerX + (k.length + d) * Math.cos(w), k.centerY + (k.length + d) * Math.sin(w)) + }); + break + } + } + }, + renderLabels: function(E, q, D, K) { + var o = this, + k = o.attr, + i = 0.5 * k.lineWidth, + u = k.position, + y = k.matrix, + A = k.textPadding, + x = y.getXX(), + d = y.getDX(), + g = y.getYY(), + c = y.getDY(), + n = 0, + I = D.majorTicks, + G = Math.max(k.majorTickSize, k.minorTickSize) + k.lineWidth, + f = Ext.draw.Draw.isBBoxIntersect, + F = o.getLabel(), + J, s, r = null, + w = 0, + b = 0, + m = D.segmenter, + B = o.getRenderer(), + t = o.getAxis(), + z = t.getTitle(), + a = z && z.attr.text !== "" && z.getBBox(), + l, h = null, + p, C, v, e, H; + if (I && F && !F.attr.hidden) { + J = F.attr.font; + if (q.font !== J) { + q.font = J + } + F.setAttributes({ + translationX: 0, + translationY: 0 + }, true); + F.applyTransformations(); + l = F.attr.inverseMatrix.elements.slice(0); + switch (u) { + case "left": + e = a ? a.x + a.width : 0; + switch (F.attr.textAlign) { + case "start": + H = E.roundPixel(e + d) - i; + break; + case "end": + H = E.roundPixel(K[2] - G + d) - i; + break; + default: + H = E.roundPixel(e + (K[2] - e - G) / 2 + d) - i + } + F.setAttributes({ + translationX: H + }, true); + break; + case "right": + e = a ? K[2] - a.x : 0; + switch (F.attr.textAlign) { + case "start": + H = E.roundPixel(G + d) + i; + break; + case "end": + H = E.roundPixel(K[2] - e + d) + i; + break; + default: + H = E.roundPixel(G + (K[2] - G - e) / 2 + d) + i + } + F.setAttributes({ + translationX: H + }, true); + break; + case "top": + e = a ? a.y + a.height : 0; + F.setAttributes({ + translationY: E.roundPixel(e + (K[3] - e - G) / 2) - i + }, true); + break; + case "bottom": + e = a ? K[3] - a.y : 0; + F.setAttributes({ + translationY: E.roundPixel(G + (K[3] - G - e) / 2) + i + }, true); + break; + case "radial": + F.setAttributes({ + translationX: k.centerX + }, true); + break; + case "angular": + F.setAttributes({ + translationY: k.centerY + }, true); + break; + case "gauge": + F.setAttributes({ + translationY: k.centerY + }, true); + break + } + if (u === "left" || u === "right") { + o.iterate(I, function(L, N, M) { + if (N === undefined) { + return + } + if (B) { + v = Ext.callback(B, null, [t, N, D, r], 0, t) + } else { + v = m.renderer(N, D, r) + } + r = N; + F.setAttributes({ + text: String(v), + translationY: E.roundPixel(L * g + c) + }, true); + F.applyTransformations(); + n = Math.max(n, F.getBBox().width + G); + if (n <= o.thickness) { + C = Ext.draw.Matrix.fly(F.attr.matrix.elements.slice(0)); + p = C.prepend.apply(C, l).transformBBox(F.getBBox(true)); + if (h && !f(p, h, A)) { + return + } + E.renderSprite(F); + h = p; + w += p.height; + b++ + } + }) + } else { + if (u === "top" || u === "bottom") { + o.iterate(I, function(L, N, M) { + if (N === undefined) { + return + } + if (B) { + v = Ext.callback(B, null, [t, N, D, r], 0, t) + } else { + v = m.renderer(N, D, r) + } + r = N; + F.setAttributes({ + text: String(v), + translationX: E.roundPixel(L * x + d) + }, true); + F.applyTransformations(); + n = Math.max(n, F.getBBox().height + G); + if (n <= o.thickness) { + C = Ext.draw.Matrix.fly(F.attr.matrix.elements.slice(0)); + p = C.prepend.apply(C, l).transformBBox(F.getBBox(true)); + if (h && !f(p, h, A)) { + return + } + E.renderSprite(F); + h = p; + w += p.width; + b++ + } + }) + } else { + if (u === "radial") { + o.iterate(I, function(L, N, M) { + if (N === undefined) { + return + } + if (B) { + v = Ext.callback(B, null, [t, N, D, r], 0, t) + } else { + v = m.renderer(N, D, r) + } + r = N; + if (typeof v !== "undefined") { + F.setAttributes({ + text: String(v), + translationX: k.centerX - E.roundPixel(L) / k.max * k.length * Math.cos(k.baseRotation + Math.PI / 2), + translationY: k.centerY - E.roundPixel(L) / k.max * k.length * Math.sin(k.baseRotation + Math.PI / 2) + }, true); + F.applyTransformations(); + p = F.attr.matrix.transformBBox(F.getBBox(true)); + if (h && !f(p, h)) { + return + } + E.renderSprite(F); + h = p; + w += p.width; + b++ + } + }) + } else { + if (u === "angular") { + s = k.majorTickSize + k.lineWidth * 0.5 + (parseInt(F.attr.fontSize, 10) || 10) / 2; + o.iterate(I, function(L, N, M) { + if (N === undefined) { + return + } + if (B) { + v = Ext.callback(B, null, [t, N, D, r], 0, t) + } else { + v = m.renderer(N, D, r) + } + r = N; + n = Math.max(n, Math.max(k.majorTickSize, k.minorTickSize) + (k.lineCap !== "butt" ? k.lineWidth * 0.5 : 0)); + if (typeof v !== "undefined") { + var O = L / (k.max + 1) * Math.PI * 2 + k.baseRotation; + F.setAttributes({ + text: String(v), + translationX: k.centerX + (k.length + s) * Math.cos(O), + translationY: k.centerY + (k.length + s) * Math.sin(O) + }, true); + F.applyTransformations(); + p = F.attr.matrix.transformBBox(F.getBBox(true)); + if (h && !f(p, h)) { + return + } + E.renderSprite(F); + h = p; + w += p.width; + b++ + } + }) + } else { + if (u === "gauge") { + var j = o.getGaugeAngles(); + o.iterate(I, function(L, N, M) { + if (N === undefined) { + return + } + if (B) { + v = Ext.callback(B, null, [t, N, D, r], 0, t) + } else { + v = m.renderer(N, D, r) + } + r = N; + if (typeof v !== "undefined") { + var O = (L - k.min) / (k.max - k.min + 1) * k.totalAngle - k.totalAngle + j.start; + F.setAttributes({ + text: String(v), + translationX: k.centerX + (k.length + 10) * Math.cos(O), + translationY: k.centerY + (k.length + 10) * Math.sin(O) + }, true); + F.applyTransformations(); + p = F.attr.matrix.transformBBox(F.getBBox(true)); + if (h && !f(p, h)) { + return + } + E.renderSprite(F); + h = p; + w += p.width; + b++ + } + }) + } + } + } + } + } + if (k.enlargeEstStepSizeByText && b) { + w /= b; + w += G; + w *= 2; + if (k.estStepSize < w) { + k.estStepSize = w + } + } + if (Math.abs(o.thickness - (n)) > 1) { + o.thickness = n; + k.bbox.plain.dirty = true; + k.bbox.transform.dirty = true; + o.doThicknessChanged(); + return false + } + } + }, + renderAxisLine: function(a, i, e, c) { + var h = this, + g = h.attr, + b = g.lineWidth * 0.5, + j = g.position, + d, f; + if (g.axisLine && g.length) { + switch (j) { + case "left": + d = a.roundPixel(c[2]) - b; + i.moveTo(d, -g.endGap); + i.lineTo(d, g.length + g.startGap + 1); + break; + case "right": + i.moveTo(b, -g.endGap); + i.lineTo(b, g.length + g.startGap + 1); + break; + case "bottom": + i.moveTo(-g.startGap, b); + i.lineTo(g.length + g.endGap, b); + break; + case "top": + d = a.roundPixel(c[3]) - b; + i.moveTo(-g.startGap, d); + i.lineTo(g.length + g.endGap, d); + break; + case "angular": + i.moveTo(g.centerX + g.length, g.centerY); + i.arc(g.centerX, g.centerY, g.length, 0, Math.PI * 2, true); + break; + case "gauge": + f = h.getGaugeAngles(); + i.moveTo(g.centerX + Math.cos(f.start) * g.length, g.centerY + Math.sin(f.start) * g.length); + i.arc(g.centerX, g.centerY, g.length, f.start, f.end, true); + break + } + } + }, + getGaugeAngles: function() { + var a = this, + c = a.attr.totalAngle, + b; + if (c <= Math.PI) { + b = (Math.PI - c) * 0.5 + } else { + b = -(Math.PI * 2 - c) * 0.5 + } + b = Math.PI * 2 - b; + return { + start: b, + end: b - c + } + }, + renderGridLines: function(m, n, s, r) { + var t = this, + b = t.getAxis(), + l = t.attr, + p = l.matrix, + d = l.startGap, + a = l.endGap, + c = p.getXX(), + k = p.getYY(), + h = p.getDX(), + g = p.getDY(), + u = l.position, + f = b.getGridAlignment(), + q = s.majorTicks, + e, o, i; + if (l.grid) { + if (q) { + if (u === "left" || u === "right") { + i = l.min * k + g + a + d; + t.iterate(q, function(j, w, v) { + e = j * k + g + a; + t.putMarker(f + "-" + (v % 2 ? "odd" : "even"), { + y: e, + height: i - e + }, o = v, true); + i = e + }); + o++; + e = 0; + t.putMarker(f + "-" + (o % 2 ? "odd" : "even"), { + y: e, + height: i - e + }, o, true) + } else { + if (u === "top" || u === "bottom") { + i = l.min * c + h + d; + if (d) { + t.putMarker(f + "-even", { + x: 0, + width: i + }, -1, true) + } + t.iterate(q, function(j, w, v) { + e = j * c + h + d; + t.putMarker(f + "-" + (v % 2 ? "odd" : "even"), { + x: e, + width: i - e + }, o = v, true); + i = e + }); + o++; + e = l.length + l.startGap + l.endGap; + t.putMarker(f + "-" + (o % 2 ? "odd" : "even"), { + x: e, + width: i - e + }, o, true) + } else { + if (u === "radial") { + t.iterate(q, function(j, w, v) { + if (!j) { + return + } + e = j / l.max * l.length; + t.putMarker(f + "-" + (v % 2 ? "odd" : "even"), { + scalingX: e, + scalingY: e + }, v, true); + i = e + }) + } else { + if (u === "angular") { + t.iterate(q, function(j, w, v) { + if (!l.length) { + return + } + e = j / (l.max + 1) * Math.PI * 2 + l.baseRotation; + t.putMarker(f + "-" + (v % 2 ? "odd" : "even"), { + rotationRads: e, + rotationCenterX: 0, + rotationCenterY: 0, + scalingX: l.length, + scalingY: l.length + }, v, true); + i = e + }) + } + } + } + } + } + } + }, + renderLimits: function(o) { + var t = this, + a = t.getAxis(), + h = a.getChart(), + p = h.getInnerPadding(), + d = Ext.Array.from(a.getLimits()); + if (!d.length) { + return + } + var r = a.limits.surface.getRect(), + m = t.attr, + n = m.matrix, + u = m.position, + k = Ext.Object.chain, + v = a.limits.titles, + c, j, b, s, l, q, f, g, e; + v.instances = []; + v.position = 0; + if (u === "left" || u === "right") { + for (q = 0, f = d.length; q < f; q++) { + s = k(d[q]); + !s.line && (s.line = {}); + l = Ext.isString(s.value) ? a.getCoordFor(s.value) : s.value; + l = l * n.getYY() + n.getDY(); + s.line.y = l + p.top; + s.line.strokeStyle = s.line.strokeStyle || m.strokeStyle; + t.putMarker("horizontal-limit-lines", s.line, q, true); + if (s.line.title) { + v.createInstance(s.line.title); + c = v.getBBoxFor(v.position - 1); + j = s.line.title.position || (u === "left" ? "start" : "end"); + switch (j) { + case "start": + g = 10; + break; + case "end": + g = r[2] - 10; + break; + case "middle": + g = r[2] / 2; + break + } + v.setAttributesFor(v.position - 1, { + x: g, + y: s.line.y - c.height / 2, + textAlign: j, + fillStyle: s.line.title.fillStyle || s.line.strokeStyle + }) + } + } + } else { + if (u === "top" || u === "bottom") { + for (q = 0, f = d.length; q < f; q++) { + s = k(d[q]); + !s.line && (s.line = {}); + l = Ext.isString(s.value) ? a.getCoordFor(s.value) : s.value; + l = l * n.getXX() + n.getDX(); + s.line.x = l + p.left; + s.line.strokeStyle = s.line.strokeStyle || m.strokeStyle; + t.putMarker("vertical-limit-lines", s.line, q, true); + if (s.line.title) { + v.createInstance(s.line.title); + c = v.getBBoxFor(v.position - 1); + j = s.line.title.position || (u === "top" ? "end" : "start"); + switch (j) { + case "start": + e = r[3] - c.width / 2 - 10; + break; + case "end": + e = c.width / 2 + 10; + break; + case "middle": + e = r[3] / 2; + break + } + v.setAttributesFor(v.position - 1, { + x: s.line.x + c.height / 2, + y: e, + fillStyle: s.line.title.fillStyle || s.line.strokeStyle, + rotationRads: Math.PI / 2 + }) + } + } + } else { + if (u === "radial") { + for (q = 0, f = d.length; q < f; q++) { + s = k(d[q]); + !s.line && (s.line = {}); + l = Ext.isString(s.value) ? a.getCoordFor(s.value) : s.value; + if (l > m.max) { + continue + } + l = l / m.max * m.length; + s.line.cx = m.centerX; + s.line.cy = m.centerY; + s.line.scalingX = l; + s.line.scalingY = l; + s.line.strokeStyle = s.line.strokeStyle || m.strokeStyle; + t.putMarker("circular-limit-lines", s.line, q, true); + if (s.line.title) { + v.createInstance(s.line.title); + c = v.getBBoxFor(v.position - 1); + v.setAttributesFor(v.position - 1, { + x: m.centerX, + y: m.centerY - l - c.height / 2, + fillStyle: s.line.title.fillStyle || s.line.strokeStyle + }) + } + } + } else { + if (u === "angular") { + for (q = 0, f = d.length; q < f; q++) { + s = k(d[q]); + !s.line && (s.line = {}); + l = Ext.isString(s.value) ? a.getCoordFor(s.value) : s.value; + l = l / (m.max + 1) * Math.PI * 2 + m.baseRotation; + s.line.translationX = m.centerX; + s.line.translationY = m.centerY; + s.line.rotationRads = l; + s.line.rotationCenterX = 0; + s.line.rotationCenterY = 0; + s.line.scalingX = m.length; + s.line.scalingY = m.length; + s.line.strokeStyle = s.line.strokeStyle || m.strokeStyle; + t.putMarker("radial-limit-lines", s.line, q, true); + if (s.line.title) { + v.createInstance(s.line.title); + c = v.getBBoxFor(v.position - 1); + b = ((l > -0.5 * Math.PI && l < 0.5 * Math.PI) || (l > 1.5 * Math.PI && l < 2 * Math.PI)) ? 1 : -1; + v.setAttributesFor(v.position - 1, { + x: m.centerX + 0.5 * m.length * Math.cos(l) + b * c.height / 2 * Math.sin(l), + y: m.centerY + 0.5 * m.length * Math.sin(l) - b * c.height / 2 * Math.cos(l), + rotationRads: b === 1 ? l : l - Math.PI, + fillStyle: s.line.title.fillStyle || s.line.strokeStyle + }) + } + } + } else { + if (u === "gauge") {} + } + } + } + } + }, + doThicknessChanged: function() { + var a = this.getAxis(); + if (a) { + a.onThicknessChanged() + } + }, + render: function(a, c, d) { + var e = this, + b = e.getLayoutContext(); + if (b) { + if (false === e.renderLabels(a, c, b, d)) { + return false + } + c.beginPath(); + e.renderTicks(a, c, b, d); + e.renderAxisLine(a, c, b, d); + e.renderGridLines(a, c, b, d); + e.renderLimits(d); + c.stroke() + } + } +}); +Ext.define("Ext.chart.axis.segmenter.Segmenter", { + config: { + axis: null + }, + constructor: function(a) { + this.initConfig(a) + }, + renderer: function(b, a) { + return String(b) + }, + from: function(a) { + return a + }, + diff: Ext.emptyFn, + align: Ext.emptyFn, + add: Ext.emptyFn, + preferredStep: Ext.emptyFn +}); +Ext.define("Ext.chart.axis.segmenter.Names", { + extend: "Ext.chart.axis.segmenter.Segmenter", + alias: "segmenter.names", + renderer: function(b, a) { + return b + }, + diff: function(b, a, c) { + return Math.floor(a - b) + }, + align: function(c, b, a) { + return Math.floor(c) + }, + add: function(c, b, a) { + return c + b + }, + preferredStep: function(c, a, b, d) { + return { + unit: 1, + step: 1 + } + } +}); +Ext.define("Ext.chart.axis.segmenter.Numeric", { + extend: "Ext.chart.axis.segmenter.Segmenter", + alias: "segmenter.numeric", + isNumeric: true, + renderer: function(b, a) { + return b.toFixed(Math.max(0, a.majorTicks.unit.fixes)) + }, + diff: function(b, a, c) { + return Math.floor((a - b) / c.scale) + }, + align: function(c, b, a) { + return Math.floor(c / (a.scale * b)) * a.scale * b + }, + add: function(c, b, a) { + return c + b * a.scale + }, + preferredStep: function(c, b) { + var a = Math.floor(Math.log(b) * Math.LOG10E), + d = Math.pow(10, a); + b /= d; + if (b < 2) { + b = 2 + } else { + if (b < 5) { + b = 5 + } else { + if (b < 10) { + b = 10; + a++ + } + } + } + return { + unit: { + fixes: -a, + scale: d + }, + step: b + } + }, + exactStep: function(c, b) { + var a = Math.floor(Math.log(b) * Math.LOG10E), + d = Math.pow(10, a); + return { + unit: { + fixes: -a + (b % d === 0 ? 0 : 1), + scale: 1 + }, + step: b + } + }, + adjustByMajorUnit: function(e, g, c) { + var d = c[0], + b = c[1], + a = e * g, + f = d % a; + if (f !== 0) { + c[0] = d - f + (d < 0 ? -a : 0) + } + f = b % a; + if (f !== 0) { + c[1] = b - f + (b > 0 ? a : 0) + } + } +}); +Ext.define("Ext.chart.axis.segmenter.Time", { + extend: "Ext.chart.axis.segmenter.Segmenter", + alias: "segmenter.time", + config: { + step: null + }, + renderer: function(c, b) { + var a = Ext.Date; + switch (b.majorTicks.unit) { + case "y": + return a.format(c, "Y"); + case "mo": + return a.format(c, "Y-m"); + case "d": + return a.format(c, "Y-m-d") + } + return a.format(c, "Y-m-d\nH:i:s") + }, + from: function(a) { + return new Date(a) + }, + diff: function(b, a, c) { + if (isFinite(b)) { + b = new Date(b) + } + if (isFinite(a)) { + a = new Date(a) + } + return Ext.Date.diff(b, a, c) + }, + align: function(a, c, b) { + if (b === "d" && c >= 7) { + a = Ext.Date.align(a, "d", c); + a.setDate(a.getDate() - a.getDay() + 1); + return a + } else { + return Ext.Date.align(a, b, c) + } + }, + add: function(c, b, a) { + return Ext.Date.add(new Date(c), a, b) + }, + stepUnits: [ + [Ext.Date.YEAR, 1, 2, 5, 10, 20, 50, 100, 200, 500], + [Ext.Date.MONTH, 1, 3, 6], + [Ext.Date.DAY, 1, 7, 14], + [Ext.Date.HOUR, 1, 6, 12], + [Ext.Date.MINUTE, 1, 5, 15, 30], + [Ext.Date.SECOND, 1, 5, 15, 30], + [Ext.Date.MILLI, 1, 2, 5, 10, 20, 50, 100, 200, 500] + ], + preferredStep: function(b, e) { + if (this.getStep()) { + return this.getStep() + } + var f = new Date(+b), + g = new Date(+b + Math.ceil(e)), + d = this.stepUnits, + l, k, h, c, a; + for (c = 0; c < d.length; c++) { + k = d[c][0]; + h = this.diff(f, g, k); + if (h > 0) { + for (a = 1; a < d[c].length; a++) { + if (h <= d[c][a]) { + l = { + unit: k, + step: d[c][a] + }; + break + } + } + if (!l) { + c--; + l = { + unit: d[c][0], + step: 1 + } + } + break + } + } + if (!l) { + l = { + unit: Ext.Date.DAY, + step: 1 + } + } + return l + } +}); +Ext.define("Ext.chart.axis.layout.Layout", { + mixins: { + observable: "Ext.mixin.Observable" + }, + config: { + axis: null + }, + constructor: function(a) { + this.mixins.observable.constructor.call(this, a) + }, + processData: function(b) { + var e = this, + c = e.getAxis(), + f = c.getDirection(), + g = c.boundSeries, + a, d; + if (b) { + b["coordinate" + f]() + } else { + for (a = 0, d = g.length; a < d; a++) { + g[a]["coordinate" + f]() + } + } + }, + calculateMajorTicks: function(a) { + var f = this, + e = a.attr, + d = e.max - e.min, + i = d / Math.max(1, e.length) * (e.visibleMax - e.visibleMin), + h = e.min + d * e.visibleMin, + b = e.min + d * e.visibleMax, + g = e.estStepSize * i, + c = f.snapEnds(a, e.min, e.max, g); + if (c) { + f.trimByRange(a, c, h, b); + a.majorTicks = c + } + }, + calculateMinorTicks: function(a) { + if (this.snapMinorEnds) { + a.minorTicks = this.snapMinorEnds(a) + } + }, + calculateLayout: function(b) { + var c = this, + a = b.attr; + if (a.length === 0) { + return null + } + if (a.majorTicks) { + c.calculateMajorTicks(b); + if (a.minorTicks) { + c.calculateMinorTicks(b) + } + } + }, + snapEnds: Ext.emptyFn, + trimByRange: function(b, f, i, a) { + var g = b.segmenter, + j = f.unit, + h = g.diff(f.from, i, j), + d = g.diff(f.from, a, j), + c = Math.max(0, Math.ceil(h / f.step)), + e = Math.min(f.steps, Math.floor(d / f.step)); + if (e < f.steps) { + f.to = g.add(f.from, e * f.step, j) + } + if (f.max > a) { + f.max = f.to + } + if (f.from < i) { + f.from = g.add(f.from, c * f.step, j); + while (f.from < i) { + c++; + f.from = g.add(f.from, f.step, j) + } + } + if (f.min < i) { + f.min = f.from + } + f.steps = e - c + } +}); +Ext.define("Ext.chart.axis.layout.Discrete", { + extend: "Ext.chart.axis.layout.Layout", + alias: "axisLayout.discrete", + isDiscrete: true, + processData: function() { + var f = this, + d = f.getAxis(), + c = d.boundSeries, + g = d.getDirection(), + b, e, a; + f.labels = []; + f.labelMap = {}; + for (b = 0, e = c.length; b < e; b++) { + a = c[b]; + if (a["get" + g + "Axis"]() === d) { + a["coordinate" + g]() + } + } + d.getSprites()[0].setAttributes({ + data: f.labels + }); + f.fireEvent("datachange", f.labels) + }, + calculateLayout: function(a) { + a.data = this.labels; + this.callParent([a]) + }, + calculateMajorTicks: function(a) { + var g = this, + f = a.attr, + d = a.data, + e = f.max - f.min, + j = e / Math.max(1, f.length) * (f.visibleMax - f.visibleMin), + i = f.min + e * f.visibleMin, + b = f.min + e * f.visibleMax, + h = f.estStepSize * j; + var c = g.snapEnds(a, Math.max(0, f.min), Math.min(f.max, d.length - 1), h); + if (c) { + g.trimByRange(a, c, i, b); + a.majorTicks = c + } + }, + snapEnds: function(e, d, a, b) { + b = Math.ceil(b); + var c = Math.floor((a - d) / b), + f = e.data; + return { + min: d, + max: a, + from: d, + to: c * b + d, + step: b, + steps: c, + unit: 1, + getLabel: function(g) { + return f[this.from + this.step * g] + }, + get: function(g) { + return this.from + this.step * g + } + } + }, + trimByRange: function(b, f, h, a) { + var i = f.unit, + g = Math.ceil((h - f.from) / i) * i, + d = Math.floor((a - f.from) / i) * i, + c = Math.max(0, Math.ceil(g / f.step)), + e = Math.min(f.steps, Math.floor(d / f.step)); + if (e < f.steps) { + f.to = e + } + if (f.max > a) { + f.max = f.to + } + if (f.from < h && f.step > 0) { + f.from = f.from + c * f.step * i; + while (f.from < h) { + c++; + f.from += f.step * i + } + } + if (f.min < h) { + f.min = f.from + } + f.steps = e - c + }, + getCoordFor: function(c, d, a, b) { + this.labels.push(c); + return this.labels.length - 1 + } +}); +Ext.define("Ext.chart.axis.layout.CombineDuplicate", { + extend: "Ext.chart.axis.layout.Discrete", + alias: "axisLayout.combineDuplicate", + getCoordFor: function(d, e, b, c) { + if (!(d in this.labelMap)) { + var a = this.labelMap[d] = this.labels.length; + this.labels.push(d); + return a + } + return this.labelMap[d] + } +}); +Ext.define("Ext.chart.axis.layout.Continuous", { + extend: "Ext.chart.axis.layout.Layout", + alias: "axisLayout.continuous", + isContinuous: true, + config: { + adjustMinimumByMajorUnit: false, + adjustMaximumByMajorUnit: false + }, + getCoordFor: function(c, d, a, b) { + return +c + }, + snapEnds: function(a, d, i, h) { + var f = a.segmenter, + c = this.getAxis(), + l = c.getMajorTickSteps(), + e = l && f.exactStep ? f.exactStep(d, (i - d) / l) : f.preferredStep(d, h), + k = e.unit, + b = e.step, + j = f.align(d, b, k), + g = (l || f.diff(d, i, k)) + 1; + return { + min: f.from(d), + max: f.from(i), + from: j, + to: f.add(j, g * b, k), + step: b, + steps: g, + unit: k, + get: function(m) { + return f.add(this.from, this.step * m, k) + } + } + }, + snapMinorEnds: function(a) { + var e = a.majorTicks, + m = this.getAxis().getMinorTickSteps(), + f = a.segmenter, + d = e.min, + i = e.max, + k = e.from, + l = e.unit, + b = e.step / m, + n = b * l.scale, + j = k - d, + c = Math.floor(j / n), + h = c + Math.floor((i - e.to) / n) + 1, + g = e.steps * m + h; + return { + min: d, + max: i, + from: d + j % n, + to: f.add(k, g * b, l), + step: b, + steps: g, + unit: l, + get: function(o) { + return (o % m + c + 1 !== 0) ? f.add(this.from, this.step * o, l) : null + } + } + } +}); +Ext.define("Ext.chart.axis.Axis", { + xtype: "axis", + mixins: { + observable: "Ext.mixin.Observable" + }, + requires: ["Ext.chart.axis.sprite.Axis", "Ext.chart.axis.segmenter.*", "Ext.chart.axis.layout.*"], + isAxis: true, + config: { + position: "bottom", + fields: [], + label: undefined, + grid: false, + limits: null, + renderer: null, + chart: null, + style: null, + margin: 0, + titleMargin: 4, + background: null, + minimum: NaN, + maximum: NaN, + reconcileRange: false, + minZoom: 1, + maxZoom: 10000, + layout: "continuous", + segmenter: "numeric", + hidden: false, + majorTickSteps: 0, + minorTickSteps: 0, + adjustByMajorUnit: true, + title: null, + increment: 0.5, + length: 0, + center: null, + radius: null, + totalAngle: Math.PI, + rotation: null, + labelInSpan: null, + visibleRange: [0, 1], + needHighPrecision: false, + linkedTo: null, + floating: null + }, + titleOffset: 0, + spriteAnimationCount: 0, + prevMin: 0, + prevMax: 1, + boundSeries: [], + sprites: null, + surface: null, + range: null, + xValues: [], + yValues: [], + masterAxis: null, + applyRotation: function(b) { + var a = Math.PI * 2; + return (b % a + Math.PI) % a - Math.PI + }, + updateRotation: function(b) { + var c = this.getSprites(), + a = this.getPosition(); + if (!this.getHidden() && a === "angular" && c[0]) { + c[0].setAttributes({ + baseRotation: b + }) + } + }, + applyTitle: function(c, b) { + var a; + if (Ext.isString(c)) { + c = { + text: c + } + } + if (!b) { + b = Ext.create("sprite.text", c); + if ((a = this.getSurface())) { + a.add(b) + } + } else { + b.setAttributes(c) + } + return b + }, + applyFloating: function(b, a) { + if (b === null) { + b = { + value: null, + alongAxis: null + } + } else { + if (Ext.isNumber(b)) { + b = { + value: b, + alongAxis: null + } + } + } + if (Ext.isObject(b)) { + if (a && a.alongAxis) { + delete this.getChart().getAxis(a.alongAxis).floatingAxes[this.getId()] + } + return b + } + return a + }, + constructor: function(a) { + var b = this, + c; + b.sprites = []; + b.labels = []; + b.floatingAxes = {}; + a = a || {}; + if (a.position === "angular") { + a.style = a.style || {}; + a.style.estStepSize = 1 + } + if ("id" in a) { + c = a.id + } else { + if ("id" in b.config) { + c = b.config.id + } else { + c = b.getId() + } + } + b.setId(c); + b.mixins.observable.constructor.apply(b, arguments) + }, + getAlignment: function() { + switch (this.getPosition()) { + case "left": + case "right": + return "vertical"; + case "top": + case "bottom": + return "horizontal"; + case "radial": + return "radial"; + case "angular": + return "angular" + } + }, + getGridAlignment: function() { + switch (this.getPosition()) { + case "left": + case "right": + return "horizontal"; + case "top": + case "bottom": + return "vertical"; + case "radial": + return "circular"; + case "angular": + return "radial" + } + }, + getSurface: function() { + var e = this, + d = e.getChart(); + if (d && !e.surface) { + var b = e.surface = d.getSurface(e.getId(), "axis"), + c = e.gridSurface = d.getSurface("main"), + a = e.getSprites()[0], + f = e.getGridAlignment(); + c.waitFor(b); + e.getGrid(); + if (e.getLimits() && f) { + f = f.replace("3d", ""); + e.limits = { + surface: d.getSurface("overlay"), + lines: new Ext.chart.Markers(), + titles: new Ext.draw.sprite.Instancing() + }; + e.limits.lines.setTemplate({ + xclass: "grid." + f + }); + e.limits.lines.getTemplate().setAttributes({ + strokeStyle: "black" + }, true); + e.limits.surface.add(e.limits.lines); + a.bindMarker(f + "-limit-lines", e.limits.lines); + e.limitTitleTpl = new Ext.draw.sprite.Text(); + e.limits.titles.setTemplate(e.limitTitleTpl); + e.limits.surface.add(e.limits.titles); + d.on("redraw", e.renderLimits, e) + } + } + return e.surface + }, + applyGrid: function(a) { + if (a === true) { + return {} + } + return a + }, + updateGrid: function(b) { + var e = this, + d = e.getChart(); + if (!d) { + e.on({ + chartattached: Ext.bind(e.updateGrid, e, [b]), + single: true + }); + return + } + var c = e.gridSurface, + a = e.getSprites()[0], + f = e.getGridAlignment(), + g; + if (b) { + g = e.gridSpriteEven; + if (!g) { + g = e.gridSpriteEven = new Ext.chart.Markers(); + g.setTemplate({ + xclass: "grid." + f + }); + c.add(g); + a.bindMarker(f + "-even", g) + } + if (Ext.isObject(b)) { + g.getTemplate().setAttributes(b); + if (Ext.isObject(b.even)) { + g.getTemplate().setAttributes(b.even) + } + } + g = e.gridSpriteOdd; + if (!g) { + g = e.gridSpriteOdd = new Ext.chart.Markers(); + g.setTemplate({ + xclass: "grid." + f + }); + c.add(g); + a.bindMarker(f + "-odd", g) + } + if (Ext.isObject(b)) { + g.getTemplate().setAttributes(b); + if (Ext.isObject(b.odd)) { + g.getTemplate().setAttributes(b.odd) + } + } + } + }, + renderLimits: function() { + this.getSprites()[0].renderLimits() + }, + getCoordFor: function(c, d, a, b) { + return this.getLayout().getCoordFor(c, d, a, b) + }, + applyPosition: function(a) { + return a.toLowerCase() + }, + applyLength: function(b, a) { + return b > 0 ? b : a + }, + applyLabel: function(b, a) { + if (!a) { + a = new Ext.draw.sprite.Text({}) + } + if (this.limitTitleTpl) { + this.limitTitleTpl.setAttributes(b) + } + a.setAttributes(b); + return a + }, + applyLayout: function(b, a) { + b = Ext.factory(b, null, a, "axisLayout"); + b.setAxis(this); + return b + }, + applySegmenter: function(a, b) { + a = Ext.factory(a, null, b, "segmenter"); + a.setAxis(this); + return a + }, + updateMinimum: function() { + this.range = null + }, + updateMaximum: function() { + this.range = null + }, + hideLabels: function() { + this.getSprites()[0].setDirty(true); + this.setLabel({ + hidden: true + }) + }, + showLabels: function() { + this.getSprites()[0].setDirty(true); + this.setLabel({ + hidden: false + }) + }, + renderFrame: function() { + this.getSurface().renderFrame() + }, + updateChart: function(d, b) { + var c = this, + a; + if (b) { + b.unregister(c); + b.un("serieschange", c.onSeriesChange, c); + b.un("redraw", c.renderLimits, c); + c.linkAxis(); + c.fireEvent("chartdetached", b, c) + } + if (d) { + d.on("serieschange", c.onSeriesChange, c); + c.surface = null; + a = c.getSurface(); + c.getLabel().setSurface(a); + a.add(c.getSprites()); + a.add(c.getTitle()); + d.register(c); + c.fireEvent("chartattached", d, c) + } + }, + applyBackground: function(a) { + var b = Ext.ClassManager.getByAlias("sprite.rect"); + return b.def.normalize(a) + }, + processData: function() { + this.getLayout().processData(); + this.range = null + }, + getDirection: function() { + return this.getChart().getDirectionForAxis(this.getPosition()) + }, + isSide: function() { + var a = this.getPosition(); + return a === "left" || a === "right" + }, + applyFields: function(a) { + return Ext.Array.from(a) + }, + applyVisibleRange: function(a, c) { + this.getChart(); + if (a[0] > a[1]) { + var b = a[0]; + a[0] = a[1]; + a[0] = b + } + if (a[1] === a[0]) { + a[1] += 1 / this.getMaxZoom() + } + if (a[1] > a[0] + 1) { + a[0] = 0; + a[1] = 1 + } else { + if (a[0] < 0) { + a[1] -= a[0]; + a[0] = 0 + } else { + if (a[1] > 1) { + a[0] -= a[1] - 1; + a[1] = 1 + } + } + } + if (c && a[0] === c[0] && a[1] === c[1]) { + return undefined + } + return a + }, + updateVisibleRange: function(a) { + this.fireEvent("visiblerangechange", this, a) + }, + onSeriesChange: function(e) { + var f = this, + b = e.getSeries(), + j = "get" + f.getDirection() + "Axis", + g = [], + c, d = b.length, + a, h; + for (c = 0; c < d; c++) { + if (this === b[c][j]()) { + g.push(b[c]) + } + } + f.boundSeries = g; + a = f.getLinkedTo(); + h = !Ext.isEmpty(a) && e.getAxis(a); + if (h) { + f.linkAxis(h) + } else { + f.getLayout().processData() + } + }, + linkAxis: function(a) { + var c = this; + + function b(f, d, e) { + e.getLayout()[f]("datachange", "onDataChange", d); + e[f]("rangechange", "onMasterAxisRangeChange", d) + } + if (c.masterAxis) { + b("un", c, c.masterAxis); + c.masterAxis = null + } + if (a) { + if (a.type !== this.type) { + Ext.Error.raise("Linked axes must be of the same type.") + } + b("on", c, a); + c.onDataChange(a.getLayout().labels); + c.onMasterAxisRangeChange(a, a.range); + c.setStyle(Ext.apply({}, c.config.style, a.config.style)); + c.setTitle(Ext.apply({}, c.config.title, a.config.title)); + c.setLabel(Ext.apply({}, c.config.label, a.config.label)); + c.masterAxis = a + } + }, + onDataChange: function(a) { + this.getLayout().labels = a + }, + onMasterAxisRangeChange: function(b, a) { + this.range = a + }, + applyRange: function(a) { + if (!a) { + return this.dataRange.slice(0) + } else { + return [a[0] === null ? this.dataRange[0] : a[0], a[1] === null ? this.dataRange[1] : a[1]] + } + }, + getRange: function() { + var m = this; + if (m.range) { + return m.range + } else { + if (m.masterAxis) { + return m.masterAxis.range + } + } + if (Ext.isNumber(m.getMinimum() + m.getMaximum())) { + return m.range = [m.getMinimum(), m.getMaximum()] + } + var d = Infinity, + n = -Infinity, + o = m.boundSeries, + h = m.getLayout(), + l = m.getSegmenter(), + p = m.getVisibleRange(), + b = "get" + m.getDirection() + "Range", + a, j, g, f, e, k; + for (e = 0, k = o.length; e < k; e++) { + f = o[e]; + var c = f[b](); + if (c) { + if (c[0] < d) { + d = c[0] + } + if (c[1] > n) { + n = c[1] + } + } + } + if (!isFinite(n)) { + n = m.prevMax + } + if (!isFinite(d)) { + d = m.prevMin + } + if (m.getLabelInSpan() || d === n) { + n += m.getIncrement(); + d -= m.getIncrement() + } + if (Ext.isNumber(m.getMinimum())) { + d = m.getMinimum() + } else { + m.prevMin = d + } + if (Ext.isNumber(m.getMaximum())) { + n = m.getMaximum() + } else { + m.prevMax = n + } + m.range = [Ext.Number.correctFloat(d), Ext.Number.correctFloat(n)]; + if (m.getReconcileRange()) { + m.reconcileRange() + } + if (m.getAdjustByMajorUnit() && l.adjustByMajorUnit && !m.getMajorTickSteps()) { + j = Ext.Object.chain(m.getSprites()[0].attr); + j.min = m.range[0]; + j.max = m.range[1]; + j.visibleMin = p[0]; + j.visibleMax = p[1]; + a = { + attr: j, + segmenter: l + }; + h.calculateLayout(a); + g = a.majorTicks; + if (g) { + l.adjustByMajorUnit(g.step, g.unit.scale, m.range); + j.min = m.range[0]; + j.max = m.range[1]; + delete a.majorTicks; + h.calculateLayout(a); + g = a.majorTicks; + l.adjustByMajorUnit(g.step, g.unit.scale, m.range) + } else { + if (!m.hasClearRangePending) { + m.hasClearRangePending = true; + m.getChart().on("layout", "clearRange", m) + } + } + } + if (!Ext.Array.equals(m.range, m.oldRange || [])) { + m.fireEvent("rangechange", m, m.range); + m.oldRange = m.range + } + return m.range + }, + clearRange: function() { + delete this.hasClearRangePending; + this.range = null + }, + reconcileRange: function() { + var e = this, + g = e.getChart().getAxes(), + f = e.getDirection(), + b, d, c, a; + if (!g) { + return + } + for (b = 0, d = g.length; b < d; b++) { + c = g[b]; + a = c.getRange(); + if (c === e || c.getDirection() !== f || !a || !c.getReconcileRange()) { + continue + } + if (a[0] < e.range[0]) { + e.range[0] = a[0] + } + if (a[1] > e.range[1]) { + e.range[1] = a[1] + } + } + }, + applyStyle: function(c, b) { + var a = Ext.ClassManager.getByAlias("sprite." + this.seriesType); + if (a && a.def) { + c = a.def.normalize(c) + } + b = Ext.apply(b || {}, c); + return b + }, + themeOnlyIfConfigured: { + grid: true + }, + updateTheme: function(d) { + var i = this, + k = d.getAxis(), + e = i.getPosition(), + o = i.getInitialConfig(), + c = i.defaultConfig, + g = i.getConfigurator().configs, + a = k.defaults, + n = k[e], + h = i.themeOnlyIfConfigured, + l, j, p, b, m, f; + k = Ext.merge({}, a, n); + for (l in k) { + j = k[l]; + f = g[l]; + if (j !== null && j !== undefined && f) { + m = o[l]; + p = Ext.isObject(j); + b = m === c[l]; + if (p) { + if (b && h[l]) { + continue + } + j = Ext.merge({}, j, m) + } + if (b || p) { + i[f.names.set](j) + } + } + } + }, + updateCenter: function(b) { + var e = this.getSprites(), + a = e[0], + d = b[0], + c = b[1]; + if (a) { + a.setAttributes({ + centerX: d, + centerY: c + }) + } + if (this.gridSpriteEven) { + this.gridSpriteEven.getTemplate().setAttributes({ + translationX: d, + translationY: c, + rotationCenterX: d, + rotationCenterY: c + }) + } + if (this.gridSpriteOdd) { + this.gridSpriteOdd.getTemplate().setAttributes({ + translationX: d, + translationY: c, + rotationCenterX: d, + rotationCenterY: c + }) + } + }, + getSprites: function() { + if (!this.getChart()) { + return + } + var i = this, + e = i.getRange(), + f = i.getPosition(), + g = i.getChart(), + c = g.getAnimation(), + d, a, b = i.getLength(), + h = i.superclass; + if (c === false) { + c = { + duration: 0 + } + } + if (e) { + a = Ext.applyIf({ + position: f, + axis: i, + min: e[0], + max: e[1], + length: b, + grid: i.getGrid(), + hidden: i.getHidden(), + titleOffset: i.titleOffset, + layout: i.getLayout(), + segmenter: i.getSegmenter(), + totalAngle: i.getTotalAngle(), + label: i.getLabel() + }, i.getStyle()); + if (!i.sprites.length) { + while (!h.xtype) { + h = h.superclass + } + d = Ext.create("sprite." + h.xtype, a); + d.fx.setCustomDurations({ + baseRotation: 0 + }); + d.fx.on("animationstart", "onAnimationStart", i); + d.fx.on("animationend", "onAnimationEnd", i); + d.setLayout(i.getLayout()); + d.setSegmenter(i.getSegmenter()); + d.setLabel(i.getLabel()); + i.sprites.push(d); + i.updateTitleSprite() + } else { + d = i.sprites[0]; + d.setAnimation(c); + d.setAttributes(a) + } + if (i.getRenderer()) { + d.setRenderer(i.getRenderer()) + } + } + return i.sprites + }, + updateTitleSprite: function() { + var f = this, + b = f.getLength(); + if (!f.sprites[0] || !Ext.isNumber(b)) { + return + } + var h = this.sprites[0].thickness, + a = f.getSurface(), + g = f.getTitle(), + e = f.getPosition(), + c = f.getMargin(), + i = f.getTitleMargin(), + d = a.roundPixel(b / 2); + if (g) { + switch (e) { + case "top": + g.setAttributes({ + x: d, + y: c + i / 2, + textBaseline: "top", + textAlign: "center" + }, true); + g.applyTransformations(); + f.titleOffset = g.getBBox().height + i; + break; + case "bottom": + g.setAttributes({ + x: d, + y: h + i / 2, + textBaseline: "top", + textAlign: "center" + }, true); + g.applyTransformations(); + f.titleOffset = g.getBBox().height + i; + break; + case "left": + g.setAttributes({ + x: c + i / 2, + y: d, + textBaseline: "top", + textAlign: "center", + rotationCenterX: c + i / 2, + rotationCenterY: d, + rotationRads: -Math.PI / 2 + }, true); + g.applyTransformations(); + f.titleOffset = g.getBBox().width + i; + break; + case "right": + g.setAttributes({ + x: h - c + i / 2, + y: d, + textBaseline: "bottom", + textAlign: "center", + rotationCenterX: h + i / 2, + rotationCenterY: d, + rotationRads: Math.PI / 2 + }, true); + g.applyTransformations(); + f.titleOffset = g.getBBox().width + i; + break + } + } + }, + onThicknessChanged: function() { + this.getChart().onThicknessChanged() + }, + getThickness: function() { + if (this.getHidden()) { + return 0 + } + return (this.sprites[0] && this.sprites[0].thickness || 1) + this.titleOffset + this.getMargin() + }, + onAnimationStart: function() { + this.spriteAnimationCount++; + if (this.spriteAnimationCount === 1) { + this.fireEvent("animationstart", this) + } + }, + onAnimationEnd: function() { + this.spriteAnimationCount--; + if (this.spriteAnimationCount === 0) { + this.fireEvent("animationend", this) + } + }, + getItemId: function() { + return this.getId() + }, + getAncestorIds: function() { + return [this.getChart().getId()] + }, + isXType: function(a) { + return a === "axis" + }, + resolveListenerScope: function(e) { + var d = this, + a = Ext._namedScopes[e], + c = d.getChart(), + b; + if (!a) { + b = c ? c.resolveListenerScope(e, false) : (e || d) + } else { + if (a.isThis) { + b = d + } else { + if (a.isController) { + b = c ? c.resolveListenerScope(e, false) : d + } else { + if (a.isSelf) { + b = c ? c.resolveListenerScope(e, false) : d; + if (b === c && !c.getInheritedConfig("defaultListenerScope")) { + b = d + } + } + } + } + } + return b + }, + destroy: function() { + var a = this; + a.setChart(null); + a.surface.destroy(); + a.surface = null; + a.callParent() + } +}); +Ext.define("Ext.chart.LegendBase", { + extend: "Ext.view.View", + config: { + tpl: ['
', '', '
', "', "{name}", "
", "
", "
"], + nodeContainerSelector: "div." + Ext.baseCSSPrefix + "legend-container", + itemSelector: "div." + Ext.baseCSSPrefix + "legend-item", + docked: "bottom" + }, + setDocked: function(d) { + var c = this, + a = c.ownerCt, + b; + c.docked = d; + switch (d) { + case "top": + case "bottom": + c.addCls(Ext.baseCSSPrefix + "horizontal"); + b = "hbox"; + break; + case "left": + case "right": + c.removeCls(Ext.baseCSSPrefix + "horizontal"); + b = "vbox"; + break + } + if (a) { + a.setDocked(d) + } + }, + setStore: function(a) { + this.bindStore(a) + }, + clearViewEl: function() { + this.callParent(arguments); + Ext.removeNode(this.getNodeContainer()) + }, + onItemClick: function(a, c, b, d) { + this.callParent(arguments); + this.toggleItem(b) + } +}); +Ext.define("Ext.chart.Legend", { + xtype: "legend", + extend: "Ext.chart.LegendBase", + config: { + baseCls: Ext.baseCSSPrefix + "legend", + padding: 5, + rect: null, + disableSelection: true, + toggleable: true + }, + toggleItem: function(c) { + if (!this.getToggleable()) { + return + } + var b = this.getStore(), + h = 0, + e, g = true, + d, f, a; + if (b) { + f = b.getCount(); + for (d = 0; d < f; d++) { + a = b.getAt(d); + if (a.get("disabled")) { + h++ + } + } + g = f - h > 1; + a = b.getAt(c); + if (a) { + e = a.get("disabled"); + if (e || g) { + a.set("disabled", !e) + } + } + } + } +}); +Ext.define("Ext.chart.AbstractChart", { + extend: "Ext.draw.Container", + requires: ["Ext.chart.theme.Default", "Ext.chart.series.Series", "Ext.chart.interactions.Abstract", "Ext.chart.axis.Axis", "Ext.data.StoreManager", "Ext.chart.Legend", "Ext.data.Store"], + isChart: true, + defaultBindProperty: "store", + config: { + store: "ext-empty-store", + theme: "default", + style: null, + animation: !Ext.isIE8, + series: [], + axes: [], + legend: null, + colors: null, + insetPadding: { + top: 10, + left: 10, + right: 10, + bottom: 10 + }, + background: null, + interactions: [], + mainRect: null, + resizeHandler: null, + highlightItem: null + }, + animationSuspendCount: 0, + chartLayoutSuspendCount: 0, + axisThicknessSuspendCount: 0, + isThicknessChanged: false, + surfaceZIndexes: { + background: 0, + main: 1, + grid: 2, + series: 3, + axis: 4, + chart: 5, + overlay: 6, + events: 7 + }, + constructor: function(a) { + var b = this; + b.itemListeners = {}; + b.surfaceMap = {}; + b.chartComponents = {}; + b.isInitializing = true; + b.suspendChartLayout(); + b.animationSuspendCount++; + b.callParent(arguments); + delete b.isInitializing; + b.getSurface("main"); + b.getSurface("chart").setFlipRtlText(b.getInherited().rtl); + b.getSurface("overlay").waitFor(b.getSurface("series")); + b.animationSuspendCount--; + b.resumeChartLayout() + }, + applyAnimation: function(a, b) { + if (!a) { + a = { + duration: 0 + } + } else { + if (a === true) { + a = { + easing: "easeInOut", + duration: 500 + } + } + } + return b ? Ext.apply({}, a, b) : a + }, + getAnimation: function() { + if (this.animationSuspendCount) { + return { + duration: 0 + } + } else { + return this.callParent() + } + }, + applyInsetPadding: function(b, a) { + if (!Ext.isObject(b)) { + return Ext.util.Format.parseBox(b) + } else { + if (!a) { + return b + } else { + return Ext.apply(a, b) + } + } + }, + suspendAnimation: function() { + var d = this, + c = d.getSeries(), + e = c.length, + b = -1, + a; + d.animationSuspendCount++; + if (d.animationSuspendCount === 1) { + while (++b < e) { + a = c[b]; + a.setAnimation(a.getAnimation()) + } + } + }, + resumeAnimation: function() { + var d = this, + c = d.getSeries(), + f = c.length, + b = -1, + a, e; + d.animationSuspendCount--; + if (d.animationSuspendCount === 0) { + while (++b < f) { + a = c[b]; + e = a.getAnimation(); + a.setAnimation(e.duration && e || d.getAnimation()) + } + } + }, + suspendChartLayout: function() { + this.chartLayoutSuspendCount++; + if (this.chartLayoutSuspendCount === 1) { + if (this.scheduledLayoutId) { + this.layoutInSuspension = true; + this.cancelChartLayout() + } else { + this.layoutInSuspension = false + } + } + }, + resumeChartLayout: function() { + this.chartLayoutSuspendCount--; + if (this.chartLayoutSuspendCount === 0) { + if (this.layoutInSuspension) { + this.scheduleLayout() + } + } + }, + cancelChartLayout: function() { + if (this.scheduledLayoutId) { + Ext.draw.Animator.cancel(this.scheduledLayoutId); + this.scheduledLayoutId = null + } + }, + scheduleLayout: function() { + var a = this; + if (a.allowSchedule() && !a.scheduledLayoutId) { + a.scheduledLayoutId = Ext.draw.Animator.schedule("doScheduleLayout", a) + } + }, + allowSchedule: function() { + return true + }, + doScheduleLayout: function() { + if (this.chartLayoutSuspendCount) { + this.layoutInSuspension = true + } else { + this.performLayout() + } + }, + suspendThicknessChanged: function() { + this.axisThicknessSuspendCount++ + }, + resumeThicknessChanged: function() { + if (this.axisThicknessSuspendCount > 0) { + this.axisThicknessSuspendCount--; + if (this.axisThicknessSuspendCount === 0 && this.isThicknessChanged) { + this.onThicknessChanged() + } + } + }, + onThicknessChanged: function() { + if (this.axisThicknessSuspendCount === 0) { + this.isThicknessChanged = false; + this.performLayout() + } else { + this.isThicknessChanged = true + } + }, + applySprites: function(b) { + var a = this.getSurface("chart"); + b = Ext.Array.from(b); + a.removeAll(true); + a.add(b); + return b + }, + initItems: function() { + var a = this.items, + b, d, c; + if (a && !a.isMixedCollection) { + this.items = []; + a = Ext.Array.from(a); + for (b = 0, d = a.length; b < d; b++) { + c = a[b]; + if (c.type) { + Ext.raise("To add custom sprites to the chart use the 'sprites' config.") + } else { + this.items.push(c) + } + } + } + this.callParent() + }, + applyBackground: function(c, e) { + var b = this.getSurface("background"), + d, a, f; + if (c) { + if (e) { + d = e.attr.width; + a = e.attr.height; + f = e.type === (c.type || "rect") + } + if (c.isSprite) { + e = c + } else { + if (c.type === "image" && Ext.isString(c.src)) { + if (f) { + e.setAttributes({ + src: c.src + }) + } else { + b.remove(e, true); + e = b.add(c) + } + } else { + if (f) { + e.setAttributes({ + fillStyle: c + }) + } else { + b.remove(e, true); + e = b.add({ + type: "rect", + fillStyle: c, + fx: { + customDurations: { + x: 0, + y: 0, + width: 0, + height: 0 + } + } + }) + } + } + } + } + if (d && a) { + e.setAttributes({ + width: d, + height: a + }) + } + e.setAnimation(this.getAnimation()); + return e + }, + getLegendStore: function() { + return this.legendStore + }, + refreshLegendStore: function() { + if (this.getLegendStore()) { + var d, e, c = this.getSeries(), + b, a = []; + if (c) { + for (d = 0, e = c.length; d < e; d++) { + b = c[d]; + if (b.getShowInLegend()) { + b.provideLegendInfo(a) + } + } + } + this.getLegendStore().setData(a) + } + }, + resetLegendStore: function() { + var c = this.getLegendStore(), + e, d, a, b; + if (c) { + e = this.getLegendStore().getData().items; + for (d = 0, a = e.length; d < a; d++) { + b = e[d]; + b.beginEdit(); + b.set("disabled", false); + b.commit() + } + } + }, + onUpdateLegendStore: function(b, a) { + var d = this.getSeries(), + c; + if (a && d) { + c = d.map[a.get("series")]; + if (c) { + c.setHiddenByIndex(a.get("index"), a.get("disabled")); + this.redraw() + } + } + }, + defaultResizeHandler: function(a) { + this.scheduleLayout(); + return false + }, + applyMainRect: function(a, b) { + if (!b) { + return a + } + this.getSeries(); + this.getAxes(); + if (a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3]) { + return b + } else { + return a + } + }, + register: function(a) { + var b = this.chartComponents, + c = a.getId(); + b[c] = a + }, + unregister: function(a) { + var b = this.chartComponents, + c = a.getId(); + delete b[c] + }, + get: function(a) { + return this.chartComponents[a] + }, + getAxis: function(a) { + if (a instanceof Ext.chart.axis.Axis) { + return a + } else { + if (Ext.isNumber(a)) { + return this.getAxes()[a] + } else { + if (Ext.isString(a)) { + return this.get(a) + } + } + } + }, + getSurface: function(b, c) { + b = b || "main"; + c = c || b; + var d = this, + a = this.callParent([b]), + f = d.surfaceZIndexes, + e = d.surfaceMap; + if (c in f) { + a.element.setStyle("zIndex", f[c]) + } + if (!e[c]) { + e[c] = [] + } + if (Ext.Array.indexOf(e[c], a) < 0) { + a.type = c; + e[c].push(a); + a.on("destroy", d.forgetSurface, d) + } + return a + }, + forgetSurface: function(a) { + var d = this.surfaceMap; + if (!d || this.isDestroying) { + return + } + var c = d[a.type], + b = c ? Ext.Array.indexOf(c, a) : -1; + if (b >= 0) { + c.splice(b, 1) + } + }, + applyAxes: function(b, k) { + var l = this, + g = { + left: "right", + right: "left" + }, + m = [], + c, d, e, a, f, h, j; + l.animationSuspendCount++; + l.getStore(); + if (!k) { + k = []; + k.map = {} + } + j = k.map; + m.map = {}; + b = Ext.Array.from(b, true); + for (f = 0, h = b.length; f < h; f++) { + c = b[f]; + if (!c) { + continue + } + if (c instanceof Ext.chart.axis.Axis) { + d = j[c.getId()]; + c.setChart(l) + } else { + c = Ext.Object.chain(c); + e = c.linkedTo; + a = c.id; + if (Ext.isNumber(e)) { + c = Ext.merge({}, b[e], c) + } else { + if (Ext.isString(e)) { + Ext.Array.each(b, function(i) { + if (i.id === c.linkedTo) { + c = Ext.merge({}, i, c); + return false + } + }) + } + } + c.id = a; + c.chart = l; + if (l.getInherited().rtl) { + c.position = g[c.position] || c.position + } + a = c.getId && c.getId() || c.id; + c = Ext.factory(c, null, d = j[a], "axis") + } + if (c) { + m.push(c); + m.map[c.getId()] = c; + if (!d) { + c.on("animationstart", "onAnimationStart", l); + c.on("animationend", "onAnimationEnd", l) + } + } + } + for (f in j) { + if (!m.map[f]) { + j[f].destroy() + } + } + l.animationSuspendCount--; + return m + }, + updateAxes: function() { + if (!this.isDestroying) { + this.scheduleLayout() + } + }, + circularCopyArray: function(e, f, d) { + var c = [], + b, a = e && e.length; + if (a) { + for (b = 0; b < d; b++) { + c.push(e[(f + b) % a]) + } + } + return c + }, + circularCopyObject: function(f, g, d) { + var c = this, + b, e, a = {}; + if (d) { + for (b in f) { + if (f.hasOwnProperty(b)) { + e = f[b]; + if (Ext.isArray(e)) { + a[b] = c.circularCopyArray(e, g, d) + } else { + a[b] = e + } + } + } + } + return a + }, + getColors: function() { + var b = this, + a = b.config.colors, + c = b.getTheme(); + if (Ext.isArray(a) && a.length > 0) { + a = b.applyColors(a) + } + return a || (c && c.getColors()) + }, + applyColors: function(a) { + a = Ext.Array.map(a, function(b) { + if (Ext.isString(b)) { + return b + } else { + return b.toString() + } + }); + return a + }, + updateColors: function(c) { + var k = this, + e = k.getTheme(), + a = c || (e && e.getColors()), + l = 0, + f = k.getSeries(), + d = f && f.length, + g, j, b, h; + if (a.length) { + for (g = 0; g < d; g++) { + j = f[g]; + h = j.themeColorCount(); + b = k.circularCopyArray(a, l, h); + l += h; + j.updateChartColors(b) + } + } + k.refreshLegendStore() + }, + applyTheme: function(a) { + if (a && a.isTheme) { + return a + } + return Ext.Factory.chartTheme(a) + }, + updateTheme: function(g) { + var e = this, + f = e.getAxes(), + d = e.getSeries(), + a = e.getColors(), + c, b; + e.updateChartTheme(g); + for (b = 0; b < f.length; b++) { + f[b].updateTheme(g) + } + for (b = 0; b < d.length; b++) { + c = d[b]; + c.updateTheme(g) + } + e.updateSpriteTheme(g); + e.updateColors(a); + e.redraw() + }, + themeOnlyIfConfigured: {}, + updateChartTheme: function(c) { + var i = this, + k = c.getChart(), + n = i.getInitialConfig(), + b = i.defaultConfig, + e = i.getConfigurator().configs, + f = k.defaults, + g = k[i.xtype], + h = i.themeOnlyIfConfigured, + l, j, o, a, m, d; + k = Ext.merge({}, f, g); + for (l in k) { + j = k[l]; + d = e[l]; + if (j !== null && j !== undefined && d) { + m = n[l]; + o = Ext.isObject(j); + a = m === b[l]; + if (o) { + if (a && h[l]) { + continue + } + j = Ext.merge({}, j, m) + } + if (a || o) { + i[d.names.set](j) + } + } + } + }, + updateSpriteTheme: function(c) { + this.getSprites(); + var j = this, + e = j.getSurface("chart"), + h = e.getItems(), + m = c.getSprites(), + k, a, l, f, d, b, g; + for (b = 0, g = h.length; b < g; b++) { + k = h[b]; + a = m[k.type]; + if (a) { + f = {}; + d = k.type === "text"; + for (l in a) { + if (!(l in k.config)) { + if (!(d && l.indexOf("font") === 0 && k.config.font)) { + f[l] = a[l] + } + } + } + k.setAttributes(f) + } + } + }, + addSeries: function(b) { + var a = this.getSeries(); + Ext.Array.push(a, b); + this.setSeries(a) + }, + removeSeries: function(d) { + d = Ext.Array.from(d); + var b = this.getSeries(), + f = [], + a = d.length, + g = {}, + c, e; + for (c = 0; c < a; c++) { + e = d[c]; + if (typeof e !== "string") { + e = e.getId() + } + g[e] = true + } + for (c = 0, a = b.length; c < a; c++) { + if (!g[b[c].getId()]) { + f.push(b[c]) + } + } + this.setSeries(f) + }, + applySeries: function(e, d) { + var g = this, + j = [], + h, a, c, f, b; + g.animationSuspendCount++; + g.getAxes(); + if (d) { + h = d.map + } else { + d = []; + h = d.map = {} + } + j.map = {}; + e = Ext.Array.from(e, true); + for (c = 0, f = e.length; c < f; c++) { + b = e[c]; + if (!b) { + continue + } + a = h[b.getId && b.getId() || b.id]; + if (b instanceof Ext.chart.series.Series) { + if (a && a !== b) { + a.destroy() + } + b.setChart(g) + } else { + if (Ext.isObject(b)) { + if (a) { + a.setConfig(b); + b = a + } else { + if (Ext.isString(b)) { + b = { + type: b + } + } + b.chart = g; + b = Ext.create(b.xclass || ("series." + b.type), b); + b.on("animationstart", "onAnimationStart", g); + b.on("animationend", "onAnimationEnd", g) + } + } + } + j.push(b); + j.map[b.getId()] = b + } + for (c in h) { + if (!j.map[h[c].getId()]) { + h[c].destroy() + } + } + g.animationSuspendCount--; + return j + }, + applyLegend: function(b, a) { + return Ext.factory(b, Ext.chart.Legend, a) + }, + updateLegend: function(b, a) { + if (a) { + a.destroy() + } + if (b) { + this.getItems(); + this.legendStore = new Ext.data.Store({ + autoDestroy: true, + fields: ["id", "name", "mark", "disabled", "series", "index"] + }); + b.setStore(this.legendStore); + this.refreshLegendStore(); + this.legendStore.on("update", "onUpdateLegendStore", this) + } + }, + updateSeries: function(b, a) { + var c = this; + if (c.isDestroying) { + return + } + c.animationSuspendCount++; + c.fireEvent("serieschange", c, b, a); + c.refreshLegendStore(); + if (!Ext.isEmpty(b)) { + c.updateTheme(c.getTheme()) + } + c.scheduleLayout(); + c.animationSuspendCount-- + }, + applyInteractions: function(h, d) { + if (!d) { + d = []; + d.map = {} + } + var g = this, + a = [], + c = d.map, + e, f, b; + a.map = {}; + h = Ext.Array.from(h, true); + for (e = 0, f = h.length; e < f; e++) { + b = h[e]; + if (!b) { + continue + } + b = Ext.factory(b, null, c[b.getId && b.getId() || b.id], "interaction"); + if (b) { + b.setChart(g); + a.push(b); + a.map[b.getId()] = b + } + } + for (e in c) { + if (!a.map[e]) { + c[e].destroy() + } + } + return a + }, + getInteraction: function(e) { + var f = this.getInteractions(), + a = f && f.length, + c = null, + b, d; + if (a) { + for (d = 0; d < a; ++d) { + b = f[d]; + if (b.type === e) { + c = b; + break + } + } + } + return c + }, + applyStore: function(a) { + return a && Ext.StoreManager.lookup(a) + }, + updateStore: function(a, c) { + var b = this; + if (c) { + c.un({ + datachanged: "onDataChanged", + update: "onDataChanged", + scope: b, + order: "after" + }); + if (c.autoDestroy) { + c.destroy() + } + } + if (a) { + a.on({ + datachanged: "onDataChanged", + update: "onDataChanged", + scope: b, + order: "after" + }) + } + b.fireEvent("storechange", b, a, c); + b.onDataChanged() + }, + redraw: function() { + this.fireEvent("redraw", this) + }, + performLayout: function() { + var d = this, + b = d.getChartSize(true), + c = [0, 0, b.width, b.height], + a = d.getBackground(); + d.hasFirstLayout = true; + d.fireEvent("layout", d); + d.cancelChartLayout(); + d.getSurface("background").setRect(c); + d.getSurface("chart").setRect(c); + a.setAttributes({ + width: b.width, + height: b.height + }) + }, + getChartSize: function(b) { + var a = this; + if (b) { + a.chartSize = null + } + return a.chartSize || (a.chartSize = a.innerElement.getSize()) + }, + getEventXY: function(a) { + return this.getSurface().getEventXY(a) + }, + getItemForPoint: function(h, g) { + var f = this, + a = f.getSeries(), + e = f.getMainRect(), + d = a.length, + b = f.hasFirstLayout ? d - 1 : -1, + c, j; + if (!(e && h >= 0 && h <= e[2] && g >= 0 && g <= e[3])) { + return null + } + for (; b >= 0; b--) { + c = a[b]; + j = c.getItemForPoint(h, g); + if (j) { + return j + } + } + return null + }, + getItemsForPoint: function(h, g) { + var f = this, + a = f.getSeries(), + d = a.length, + b = f.hasFirstLayout ? d - 1 : -1, + e = [], + c, j; + for (; b >= 0; b--) { + c = a[b]; + j = c.getItemForPoint(h, g); + if (j) { + e.push(j) + } + } + return e + }, + onAnimationStart: function() { + this.fireEvent("animationstart", this) + }, + onAnimationEnd: function() { + this.fireEvent("animationend", this) + }, + onDataChanged: function() { + var d = this; + if (d.isInitializing) { + return + } + var c = d.getMainRect(), + a = d.getStore(), + b = d.getSeries(), + e = d.getAxes(); + if (!a || !e || !b) { + return + } + if (!c) { + d.on({ + redraw: d.onDataChanged, + scope: d, + single: true + }); + return + } + d.processData(); + d.redraw() + }, + recordCount: 0, + processData: function() { + var g = this, + e = g.getStore().getCount(), + c = g.getSeries(), + f = c.length, + d = false, + b = 0, + a; + for (; b < f; b++) { + a = c[b]; + a.processData(); + if (!d && a.isStoreDependantColorCount) { + d = true + } + } + if (d && e > g.recordCount) { + g.updateColors(g.getColors()); + g.recordCount = e + } + }, + bindStore: function(a) { + this.setStore(a) + }, + applyHighlightItem: function(f, a) { + if (f === a) { + return + } + if (Ext.isObject(f) && Ext.isObject(a)) { + var e = f, + d = a, + c = e.sprite && (e.sprite[0] || e.sprite), + b = d.sprite && (d.sprite[0] || d.sprite); + if (c === b && e.index === d.index) { + return + } + } + return f + }, + updateHighlightItem: function(b, a) { + if (a) { + a.series.setAttributesForItem(a, { + highlighted: false + }) + } + if (b) { + b.series.setAttributesForItem(b, { + highlighted: true + }); + this.fireEvent("itemhighlight", this, b, a) + } + this.fireEvent("itemhighlightchange", this, b, a) + }, + destroyChart: function() { + var f = this, + d = f.getLegend(), + g = f.getAxes(), + c = f.getSeries(), + h = f.getInteractions(), + b = [], + a, e; + f.surfaceMap = null; + for (a = 0, e = h.length; a < e; a++) { + h[a].destroy() + } + for (a = 0, e = g.length; a < e; a++) { + g[a].destroy() + } + for (a = 0, e = c.length; a < e; a++) { + c[a].destroy() + } + f.setInteractions(b); + f.setAxes(b); + f.setSeries(b); + if (d) { + d.destroy(); + f.setLegend(null) + } + f.legendStore = null; + f.setStore(null); + f.cancelChartLayout() + }, + getRefItems: function(b) { + var g = this, + e = g.getSeries(), + h = g.getAxes(), + a = g.getInteractions(), + c = [], + d, f; + for (d = 0, f = e.length; d < f; d++) { + c.push(e[d]); + if (e[d].getRefItems) { + c.push.apply(c, e[d].getRefItems(b)) + } + } + for (d = 0, f = h.length; d < f; d++) { + c.push(h[d]); + if (h[d].getRefItems) { + c.push.apply(c, h[d].getRefItems(b)) + } + } + for (d = 0, f = a.length; d < f; d++) { + c.push(a[d]); + if (a[d].getRefItems) { + c.push.apply(c, a[d].getRefItems(b)) + } + } + return c + } +}); +Ext.define("Ext.chart.overrides.AbstractChart", { + override: "Ext.chart.AbstractChart", + updateLegend: function(b, a) { + var c; + this.callParent([b, a]); + if (b) { + c = b.docked; + this.addDocked({ + dock: c, + xtype: "panel", + shrinkWrap: true, + scrollable: true, + layout: { + type: c === "top" || c === "bottom" ? "hbox" : "vbox", + pack: "center" + }, + items: b, + cls: Ext.baseCSSPrefix + "legend-panel" + }) + } + }, + performLayout: function() { + if (this.isVisible(true)) { + return this.callParent() + } + this.cancelChartLayout(); + return false + }, + afterComponentLayout: function(c, a, b, d) { + this.callParent([c, a, b, d]); + this.scheduleLayout() + }, + allowSchedule: function() { + return this.rendered + }, + onDestroy: function() { + this.destroyChart(); + this.callParent(arguments) + } +}); +Ext.define("Ext.chart.grid.HorizontalGrid", { + extend: "Ext.draw.sprite.Sprite", + alias: "grid.horizontal", + inheritableStatics: { + def: { + processors: { + x: "number", + y: "number", + width: "number", + height: "number" + }, + defaults: { + x: 0, + y: 0, + width: 1, + height: 1, + strokeStyle: "#DDD" + } + } + }, + render: function(b, c, e) { + var a = this.attr, + f = b.roundPixel(a.y), + d = c.lineWidth * 0.5; + c.beginPath(); + c.rect(e[0] - b.matrix.getDX(), f + d, +e[2], a.height); + c.fill(); + c.beginPath(); + c.moveTo(e[0] - b.matrix.getDX(), f + d); + c.lineTo(e[0] + e[2] - b.matrix.getDX(), f + d); + c.stroke() + } +}); +Ext.define("Ext.chart.grid.VerticalGrid", { + extend: "Ext.draw.sprite.Sprite", + alias: "grid.vertical", + inheritableStatics: { + def: { + processors: { + x: "number", + y: "number", + width: "number", + height: "number" + }, + defaults: { + x: 0, + y: 0, + width: 1, + height: 1, + strokeStyle: "#DDD" + } + } + }, + render: function(c, d, f) { + var b = this.attr, + a = c.roundPixel(b.x), + e = d.lineWidth * 0.5; + d.beginPath(); + d.rect(a - e, f[1] - c.matrix.getDY(), b.width, f[3]); + d.fill(); + d.beginPath(); + d.moveTo(a - e, f[1] - c.matrix.getDY()); + d.lineTo(a - e, f[1] + f[3] - c.matrix.getDY()); + d.stroke() + } +}); +Ext.define("Ext.chart.CartesianChart", { + extend: "Ext.chart.AbstractChart", + alternateClassName: "Ext.chart.Chart", + requires: ["Ext.chart.grid.HorizontalGrid", "Ext.chart.grid.VerticalGrid"], + xtype: ["cartesian", "chart"], + isCartesian: true, + config: { + flipXY: false, + innerRect: [0, 0, 1, 1], + innerPadding: { + top: 0, + left: 0, + right: 0, + bottom: 0 + } + }, + applyInnerPadding: function(b, a) { + if (!Ext.isObject(b)) { + return Ext.util.Format.parseBox(b) + } else { + if (!a) { + return b + } else { + return Ext.apply(a, b) + } + } + }, + getDirectionForAxis: function(a) { + var b = this.getFlipXY(); + if (a === "left" || a === "right") { + if (b) { + return "X" + } else { + return "Y" + } + } else { + if (b) { + return "Y" + } else { + return "X" + } + } + }, + performLayout: function() { + var A = this; + A.animationSuspendCount++; + if (A.callParent() === false) { + --A.animationSuspendCount; + return + } + A.suspendThicknessChanged(); + var d = A.getSurface("chart").getRect(), + o = d[2], + n = d[3], + z = A.getAxes(), + b, q = A.getSeries(), + h, l, a, f = A.getInsetPadding(), + v = A.getInnerPadding(), + r, c, e = Ext.apply({}, f), + u, p, s, k, m, y, t, x, g, j = A.getInherited().rtl, + w = A.getFlipXY(); + if (o <= 0 || n <= 0) { + return + } + for (x = 0; x < z.length; x++) { + b = z[x]; + l = b.getSurface(); + m = b.getFloating(); + y = m ? m.value : null; + a = b.getThickness(); + switch (b.getPosition()) { + case "top": + l.setRect([0, e.top + 1, o, a]); + break; + case "bottom": + l.setRect([0, n - (e.bottom + a), o, a]); + break; + case "left": + l.setRect([e.left, 0, a, n]); + break; + case "right": + l.setRect([o - (e.right + a), 0, a, n]); + break + } + if (y === null) { + e[b.getPosition()] += a + } + } + o -= e.left + e.right; + n -= e.top + e.bottom; + u = [e.left, e.top, o, n]; + e.left += v.left; + e.top += v.top; + e.right += v.right; + e.bottom += v.bottom; + p = o - v.left - v.right; + s = n - v.top - v.bottom; + A.setInnerRect([e.left, e.top, p, s]); + if (p <= 0 || s <= 0) { + return + } + A.setMainRect(u); + A.getSurface().setRect(u); + for (x = 0, g = A.surfaceMap.grid && A.surfaceMap.grid.length; x < g; x++) { + c = A.surfaceMap.grid[x]; + c.setRect(u); + c.matrix.set(1, 0, 0, 1, v.left, v.top); + c.matrix.inverse(c.inverseMatrix) + } + for (x = 0; x < z.length; x++) { + b = z[x]; + l = b.getSurface(); + t = l.matrix; + k = t.elements; + switch (b.getPosition()) { + case "top": + case "bottom": + k[4] = e.left; + b.setLength(p); + break; + case "left": + case "right": + k[5] = e.top; + b.setLength(s); + break + } + b.updateTitleSprite(); + t.inverse(l.inverseMatrix) + } + for (x = 0, g = q.length; x < g; x++) { + h = q[x]; + r = h.getSurface(); + r.setRect(u); + if (w) { + if (j) { + r.matrix.set(0, -1, -1, 0, v.left + p, v.top + s) + } else { + r.matrix.set(0, -1, 1, 0, v.left, v.top + s) + } + } else { + r.matrix.set(1, 0, 0, -1, v.left, v.top + s) + } + r.matrix.inverse(r.inverseMatrix); + h.getOverlaySurface().setRect(u) + } + A.redraw(); + A.animationSuspendCount--; + A.resumeThicknessChanged() + }, + refloatAxes: function() { + var h = this, + g = h.getAxes(), + o = (g && g.length) || 0, + c, d, n, f, l, b, k, r = h.getChartSize(), + q = h.getInsetPadding(), + p = h.getInnerPadding(), + a = r.width - q.left - q.right, + m = r.height - q.top - q.bottom, + j, e; + for (e = 0; e < o; e++) { + c = g[e]; + f = c.getFloating(); + l = f ? f.value : null; + if (l === null) { + delete c.floatingAtCoord; + continue + } + d = c.getSurface(); + n = d.getRect(); + if (!n) { + continue + } + n = n.slice(); + b = h.getAxis(f.alongAxis); + if (b) { + j = b.getAlignment() === "horizontal"; + if (Ext.isString(l)) { + l = b.getCoordFor(l) + } + b.floatingAxes[c.getId()] = l; + k = b.getSprites()[0].attr.matrix; + if (j) { + l = l * k.getXX() + k.getDX(); + c.floatingAtCoord = l + p.left + p.right + } else { + l = l * k.getYY() + k.getDY(); + c.floatingAtCoord = l + p.top + p.bottom + } + } else { + j = c.getAlignment() === "horizontal"; + if (j) { + c.floatingAtCoord = l + p.top + p.bottom + } else { + c.floatingAtCoord = l + p.left + p.right + } + l = d.roundPixel(0.01 * l * (j ? m : a)) + } + switch (c.getPosition()) { + case "top": + n[1] = q.top + p.top + l - n[3] + 1; + break; + case "bottom": + n[1] = q.top + p.top + (b ? l : m - l); + break; + case "left": + n[0] = q.left + p.left + l - n[2]; + break; + case "right": + n[0] = q.left + p.left + (b ? l : a - l) - 1; + break + } + d.setRect(n) + } + }, + redraw: function() { + var C = this, + r = C.getSeries(), + z = C.getAxes(), + b = C.getMainRect(), + p, t, w = C.getInnerPadding(), + f, l, s, e, q, A, v, g, d, c, a, k, n, y = C.getFlipXY(), + x = 1000, + m, u, h, o, B; + if (!b) { + return + } + p = b[2] - w.left - w.right; + t = b[3] - w.top - w.bottom; + for (A = 0; A < r.length; A++) { + h = r[A]; + if ((c = h.getXAxis())) { + n = c.getVisibleRange(); + l = c.getRange(); + l = [l[0] + (l[1] - l[0]) * n[0], l[0] + (l[1] - l[0]) * n[1]] + } else { + l = h.getXRange() + } + if ((a = h.getYAxis())) { + n = a.getVisibleRange(); + s = a.getRange(); + s = [s[0] + (s[1] - s[0]) * n[0], s[0] + (s[1] - s[0]) * n[1]] + } else { + s = h.getYRange() + } + q = { + visibleMinX: l[0], + visibleMaxX: l[1], + visibleMinY: s[0], + visibleMaxY: s[1], + innerWidth: p, + innerHeight: t, + flipXY: y + }; + f = h.getSprites(); + for (v = 0, g = f.length; v < g; v++) { + o = f[v]; + m = o.attr.zIndex; + if (m < x) { + m += (A + 1) * 100 + x; + o.attr.zIndex = m; + B = o.getMarker("items"); + if (B) { + u = B.attr.zIndex; + if (u === Number.MAX_VALUE) { + B.attr.zIndex = m + } else { + if (u < x) { + B.attr.zIndex = m + u + } + } + } + } + o.setAttributes(q, true) + } + } + for (A = 0; A < z.length; A++) { + d = z[A]; + e = d.isSide(); + f = d.getSprites(); + k = d.getRange(); + n = d.getVisibleRange(); + q = { + dataMin: k[0], + dataMax: k[1], + visibleMin: n[0], + visibleMax: n[1] + }; + if (e) { + q.length = t; + q.startGap = w.bottom; + q.endGap = w.top + } else { + q.length = p; + q.startGap = w.left; + q.endGap = w.right + } + for (v = 0, g = f.length; v < g; v++) { + f[v].setAttributes(q, true) + } + } + C.renderFrame(); + C.callParent(arguments) + }, + renderFrame: function() { + this.refloatAxes(); + this.callParent() + } +}); +Ext.define("Ext.chart.grid.CircularGrid", { + extend: "Ext.draw.sprite.Circle", + alias: "grid.circular", + inheritableStatics: { + def: { + defaults: { + r: 1, + strokeStyle: "#DDD" + } + } + } +}); +Ext.define("Ext.chart.grid.RadialGrid", { + extend: "Ext.draw.sprite.Path", + alias: "grid.radial", + inheritableStatics: { + def: { + processors: { + startRadius: "number", + endRadius: "number" + }, + defaults: { + startRadius: 0, + endRadius: 1, + scalingCenterX: 0, + scalingCenterY: 0, + strokeStyle: "#DDD" + }, + triggers: { + startRadius: "path,bbox", + endRadius: "path,bbox" + } + } + }, + render: function() { + this.callParent(arguments) + }, + updatePath: function(d, a) { + var b = a.startRadius, + c = a.endRadius; + d.moveTo(b, 0); + d.lineTo(c, 0) + } +}); +Ext.define("Ext.chart.PolarChart", { + extend: "Ext.chart.AbstractChart", + requires: ["Ext.chart.grid.CircularGrid", "Ext.chart.grid.RadialGrid"], + xtype: "polar", + isPolar: true, + config: { + center: [0, 0], + radius: 0, + innerPadding: 0 + }, + getDirectionForAxis: function(a) { + return a === "radial" ? "Y" : "X" + }, + applyCenter: function(a, b) { + if (b && a[0] === b[0] && a[1] === b[1]) { + return + } + return [+a[0], +a[1]] + }, + updateCenter: function(a) { + var g = this, + h = g.getAxes(), + d = g.getSeries(), + c, f, e, b; + for (c = 0, f = h.length; c < f; c++) { + e = h[c]; + e.setCenter(a) + } + for (c = 0, f = d.length; c < f; c++) { + b = d[c]; + b.setCenter(a) + } + }, + applyInnerPadding: function(b, a) { + return Ext.isNumber(b) ? b : a + }, + doSetSurfaceRect: function(b, c) { + var a = this.getMainRect(); + b.setRect(c); + b.matrix.set(1, 0, 0, 1, a[0] - c[0], a[1] - c[1]); + b.inverseMatrix.set(1, 0, 0, 1, c[0] - a[0], c[1] - a[1]) + }, + applyAxes: function(f, h) { + var e = this, + g = Ext.Array.from(e.config.series)[0], + b, d, c, a; + if (g.type === "radar" && f && f.length) { + for (b = 0, d = f.length; b < d; b++) { + c = f[b]; + if (c.position === "angular") { + a = true; + break + } + } + if (!a) { + f.push({ + type: "category", + position: "angular", + fields: g.xField || g.angleField, + style: { + estStepSize: 1 + }, + grid: true + }) + } + } + return this.callParent(arguments) + }, + performLayout: function() { + var F = this, + g = true; + try { + F.animationSuspendCount++; + if (this.callParent() === false) { + g = false; + return + } + F.suspendThicknessChanged(); + var h = F.getSurface("chart").getRect(), + v = F.getInsetPadding(), + G = F.getInnerPadding(), + l = Ext.apply({}, v), + d, s = h[2] - v.left - v.right, + r = h[3] - v.top - v.bottom, + x = [v.left, v.top, s, r], + u = F.getSeries(), + p, t = s - G * 2, + w = r - G * 2, + D = [t * 0.5 + G, w * 0.5 + G], + j = Math.min(t, w) * 0.5, + A = F.getAxes(), + f, a, k, m = [], + o = [], + E = j - G, + z, n, b, q, y, c, C; + F.setMainRect(x); + F.doSetSurfaceRect(F.getSurface(), x); + for (z = 0, n = F.surfaceMap.grid && F.surfaceMap.grid.length; z < n; z++) { + F.doSetSurfaceRect(F.surfaceMap.grid[z], h) + } + for (z = 0, n = A.length; z < n; z++) { + f = A[z]; + switch (f.getPosition()) { + case "angular": + m.push(f); + break; + case "radial": + o.push(f); + break + } + } + for (z = 0, n = m.length; z < n; z++) { + f = m[z]; + q = f.getFloating(); + y = q ? q.value : null; + F.doSetSurfaceRect(f.getSurface(), h); + a = f.getThickness(); + for (d in l) { + l[d] += a + } + s = h[2] - l.left - l.right; + r = h[3] - l.top - l.bottom; + b = Math.min(s, r) * 0.5; + if (z === 0) { + E = b - G + } + f.setMinimum(0); + f.setLength(b); + f.getSprites(); + k = f.sprites[0].attr.lineWidth * 0.5; + for (d in l) { + l[d] += k + } + } + for (z = 0, n = o.length; z < n; z++) { + f = o[z]; + F.doSetSurfaceRect(f.getSurface(), h); + f.setMinimum(0); + f.setLength(E); + f.getSprites() + } + for (z = 0, n = u.length; z < n; z++) { + p = u[z]; + if (p.type === "gauge" && !c) { + c = p + } else { + p.setRadius(E) + } + F.doSetSurfaceRect(p.getSurface(), x) + } + F.doSetSurfaceRect(F.getSurface("overlay"), h); + if (c) { + c.setRect(x); + C = c.getRadius() - G; + F.setRadius(C); + F.setCenter(c.getCenter()); + c.setRadius(C); + if (A.length && A[0].getPosition() === "gauge") { + f = A[0]; + F.doSetSurfaceRect(f.getSurface(), h); + f.setTotalAngle(c.getTotalAngle()); + f.setLength(C) + } + } else { + F.setRadius(j); + F.setCenter(D) + } + F.redraw() + } catch (B) { + throw B + } finally { + F.animationSuspendCount--; + if (g) { + F.resumeThicknessChanged() + } + } + }, + refloatAxes: function() { + var j = this, + g = j.getAxes(), + h = j.getMainRect(), + f, k, b, d, a, c, e; + if (!h) { + return + } + e = 0.5 * Math.min(h[2], h[3]); + for (d = 0, a = g.length; d < a; d++) { + c = g[d]; + f = c.getFloating(); + k = f ? f.value : null; + if (k !== null) { + b = j.getAxis(f.alongAxis); + if (c.getPosition() === "angular") { + if (b) { + k = b.getLength() * k / b.getRange()[1] + } else { + k = 0.01 * k * e + } + c.sprites[0].setAttributes({ + length: k + }, true) + } else { + if (b) { + if (Ext.isString(k)) { + k = b.getCoordFor(k) + } + k = k / (b.getRange()[1] + 1) * Math.PI * 2 - Math.PI * 1.5 + c.getRotation() + } else { + k = Ext.draw.Draw.rad(k) + } + c.sprites[0].setAttributes({ + baseRotation: k + }, true) + } + } + } + }, + redraw: function() { + var f = this, + g = f.getAxes(), + d, c = f.getSeries(), + b, a, e; + for (a = 0, e = g.length; a < e; a++) { + d = g[a]; + d.getSprites() + } + for (a = 0, e = c.length; a < e; a++) { + b = c[a]; + b.getSprites() + } + f.renderFrame(); + f.callParent(arguments) + }, + renderFrame: function() { + this.refloatAxes(); + this.callParent() + } +}); +Ext.define("Ext.chart.SpaceFillingChart", { + extend: "Ext.chart.AbstractChart", + xtype: "spacefilling", + config: {}, + performLayout: function() { + var j = this; + try { + j.animationSuspendCount++; + if (j.callParent() === false) { + return + } + var k = j.getSurface("chart").getRect(), + l = j.getInsetPadding(), + a = k[2] - l.left - l.right, + m = k[3] - l.top - l.bottom, + h = [l.left, l.top, a, m], + b = j.getSeries(), + d, c, g; + j.getSurface().setRect(h); + j.setMainRect(h); + for (c = 0, g = b.length; c < g; c++) { + d = b[c]; + d.getSurface().setRect(h); + if (d.setRect) { + d.setRect(h) + } + d.getOverlaySurface().setRect(k) + } + j.redraw() + } catch (f) { + throw f + } finally { + j.animationSuspendCount-- + } + }, + redraw: function() { + var e = this, + c = e.getSeries(), + b, a, d; + for (a = 0, d = c.length; a < d; a++) { + b = c[a]; + b.getSprites() + } + e.renderFrame(); + e.callParent(arguments) + } +}); +Ext.define("Ext.chart.axis.sprite.Axis3D", { + extend: "Ext.chart.axis.sprite.Axis", + alias: "sprite.axis3d", + type: "axis3d", + inheritableStatics: { + def: { + processors: { + depth: "number" + }, + defaults: { + depth: 0 + }, + triggers: { + depth: "layout" + } + } + }, + config: { + fx: { + customDurations: { + depth: 0 + } + } + }, + layoutUpdater: function() { + var h = this, + f = h.getAxis().getChart(); + if (f.isInitializing) { + return + } + var e = h.attr, + d = h.getLayout(), + c = d.isDiscrete ? 0 : e.depth, + g = f.getInherited().rtl, + b = e.dataMin + (e.dataMax - e.dataMin) * e.visibleMin, + i = e.dataMin + (e.dataMax - e.dataMin) * e.visibleMax, + a = { + attr: e, + segmenter: h.getSegmenter(), + renderer: h.defaultRenderer + }; + if (e.position === "left" || e.position === "right") { + e.translationX = 0; + e.translationY = i * (e.length - c) / (i - b) + c; + e.scalingX = 1; + e.scalingY = (-e.length + c) / (i - b); + e.scalingCenterY = 0; + e.scalingCenterX = 0; + h.applyTransformations(true) + } else { + if (e.position === "top" || e.position === "bottom") { + if (g) { + e.translationX = e.length + b * e.length / (i - b) + 1 + } else { + e.translationX = -b * e.length / (i - b) + } + e.translationY = 0; + e.scalingX = (g ? -1 : 1) * (e.length - c) / (i - b); + e.scalingY = 1; + e.scalingCenterY = 0; + e.scalingCenterX = 0; + h.applyTransformations(true) + } + } + if (d) { + d.calculateLayout(a); + h.setLayoutContext(a) + } + }, + renderAxisLine: function(a, j, f, c) { + var i = this, + h = i.attr, + b = h.lineWidth * 0.5, + f = i.getLayout(), + d = f.isDiscrete ? 0 : h.depth, + k = h.position, + e, g; + if (h.axisLine && h.length) { + switch (k) { + case "left": + e = a.roundPixel(c[2]) - b; + j.moveTo(e, -h.endGap + d); + j.lineTo(e, h.length + h.startGap); + break; + case "right": + j.moveTo(b, -h.endGap); + j.lineTo(b, h.length + h.startGap); + break; + case "bottom": + j.moveTo(-h.startGap, b); + j.lineTo(h.length - d + h.endGap, b); + break; + case "top": + e = a.roundPixel(c[3]) - b; + j.moveTo(-h.startGap, e); + j.lineTo(h.length + h.endGap, e); + break; + case "angular": + j.moveTo(h.centerX + h.length, h.centerY); + j.arc(h.centerX, h.centerY, h.length, 0, Math.PI * 2, true); + break; + case "gauge": + g = i.getGaugeAngles(); + j.moveTo(h.centerX + Math.cos(g.start) * h.length, h.centerY + Math.sin(g.start) * h.length); + j.arc(h.centerX, h.centerY, h.length, g.start, g.end, true); + break + } + } + } +}); +Ext.define("Ext.chart.axis.Axis3D", { + extend: "Ext.chart.axis.Axis", + xtype: "axis3d", + requires: ["Ext.chart.axis.sprite.Axis3D"], + config: { + depth: 0 + }, + onSeriesChange: function(e) { + var g = this, + b = "depthchange", + f = "onSeriesDepthChange", + d, c; + + function a(h) { + var i = g.boundSeries; + for (d = 0; d < i.length; d++) { + c = i[d]; + c[h](b, f, g) + } + } + a("un"); + g.callParent(arguments); + a("on") + }, + onSeriesDepthChange: function(b, f) { + var d = this, + g = f, + e = d.boundSeries, + a, c; + if (f > d.getDepth()) { + g = f + } else { + for (a = 0; a < e.length; a++) { + c = e[a]; + if (c !== b && c.getDepth) { + f = c.getDepth(); + if (f > g) { + g = f + } + } + } + } + d.setDepth(g) + }, + updateDepth: function(d) { + var b = this, + c = b.getSprites(), + a = { + depth: d + }; + if (c && c.length) { + c[0].setAttributes(a) + } + if (b.gridSpriteEven && b.gridSpriteOdd) { + b.gridSpriteEven.getTemplate().setAttributes(a); + b.gridSpriteOdd.getTemplate().setAttributes(a) + } + }, + getGridAlignment: function() { + switch (this.getPosition()) { + case "left": + case "right": + return "horizontal3d"; + case "top": + case "bottom": + return "vertical3d" + } + } +}); +Ext.define("Ext.chart.axis.Category", { + requires: ["Ext.chart.axis.layout.CombineDuplicate", "Ext.chart.axis.segmenter.Names"], + extend: "Ext.chart.axis.Axis", + alias: "axis.category", + type: "category", + config: { + layout: "combineDuplicate", + segmenter: "names" + } +}); +Ext.define("Ext.chart.axis.Category3D", { + requires: ["Ext.chart.axis.layout.CombineDuplicate", "Ext.chart.axis.segmenter.Names"], + extend: "Ext.chart.axis.Axis3D", + alias: "axis.category3d", + type: "category3d", + config: { + layout: "combineDuplicate", + segmenter: "names" + } +}); +Ext.define("Ext.chart.axis.Numeric", { + extend: "Ext.chart.axis.Axis", + type: "numeric", + alias: ["axis.numeric", "axis.radial"], + requires: ["Ext.chart.axis.layout.Continuous", "Ext.chart.axis.segmenter.Numeric"], + config: { + layout: "continuous", + segmenter: "numeric", + aggregator: "double" + } +}); +Ext.define("Ext.chart.axis.Numeric3D", { + extend: "Ext.chart.axis.Axis3D", + alias: ["axis.numeric3d"], + type: "numeric3d", + requires: ["Ext.chart.axis.layout.Continuous", "Ext.chart.axis.segmenter.Numeric"], + config: { + layout: "continuous", + segmenter: "numeric", + aggregator: "double" + } +}); +Ext.define("Ext.chart.axis.Time", { + extend: "Ext.chart.axis.Numeric", + alias: "axis.time", + type: "time", + requires: ["Ext.chart.axis.layout.Continuous", "Ext.chart.axis.segmenter.Time"], + config: { + calculateByLabelSize: true, + dateFormat: null, + fromDate: null, + toDate: null, + step: [Ext.Date.DAY, 1], + layout: "continuous", + segmenter: "time", + aggregator: "time" + }, + updateDateFormat: function(a) { + this.setRenderer(function(c, b) { + return Ext.Date.format(new Date(b), a) + }) + }, + updateFromDate: function(a) { + this.setMinimum(+a) + }, + updateToDate: function(a) { + this.setMaximum(+a) + }, + getCoordFor: function(a) { + if (Ext.isString(a)) { + a = new Date(a) + } + return +a + } +}); +Ext.define("Ext.chart.axis.Time3D", { + extend: "Ext.chart.axis.Numeric3D", + alias: "axis.time3d", + type: "time3d", + requires: ["Ext.chart.axis.layout.Continuous", "Ext.chart.axis.segmenter.Time"], + config: { + calculateByLabelSize: true, + dateFormat: null, + fromDate: null, + toDate: null, + step: [Ext.Date.DAY, 1], + layout: "continuous", + segmenter: "time", + aggregator: "time" + }, + updateDateFormat: function(a) { + this.setRenderer(function(c, b) { + return Ext.Date.format(new Date(b), a) + }) + }, + updateFromDate: function(a) { + this.setMinimum(+a) + }, + updateToDate: function(a) { + this.setMaximum(+a) + }, + getCoordFor: function(a) { + if (Ext.isString(a)) { + a = new Date(a) + } + return +a + } +}); +Ext.define("Ext.chart.grid.HorizontalGrid3D", { + extend: "Ext.chart.grid.HorizontalGrid", + alias: "grid.horizontal3d", + inheritableStatics: { + def: { + processors: { + depth: "number" + }, + defaults: { + depth: 0 + } + } + }, + render: function(a, k, d) { + var f = this.attr, + i = a.roundPixel(f.x), + h = a.roundPixel(f.y), + l = a.matrix.getDX(), + c = k.lineWidth * 0.5, + j = f.height, + e = f.depth, + b, g; + if (h <= d[1]) { + return + } + b = d[0] + e - l; + g = h + c - e; + k.beginPath(); + k.rect(b, g, d[2], j); + k.fill(); + k.beginPath(); + k.moveTo(b, g); + k.lineTo(b + d[2], g); + k.stroke(); + b = d[0] + i - l; + g = h + c; + k.beginPath(); + k.moveTo(b, g); + k.lineTo(b + e, g - e); + k.lineTo(b + e, g - e + j); + k.lineTo(b, g + j); + k.closePath(); + k.fill(); + k.beginPath(); + k.moveTo(b, g); + k.lineTo(b + e, g - e); + k.stroke() + } +}); +Ext.define("Ext.chart.grid.VerticalGrid3D", { + extend: "Ext.chart.grid.VerticalGrid", + alias: "grid.vertical3d", + inheritableStatics: { + def: { + processors: { + depth: "number" + }, + defaults: { + depth: 0 + } + } + }, + render_: function(c, d, f) { + var b = this.attr, + a = c.roundPixel(b.x), + e = d.lineWidth * 0.5; + d.beginPath(); + d.rect(a - e, f[1] - c.matrix.getDY(), b.width, f[3]); + d.fill(); + d.beginPath(); + d.moveTo(a - e, f[1] - c.matrix.getDY()); + d.lineTo(a - e, f[1] + f[3] - c.matrix.getDY()); + d.stroke() + }, + render: function(b, j, e) { + var g = this.attr, + i = b.roundPixel(g.x), + k = b.matrix.getDY(), + d = j.lineWidth * 0.5, + a = g.width, + f = g.depth, + c, h; + if (i >= e[2]) { + return + } + c = i - d + f; + h = e[1] - f - k; + j.beginPath(); + j.rect(c, h, a, e[3]); + j.fill(); + j.beginPath(); + j.moveTo(c, h); + j.lineTo(c, h + e[3]); + j.stroke(); + c = i - d; + h = e[3]; + j.beginPath(); + j.moveTo(c, h); + j.lineTo(c + f, h - f); + j.lineTo(c + f + a, h - f); + j.lineTo(c + a, h); + j.closePath(); + j.fill(); + c = i - d; + h = e[3]; + j.beginPath(); + j.moveTo(c, h); + j.lineTo(c + f, h - f); + j.stroke() + } +}); +Ext.define("Ext.chart.interactions.CrossZoom", { + extend: "Ext.chart.interactions.Abstract", + type: "crosszoom", + alias: "interaction.crosszoom", + isCrossZoom: true, + config: { + axes: true, + gestures: { + dragstart: "onGestureStart", + drag: "onGesture", + dragend: "onGestureEnd", + dblclick: "onDoubleTap" + }, + undoButton: {} + }, + stopAnimationBeforeSync: false, + zoomAnimationInProgress: false, + constructor: function() { + this.callParent(arguments); + this.zoomHistory = [] + }, + applyAxes: function(b) { + var a = {}; + if (b === true) { + return { + top: {}, + right: {}, + bottom: {}, + left: {} + } + } else { + if (Ext.isArray(b)) { + a = {}; + Ext.each(b, function(c) { + a[c] = {} + }) + } else { + if (Ext.isObject(b)) { + Ext.iterate(b, function(c, d) { + if (d === true) { + a[c] = {} + } else { + if (d !== false) { + a[c] = d + } + } + }) + } + } + } + return a + }, + applyUndoButton: function(b, a) { + var c = this; + if (a) { + a.destroy() + } + if (b) { + return Ext.create("Ext.Button", Ext.apply({ + cls: [], + text: "Undo Zoom", + disabled: true, + handler: function() { + c.undoZoom() + } + }, b)) + } + }, + getSurface: function() { + return this.getChart() && this.getChart().getSurface("main") + }, + setSeriesOpacity: function(b) { + var a = this.getChart() && this.getChart().getSurface("series"); + if (a) { + a.element.setStyle("opacity", b) + } + }, + onGestureStart: function(h) { + var j = this, + i = j.getChart(), + d = j.getSurface(), + l = i.getInnerRect(), + c = i.getInnerPadding(), + g = c.left, + b = g + l[2], + f = c.top, + a = f + l[3], + n = i.getEventXY(h), + m = n[0], + k = n[1]; + if (j.zoomAnimationInProgress) { + return + } + if (m > g && m < b && k > f && k < a) { + j.gestureEvent = "drag"; + j.lockEvents(j.gestureEvent); + j.startX = m; + j.startY = k; + j.selectionRect = d.add({ + type: "rect", + globalAlpha: 0.5, + fillStyle: "rgba(80,80,140,0.5)", + strokeStyle: "rgba(80,80,140,1)", + lineWidth: 2, + x: m, + y: k, + width: 0, + height: 0, + zIndex: 10000 + }); + j.setSeriesOpacity(0.8); + return false + } + }, + onGesture: function(h) { + var j = this; + if (j.zoomAnimationInProgress) { + return + } + if (j.getLocks()[j.gestureEvent] === j) { + var i = j.getChart(), + d = j.getSurface(), + l = i.getInnerRect(), + c = i.getInnerPadding(), + g = c.left, + b = g + l[2], + f = c.top, + a = f + l[3], + n = i.getEventXY(h), + m = n[0], + k = n[1]; + if (m < g) { + m = g + } else { + if (m > b) { + m = b + } + } + if (k < f) { + k = f + } else { + if (k > a) { + k = a + } + } + j.selectionRect.setAttributes({ + width: m - j.startX, + height: k - j.startY + }); + if (Math.abs(j.startX - m) < 11 || Math.abs(j.startY - k) < 11) { + j.selectionRect.setAttributes({ + globalAlpha: 0.5 + }) + } else { + j.selectionRect.setAttributes({ + globalAlpha: 1 + }) + } + d.renderFrame(); + return false + } + }, + onGestureEnd: function(i) { + var l = this; + if (l.zoomAnimationInProgress) { + return + } + if (l.getLocks()[l.gestureEvent] === l) { + var k = l.getChart(), + d = l.getSurface(), + n = k.getInnerRect(), + c = k.getInnerPadding(), + g = c.left, + b = g + n[2], + f = c.top, + a = f + n[3], + h = n[2], + j = n[3], + p = k.getEventXY(i), + o = p[0], + m = p[1]; + if (o < g) { + o = g + } else { + if (o > b) { + o = b + } + } + if (m < f) { + m = f + } else { + if (m > a) { + m = a + } + } + if (Math.abs(l.startX - o) < 11 || Math.abs(l.startY - m) < 11) { + d.remove(l.selectionRect) + } else { + l.zoomBy([Math.min(l.startX, o) / h, 1 - Math.max(l.startY, m) / j, Math.max(l.startX, o) / h, 1 - Math.min(l.startY, m) / j]); + l.selectionRect.setAttributes({ + x: Math.min(l.startX, o), + y: Math.min(l.startY, m), + width: Math.abs(l.startX - o), + height: Math.abs(l.startY - m) + }); + l.selectionRect.setAnimation(k.getAnimation() || { + duration: 0 + }); + l.selectionRect.setAttributes({ + globalAlpha: 0, + x: 0, + y: 0, + width: h, + height: j + }); + l.zoomAnimationInProgress = true; + k.suspendThicknessChanged(); + l.selectionRect.fx.on("animationend", function() { + k.resumeThicknessChanged(); + d.remove(l.selectionRect); + l.selectionRect = null; + l.zoomAnimationInProgress = false + }) + } + d.renderFrame(); + l.sync(); + l.unlockEvents(l.gestureEvent); + l.setSeriesOpacity(1); + if (!l.zoomAnimationInProgress) { + d.remove(l.selectionRect); + l.selectionRect = null + } + } + }, + zoomBy: function(o) { + var n = this, + a = n.getAxes(), + k = n.getChart(), + j = k.getAxes(), + l = k.getInherited().rtl, + f, d = {}, + c, b; + if (l) { + o = o.slice(); + c = 1 - o[0]; + b = 1 - o[2]; + o[0] = Math.min(c, b); + o[2] = Math.max(c, b) + } + for (var h = 0; h < j.length; h++) { + var g = j[h]; + f = a[g.getPosition()]; + if (f && f.allowZoom !== false) { + var e = g.isSide(), + m = g.getVisibleRange(); + d[g.getId()] = m.slice(0); + if (!e) { + g.setVisibleRange([(m[1] - m[0]) * o[0] + m[0], (m[1] - m[0]) * o[2] + m[0]]) + } else { + g.setVisibleRange([(m[1] - m[0]) * o[1] + m[0], (m[1] - m[0]) * o[3] + m[0]]) + } + } + } + n.zoomHistory.push(d); + n.getUndoButton().setDisabled(false) + }, + undoZoom: function() { + var c = this.zoomHistory.pop(), + d = this.getChart().getAxes(); + if (c) { + for (var a = 0; a < d.length; a++) { + var b = d[a]; + if (c[b.getId()]) { + b.setVisibleRange(c[b.getId()]) + } + } + } + this.getUndoButton().setDisabled(this.zoomHistory.length === 0); + this.sync() + }, + onDoubleTap: function(a) { + this.undoZoom() + }, + destroy: function() { + this.setUndoButton(null); + this.callParent(arguments) + } +}); +Ext.define("Ext.chart.interactions.Crosshair", { + extend: "Ext.chart.interactions.Abstract", + requires: ["Ext.chart.grid.HorizontalGrid", "Ext.chart.grid.VerticalGrid", "Ext.chart.CartesianChart", "Ext.chart.axis.layout.Discrete"], + type: "crosshair", + alias: "interaction.crosshair", + config: { + axes: { + top: { + label: {}, + rect: {} + }, + right: { + label: {}, + rect: {} + }, + bottom: { + label: {}, + rect: {} + }, + left: { + label: {}, + rect: {} + } + }, + lines: { + horizontal: { + strokeStyle: "black", + lineDash: [5, 5] + }, + vertical: { + strokeStyle: "black", + lineDash: [5, 5] + } + }, + gesture: "drag" + }, + applyAxes: function(b, a) { + return Ext.merge(a || {}, b) + }, + applyLines: function(a, b) { + return Ext.merge(b || {}, a) + }, + updateChart: function(a) { + if (a && !a.isCartesian) { + Ext.raise("Crosshair interaction can only be used on cartesian charts.") + } + this.callParent(arguments) + }, + getGestures: function() { + var a = this, + b = {}; + b[a.getGesture()] = "onGesture"; + b[a.getGesture() + "start"] = "onGestureStart"; + b[a.getGesture() + "end"] = "onGestureEnd"; + return b + }, + onGestureStart: function(N) { + var m = this, + O = m.getChart(), + B = O.getTheme().getAxis(), + A, F = O.getSurface("overlay"), + s = O.getInnerRect(), + n = s[2], + M = s[3], + r = O.getEventXY(N), + D = r[0], + C = r[1], + E = O.getAxes(), + u = m.getAxes(), + h = m.getLines(), + q, v, b, d, k, z, G, L, J, o, I, w, l, f, p, j, t, a, g, H, c, K; + if (D > 0 && D < n && C > 0 && C < M) { + m.lockEvents(m.getGesture()); + H = Ext.apply({ + xclass: "Ext.chart.grid.HorizontalGrid", + x: 0, + y: C, + width: n + }, h.horizontal); + c = Ext.apply({ + xclass: "Ext.chart.grid.VerticalGrid", + x: D, + y: 0, + height: M + }, h.vertical); + m.axesLabels = m.axesLabels || {}; + for (K = 0; K < E.length; K++) { + q = E[K]; + v = q.getSurface(); + b = v.getRect(); + w = q.getSprites()[0]; + d = b[2]; + k = b[3]; + z = q.getPosition(); + G = q.getAlignment(); + t = q.getTitle(); + a = t && t.attr.text !== "" && t.getBBox(); + l = w.attr; + f = w.thickness; + p = l.axisLine ? l.lineWidth : 0; + j = p / 2; + I = Math.max(l.majorTickSize, l.minorTickSize) + p; + L = m.axesLabels[z] = v.add({ + type: "composite" + }); + L.labelRect = L.add(Ext.apply({ + type: "rect", + fillStyle: "white", + x: z === "right" ? p : 0, + y: z === "bottom" ? p : 0, + width: d - p - (G === "vertical" && a ? a.width : 0), + height: k - p - (G === "horizontal" && a ? a.height : 0), + translationX: z === "left" && a ? a.width : 0, + translationY: z === "top" && a ? a.height : 0 + }, u.rect || u[z].rect)); + if (G === "vertical" && !c.strokeStyle) { + c.strokeStyle = l.strokeStyle + } + if (G === "horizontal" && !H.strokeStyle) { + H.strokeStyle = l.strokeStyle + } + A = Ext.merge({}, B.defaults, B[z]); + J = Ext.apply({}, q.config.label, A.label); + o = u.label || u[z].label; + L.labelText = L.add(Ext.apply(J, o, { + type: "text", + x: (function() { + switch (z) { + case "left": + g = a ? a.x + a.width : 0; + return g + (d - g - I) / 2 - j; + case "right": + g = a ? d - a.x : 0; + return I + (d - I - g) / 2 + j; + default: + return 0 + } + })(), + y: (function() { + switch (z) { + case "top": + g = a ? a.y + a.height : 0; + return g + (k - g - I) / 2 - j; + case "bottom": + g = a ? k - a.y : 0; + return I + (k - I - g) / 2 + j; + default: + return 0 + } + })() + })) + } + m.horizontalLine = F.add(H); + m.verticalLine = F.add(c); + return false + } + }, + onGesture: function(G) { + var K = this; + if (K.getLocks()[K.getGesture()] !== K) { + return + } + var u = K.getChart(), + z = u.getSurface("overlay"), + a = Ext.Array.slice(u.getInnerRect()), + r = u.getInnerPadding(), + t = r.left, + q = r.top, + E = a[2], + f = a[3], + d = u.getEventXY(G), + k = d[0], + j = d[1], + D = u.getAxes(), + c, h, m, p, J, w, I, H, s, b, C, g, v, n, l, A, F, o, B; + if (k < 0) { + k = 0 + } else { + if (k > E) { + k = E + } + } + if (j < 0) { + j = 0 + } else { + if (j > f) { + j = f + } + } + k += t; + j += q; + for (B = 0; B < D.length; B++) { + c = D[B]; + h = c.getPosition(); + m = c.getAlignment(); + p = c.getSurface(); + J = c.getSprites()[0]; + w = J.attr.matrix; + C = J.attr.textPadding * 2; + s = K.axesLabels[h]; + I = J.getLayoutContext(); + H = c.getSegmenter(); + if (s) { + if (m === "vertical") { + v = w.getYY(); + l = w.getDY(); + F = (j - l - q) / v; + if (c.getLayout() instanceof Ext.chart.axis.layout.Discrete) { + j = Math.round(F) * v + l + q; + F = H.from(Math.round(F)); + F = J.attr.data[F] + } else { + F = H.from(F) + } + o = H.renderer(F, I); + s.setAttributes({ + translationY: j - q + }); + s.labelText.setAttributes({ + text: o + }); + b = s.labelText.getBBox(); + s.labelRect.setAttributes({ + height: b.height + C, + y: -(b.height + C) / 2 + }); + p.renderFrame() + } else { + g = w.getXX(); + n = w.getDX(); + A = (k - n - t) / g; + if (c.getLayout() instanceof Ext.chart.axis.layout.Discrete) { + k = Math.round(A) * g + n + t; + A = H.from(Math.round(A)); + A = J.attr.data[A] + } else { + A = H.from(A) + } + o = H.renderer(A, I); + s.setAttributes({ + translationX: k - t + }); + s.labelText.setAttributes({ + text: o + }); + b = s.labelText.getBBox(); + s.labelRect.setAttributes({ + width: b.width + C, + x: -(b.width + C) / 2 + }); + p.renderFrame() + } + } + } + K.horizontalLine.setAttributes({ + y: j, + strokeStyle: J.attr.strokeStyle + }); + K.verticalLine.setAttributes({ + x: k, + strokeStyle: J.attr.strokeStyle + }); + z.renderFrame(); + return false + }, + onGestureEnd: function(h) { + var l = this, + k = l.getChart(), + a = k.getSurface("overlay"), + j = k.getAxes(), + c, g, d, b, f; + a.remove(l.verticalLine); + a.remove(l.horizontalLine); + for (f = 0; f < j.length; f++) { + c = j[f]; + g = c.getPosition(); + d = c.getSurface(); + b = l.axesLabels[g]; + if (b) { + delete l.axesLabels[g]; + d.remove(b) + } + d.renderFrame() + } + a.renderFrame(); + l.unlockEvents(l.getGesture()) + } +}); +Ext.define("Ext.chart.interactions.ItemHighlight", { + extend: "Ext.chart.interactions.Abstract", + type: "itemhighlight", + alias: "interaction.itemhighlight", + isItemHighlight: true, + config: { + gestures: { + tap: "onTapGesture", + mousemove: "onMouseMoveGesture", + mousedown: "onMouseDownGesture", + mouseup: "onMouseUpGesture", + mouseleave: "onMouseUpGesture" + }, + sticky: false + }, + stickyHighlightItem: null, + onMouseMoveGesture: function(g) { + var d = this, + h = d.tipItem, + a = g.pointerType === "mouse", + c, f, b; + if (d.getSticky()) { + return true + } + if (d.isDragging) { + if (h && a) { + h.series.hideTooltip(h); + d.tipItem = null + } + } else { + if (!d.stickyHighlightItem) { + c = d.getItemForEvent(g); + b = d.getChart(); + if (c !== b.getHighlightItem()) { + d.highlight(c); + d.sync() + } + if (a) { + if (h && (!c || h.field !== c.field || h.record !== c.record)) { + h.series.hideTooltip(h); + d.tipItem = h = null + } + if (c && (f = c.series.getTooltip())) { + if (f.trackMouse || !h) { + c.series.showTooltip(c, g.getXY()) + } + d.tipItem = c + } + } + return false + } + } + }, + highlight: function(a) { + this.getChart().setHighlightItem(a) + }, + showTooltip: function(b, a) { + a.series.showTooltip(a, b.getXY()); + this.tipItem = a + }, + onMouseDownGesture: function() { + this.isDragging = true + }, + onMouseUpGesture: function() { + this.isDragging = false + }, + onTapGesture: function(c) { + var b = this; + if (c.pointerType === "mouse" && !b.getSticky()) { + return + } + var a = b.getItemForEvent(c); + if (b.stickyHighlightItem && a && (b.stickyHighlightItem.index === a.index)) { + a = null + } + b.stickyHighlightItem = a; + b.highlight(a) + } +}); +Ext.define("Ext.chart.interactions.ItemEdit", { + extend: "Ext.chart.interactions.ItemHighlight", + requires: ["Ext.tip.ToolTip"], + type: "itemedit", + alias: "interaction.itemedit", + isItemEdit: true, + config: { + style: null, + renderer: null, + tooltip: true, + gestures: { + dragstart: "onDragStart", + drag: "onDrag", + dragend: "onDragEnd" + }, + cursors: { + ewResize: "ew-resize", + nsResize: "ns-resize", + move: "move" + } + }, + item: null, + applyTooltip: function(b) { + if (b) { + var a = Ext.apply({}, b, { + renderer: this.defaultTooltipRenderer, + constrainPosition: true, + shrinkWrapDock: true, + autoHide: true, + offsetX: 10, + offsetY: 10 + }); + b = new Ext.tip.ToolTip(a) + } + return b + }, + defaultTooltipRenderer: function(b, a, f, d) { + var c = []; + if (f.xField) { + c.push(f.xField + ": " + f.xValue) + } + if (f.yField) { + c.push(f.yField + ": " + f.yValue) + } + b.setHtml(c.join("
")) + }, + onDragStart: function(d) { + var c = this, + a = c.getChart(), + b = a.getHighlightItem(); + if (b) { + a.fireEvent("beginitemedit", a, c, c.item = b); + return false + } + }, + onDrag: function(f) { + var d = this, + b = d.getChart(), + c = b.getHighlightItem(), + a = c && c.sprite.type; + if (c) { + switch (a) { + case "barSeries": + return d.onDragBar(f); + break; + case "scatterSeries": + return d.onDragScatter(f); + break + } + } + }, + highlight: function(f) { + var e = this, + d = e.getChart(), + a = d.getFlipXY(), + g = e.getCursors(), + c = f && f.sprite.type, + b = d.el.dom.style; + e.callParent([f]); + if (f) { + switch (c) { + case "barSeries": + if (a) { + b.cursor = g.ewResize + } else { + b.cursor = g.nsResize + } + break; + case "scatterSeries": + b.cursor = g.move; + break + } + } else { + d.el.dom.style.cursor = "default" + } + }, + onDragBar: function(i) { + var m = this, + k = m.getChart(), + l = k.getInherited().rtl, + f = k.isCartesian && k.getFlipXY(), + q = k.getHighlightItem(), + g = q.sprite.getMarker("items"), + p = g.getMarkerFor(q.sprite.getId(), q.index), + b = q.sprite.getSurface(), + c = b.getRect(), + r = b.getEventXY(i), + o = q.sprite.attr.matrix, + j = m.getRenderer(), + a, n, d, h; + if (f) { + h = l ? c[2] - r[0] : r[0] + } else { + h = c[3] - r[1] + } + a = { + x: p.x, + y: h, + width: p.width, + height: p.height + (p.y - h), + radius: p.radius, + fillStyle: "none", + lineDash: [4, 4], + zIndex: 100 + }; + Ext.apply(a, m.getStyle()); + if (Ext.isArray(q.series.getYField())) { + h = h - p.y - p.height + } + m.target = { + index: q.index, + yField: q.field, + yValue: (h - o.getDY()) / o.getYY() + }; + d = [k, { + target: m.target, + style: a, + item: q + }]; + n = Ext.callback(j, null, d, 0, k); + if (n) { + Ext.apply(a, n) + } + q.sprite.putMarker("items", a, "itemedit"); + m.showTooltip(i, m.target, q); + b.renderFrame() + }, + onDragScatter: function(n) { + var t = this, + g = t.getChart(), + d = g.getInherited().rtl, + l = g.isCartesian && g.getFlipXY(), + o = g.getHighlightItem(), + b = o.sprite.getMarker("items"), + p = b.getMarkerFor(o.sprite.getId(), o.index), + j = o.sprite.getSurface(), + h = j.getRect(), + a = j.getEventXY(n), + k = o.sprite.attr.matrix, + c = o.series.getXAxis(), + f = c && c.getLayout().isContinuous, + i = t.getRenderer(), + m, u, q, s, r; + if (l) { + r = d ? h[2] - a[0] : a[0] + } else { + r = h[3] - a[1] + } + if (f) { + if (l) { + s = h[3] - a[1] + } else { + s = a[0] + } + } else { + s = p.translationX + } + m = { + translationX: s, + translationY: r, + scalingX: p.scalingX, + scalingY: p.scalingY, + r: p.r, + fillStyle: "none", + lineDash: [4, 4], + zIndex: 100 + }; + Ext.apply(m, t.getStyle()); + t.target = { + index: o.index, + yField: o.field, + yValue: (r - k.getDY()) / k.getYY() + }; + if (f) { + Ext.apply(t.target, { + xField: o.series.getXField(), + xValue: (s - k.getDX()) / k.getXX() + }) + } + q = [g, { + target: t.target, + style: m, + item: o + }]; + u = Ext.callback(i, null, q, 0, g); + if (u) { + Ext.apply(m, u) + } + o.sprite.putMarker("items", m, "itemedit"); + t.showTooltip(n, t.target, o); + j.renderFrame() + }, + showTooltip: function(g, f, c) { + var d = this.getTooltip(), + a, b; + if (d && Ext.toolkit !== "modern") { + a = d.config; + b = this.getChart(); + Ext.callback(a.renderer, null, [d, c, f, g], 0, b); + d.show([g.x + a.offsetX, g.y + a.offsetY]) + } + }, + hideTooltip: function() { + var a = this.getTooltip(); + if (a && Ext.toolkit !== "modern") { + a.hide() + } + }, + onDragEnd: function(g) { + var d = this, + f = d.target, + c = d.getChart(), + b = c.getStore(), + a; + if (f) { + a = b.getAt(f.index); + if (f.yField) { + a.set(f.yField, f.yValue, { + convert: false + }) + } + if (f.xField) { + a.set(f.xField, f.xValue, { + convert: false + }) + } + if (f.yField || f.xField) { + d.getChart().onDataChanged() + } + d.target = null + } + d.hideTooltip(); + if (d.item) { + c.fireEvent("enditemedit", c, d, d.item, f) + } + d.highlight(d.item = null) + }, + destroy: function() { + var a = this.getConfig("tooltip", true); + Ext.destroy(a); + this.callParent() + } +}); +Ext.define("Ext.chart.interactions.PanZoom", { + extend: "Ext.chart.interactions.Abstract", + type: "panzoom", + alias: "interaction.panzoom", + requires: ["Ext.draw.Animator"], + config: { + axes: { + top: {}, + right: {}, + bottom: {}, + left: {} + }, + minZoom: null, + maxZoom: null, + showOverflowArrows: true, + panGesture: "drag", + zoomGesture: "pinch", + zoomOnPanGesture: false, + modeToggleButton: { + xtype: "segmentedbutton", + width: 200, + defaults: { + ui: "default-toolbar" + }, + cls: Ext.baseCSSPrefix + "panzoom-toggle", + items: [{ + text: "Pan" + }, { + text: "Zoom" + }] + }, + hideLabelInGesture: false + }, + stopAnimationBeforeSync: true, + applyAxes: function(b, a) { + return Ext.merge(a || {}, b) + }, + applyZoomOnPanGesture: function(a) { + this.getChart(); + if (this.isMultiTouch()) { + return false + } + return a + }, + updateZoomOnPanGesture: function(b) { + var a = this.getModeToggleButton(); + if (!this.isMultiTouch()) { + a.show(); + a.setValue(b ? 1 : 0) + } else { + a.hide() + } + }, + toggleMode: function() { + var a = this; + if (!a.isMultiTouch()) { + a.setZoomOnPanGesture(!a.getZoomOnPanGesture()) + } + }, + applyModeToggleButton: function(c, b) { + var d = this, + a = Ext.factory(c, "Ext.button.Segmented", b); + if (!a && b) { + b.destroy() + } + if (a && !b) { + a.addListener("toggle", function(e) { + d.setZoomOnPanGesture(e.getValue() === 1) + }) + } + return a + }, + getGestures: function() { + var c = this, + e = {}, + d = c.getPanGesture(), + b = c.getZoomGesture(), + a = Ext.supports.Touch; + e[b] = "onZoomGestureMove"; + e[b + "start"] = "onZoomGestureStart"; + e[b + "end"] = "onZoomGestureEnd"; + e[d] = "onPanGestureMove"; + e[d + "start"] = "onPanGestureStart"; + e[d + "end"] = "onPanGestureEnd"; + e.doubletap = "onDoubleTap"; + return e + }, + onDoubleTap: function(h) { + var f = this, + c = f.getChart(), + g = c.getAxes(), + b, a, d; + for (a = 0, d = g.length; a < d; a++) { + b = g[a]; + b.setVisibleRange([0, 1]) + } + c.redraw() + }, + onPanGestureStart: function(d) { + if (!d || !d.touches || d.touches.length < 2) { + var b = this, + a = b.getChart().getInnerRect(), + c = b.getChart().element.getXY(); + b.startX = d.getX() - c[0] - a[0]; + b.startY = d.getY() - c[1] - a[1]; + b.oldVisibleRanges = null; + b.hideLabels(); + b.getChart().suspendThicknessChanged(); + b.lockEvents(b.getPanGesture()); + return false + } + }, + onPanGestureMove: function(d) { + var b = this; + if (b.getLocks()[b.getPanGesture()] === b) { + var a = b.getChart().getInnerRect(), + c = b.getChart().element.getXY(); + if (b.getZoomOnPanGesture()) { + b.transformAxesBy(b.getZoomableAxes(d), 0, 0, (d.getX() - c[0] - a[0]) / b.startX, b.startY / (d.getY() - c[1] - a[1])) + } else { + b.transformAxesBy(b.getPannableAxes(d), d.getX() - c[0] - a[0] - b.startX, d.getY() - c[1] - a[1] - b.startY, 1, 1) + } + b.sync(); + return false + } + }, + onPanGestureEnd: function(b) { + var a = this, + c = a.getPanGesture(); + if (a.getLocks()[c] === a) { + a.getChart().resumeThicknessChanged(); + a.showLabels(); + a.sync(); + a.unlockEvents(c); + return false + } + }, + onZoomGestureStart: function(b) { + if (b.touches && b.touches.length === 2) { + var c = this, + i = c.getChart().element.getXY(), + f = c.getChart().getInnerRect(), + h = i[0] + f[0], + d = i[1] + f[1], + j = [b.touches[0].point.x - h, b.touches[0].point.y - d, b.touches[1].point.x - h, b.touches[1].point.y - d], + g = Math.max(44, Math.abs(j[2] - j[0])), + a = Math.max(44, Math.abs(j[3] - j[1])); + c.getChart().suspendThicknessChanged(); + c.lastZoomDistances = [g, a]; + c.lastPoints = j; + c.oldVisibleRanges = null; + c.hideLabels(); + c.lockEvents(c.getZoomGesture()); + return false + } + }, + onZoomGestureMove: function(d) { + var f = this; + if (f.getLocks()[f.getZoomGesture()] === f) { + var i = f.getChart().getInnerRect(), + n = f.getChart().element.getXY(), + k = n[0] + i[0], + h = n[1] + i[1], + o = Math.abs, + c = f.lastPoints, + m = [d.touches[0].point.x - k, d.touches[0].point.y - h, d.touches[1].point.x - k, d.touches[1].point.y - h], + g = Math.max(44, o(m[2] - m[0])), + b = Math.max(44, o(m[3] - m[1])), + a = this.lastZoomDistances || [g, b], + l = g / a[0], + j = b / a[1]; + f.transformAxesBy(f.getZoomableAxes(d), i[2] * (l - 1) / 2 + m[2] - c[2] * l, i[3] * (j - 1) / 2 + m[3] - c[3] * j, l, j); + f.sync(); + return false + } + }, + onZoomGestureEnd: function(c) { + var b = this, + a = b.getZoomGesture(); + if (b.getLocks()[a] === b) { + b.getChart().resumeThicknessChanged(); + b.showLabels(); + b.sync(); + b.unlockEvents(a); + return false + } + }, + hideLabels: function() { + if (this.getHideLabelInGesture()) { + this.eachInteractiveAxes(function(a) { + a.hideLabels() + }) + } + }, + showLabels: function() { + if (this.getHideLabelInGesture()) { + this.eachInteractiveAxes(function(a) { + a.showLabels() + }) + } + }, + isEventOnAxis: function(c, a) { + var b = a.getSurface().getRect(); + return b[0] <= c.getX() && c.getX() <= b[0] + b[2] && b[1] <= c.getY() && c.getY() <= b[1] + b[3] + }, + getPannableAxes: function(d) { + var h = this, + a = h.getAxes(), + f = h.getChart().getAxes(), + c, g = f.length, + k = [], + j = false, + b; + if (d) { + for (c = 0; c < g; c++) { + if (this.isEventOnAxis(d, f[c])) { + j = true; + break + } + } + } + for (c = 0; c < g; c++) { + b = a[f[c].getPosition()]; + if (b && b.allowPan !== false && (!j || this.isEventOnAxis(d, f[c]))) { + k.push(f[c]) + } + } + return k + }, + getZoomableAxes: function(f) { + var j = this, + a = j.getAxes(), + g = j.getChart().getAxes(), + l = [], + d, h = g.length, + c, k = false, + b; + if (f) { + for (d = 0; d < h; d++) { + if (this.isEventOnAxis(f, g[d])) { + k = true; + break + } + } + } + for (d = 0; d < h; d++) { + c = g[d]; + b = a[c.getPosition()]; + if (b && b.allowZoom !== false && (!k || this.isEventOnAxis(f, c))) { + l.push(c) + } + } + return l + }, + eachInteractiveAxes: function(c) { + var d = this, + b = d.getAxes(), + e = d.getChart().getAxes(); + for (var a = 0; a < e.length; a++) { + if (b[e[a].getPosition()]) { + if (false === c.call(this, e[a])) { + return + } + } + } + }, + transformAxesBy: function(d, j, g, h, e) { + var f = this.getChart().getInnerRect(), + a = this.getAxes(), + k, b = this.oldVisibleRanges, + l = false; + if (!b) { + this.oldVisibleRanges = b = {}; + this.eachInteractiveAxes(function(i) { + b[i.getId()] = i.getVisibleRange() + }) + } + if (!f) { + return + } + for (var c = 0; c < d.length; c++) { + k = a[d[c].getPosition()]; + l = this.transformAxisBy(d[c], b[d[c].getId()], j, g, h, e, this.minZoom || k.minZoom, this.maxZoom || k.maxZoom) || l + } + return l + }, + transformAxisBy: function(c, o, r, q, k, i, h, m) { + var s = this, + b = o[1] - o[0], + l = c.getVisibleRange(), + g = h || s.getMinZoom() || c.config.minZoom, + j = m || s.getMaxZoom() || c.config.maxZoom, + a = s.getChart().getInnerRect(), + f, p; + if (!a) { + return + } + var d = c.isSide(), + e = d ? a[3] : a[2], + n = d ? -q : r; + b /= d ? i : k; + if (b < 0) { + b = -b + } + if (b * g > 1) { + b = 1 + } + if (b * j < 1) { + b = 1 / j + } + f = o[0]; + p = o[1]; + l = l[1] - l[0]; + if (b === l && l === 1) { + return + } + c.setVisibleRange([(o[0] + o[1] - b) * 0.5 - n / e * b, (o[0] + o[1] + b) * 0.5 - n / e * b]); + return (Math.abs(f - c.getVisibleRange()[0]) > 1e-10 || Math.abs(p - c.getVisibleRange()[1]) > 1e-10) + }, + destroy: function() { + this.setModeToggleButton(null); + this.callParent() + } +}); +Ext.define("Ext.chart.interactions.Rotate", { + extend: "Ext.chart.interactions.Abstract", + type: "rotate", + alias: "interaction.rotate", + config: { + gesture: "rotate", + gestures: { + rotate: "onRotate", + rotateend: "onRotate", + dragstart: "onGestureStart", + drag: "onGesture", + dragend: "onGestureEnd" + }, + rotation: 0 + }, + oldRotations: null, + getAngle: function(f) { + var c = this, + b = c.getChart(), + d = b.getEventXY(f), + a = b.getCenter(); + return Math.atan2(d[1] - a[1], d[0] - a[0]) + }, + getRadius: function(a) { + return this.getChart().getRadius() + }, + getEventRadius: function(h) { + var f = this, + d = f.getChart(), + g = d.getEventXY(h), + a = d.getCenter(), + c = g[0] - a[0], + b = g[1] - a[1]; + return Math.sqrt(c * c + b * b) + }, + onGestureStart: function(d) { + var c = this, + b = c.getRadius(d), + a = c.getEventRadius(d); + if (b >= a) { + c.lockEvents("drag"); + c.angle = c.getAngle(d); + c.oldRotations = {}; + return false + } + }, + onGesture: function(b) { + var a = this, + c = a.getAngle(b) - a.angle; + if (a.getLocks().drag === a) { + a.doRotateTo(c, true); + return false + } + }, + doRotateTo: function(d, a, b) { + var n = this, + l = n.getChart(), + k = l.getAxes(), + f = l.getSeries(), + m = n.oldRotations, + c, j, g, e, h; + if (!b) { + l.suspendAnimation() + } + for (e = 0, h = k.length; e < h; e++) { + c = k[e]; + g = m[c.getId()] || (m[c.getId()] = c.getRotation()); + c.setRotation(d + (a ? g : 0)) + } + for (e = 0, h = f.length; e < h; e++) { + j = f[e]; + g = m[j.getId()] || (m[j.getId()] = j.getRotation()); + j.setRotation(d + (a ? g : 0)) + } + n.setRotation(d + (a ? g : 0)); + n.fireEvent("rotate", n, n.getRotation()); + n.sync(); + if (!b) { + l.resumeAnimation() + } + }, + rotateTo: function(c, b, a) { + this.doRotateTo(c, b, a); + this.oldRotations = {} + }, + onGestureEnd: function(b) { + var a = this; + if (a.getLocks().drag === a) { + a.onGesture(b); + a.unlockEvents("drag"); + a.fireEvent("rotationEnd", a, a.getRotation()); + return false + } + }, + onRotate: function(a) {} +}); +Ext.define("Ext.chart.interactions.RotatePie3D", { + extend: "Ext.chart.interactions.Rotate", + type: "rotatePie3d", + alias: "interaction.rotatePie3d", + getAngle: function(g) { + var a = this.getChart(), + f = a.getInherited().rtl, + d = f ? -1 : 1, + h = g.getXY(), + c = a.element.getXY(), + b = a.getMainRect(); + return d * Math.atan2(h[1] - c[1] - b[3] * 0.5, h[0] - c[0] - b[2] * 0.5) + }, + getRadius: function(j) { + var f = this.getChart(), + a = f.getRadius(), + d = f.getSeries(), + h = d.length, + c = 0, + b, g; + for (; c < h; c++) { + b = d[c]; + if (b.isPie3D) { + g = b.getRadius(); + if (g > a) { + a = g + } + } + } + return a + } +}); +Ext.define("Ext.chart.plugin.ItemEvents", { + extend: "Ext.plugin.Abstract", + alias: "plugin.chartitemevents", + moveEvents: false, + mouseMoveEvents: { + mousemove: true, + mouseover: true, + mouseout: true + }, + itemMouseMoveEvents: { + itemmousemove: true, + itemmouseover: true, + itemmouseout: true + }, + init: function(b) { + var a = "handleEvent"; + this.chart = b; + b.addElementListener({ + click: a, + dblclick: a, + mousedown: a, + mousemove: a, + mouseup: a, + mouseover: a, + mouseout: a, + priority: 1001, + scope: this + }) + }, + hasItemMouseMoveListeners: function() { + var b = this.chart.hasListeners, + a; + for (a in this.itemMouseMoveEvents) { + if (a in b) { + return true + } + } + return false + }, + handleEvent: function(g) { + var d = this, + a = d.chart, + h = g.type in d.mouseMoveEvents, + c = d.lastItem, + f, b; + if (h && !d.hasItemMouseMoveListeners() && !d.moveEvents) { + return + } + f = a.getEventXY(g); + b = a.getItemForPoint(f[0], f[1]); + if (h && !Ext.Object.equals(b, c)) { + if (c) { + a.fireEvent("itemmouseout", a, c, g); + c.series.fireEvent("itemmouseout", c.series, c, g) + } + if (b) { + a.fireEvent("itemmouseover", a, b, g); + b.series.fireEvent("itemmouseover", b.series, b, g) + } + } + if (b) { + a.fireEvent("item" + g.type, a, b, g); + b.series.fireEvent("item" + g.type, b.series, b, g) + } + d.lastItem = b + } +}); +Ext.define("Ext.chart.series.Cartesian", { + extend: "Ext.chart.series.Series", + config: { + xField: null, + yField: null, + xAxis: null, + yAxis: null + }, + directions: ["X", "Y"], + fieldCategoryX: ["X"], + fieldCategoryY: ["Y"], + applyXAxis: function(a, b) { + return this.getChart().getAxis(a) || b + }, + applyYAxis: function(a, b) { + return this.getChart().getAxis(a) || b + }, + updateXAxis: function(a) { + a.processData(this) + }, + updateYAxis: function(a) { + a.processData(this) + }, + coordinateX: function() { + return this.coordinate("X", 0, 2) + }, + coordinateY: function() { + return this.coordinate("Y", 1, 2) + }, + getItemForPoint: function(a, g) { + if (this.getSprites()) { + var f = this, + d = f.getSprites()[0], + b = f.getStore(), + e, c; + if (f.getHidden()) { + return null + } + if (d) { + c = d.getIndexNearPoint(a, g); + if (c !== -1) { + e = { + series: f, + category: f.getItemInstancing() ? "items" : "markers", + index: c, + record: b.getData().items[c], + field: f.getYField(), + sprite: d + }; + return e + } + } + } + }, + createSprite: function() { + var c = this, + a = c.callParent(), + b = c.getChart(), + d = c.getXAxis(); + a.setAttributes({ + flipXY: b.getFlipXY(), + xAxis: d + }); + if (a.setAggregator && d && d.getAggregator) { + if (d.getAggregator) { + a.setAggregator({ + strategy: d.getAggregator() + }) + } else { + a.setAggregator({}) + } + } + return a + }, + getSprites: function() { + var d = this, + c = this.getChart(), + e = d.getAnimation() || c && c.getAnimation(), + b = d.getItemInstancing(), + f = d.sprites, + a; + if (!c) { + return [] + } + if (!f.length) { + a = d.createSprite() + } else { + a = f[0] + } + if (e) { + if (b) { + a.itemsMarker.getTemplate().setAnimation(e) + } + a.setAnimation(e) + } + return f + }, + provideLegendInfo: function(d) { + var b = this, + a = b.getSubStyleWithTheme(), + c = a.fillStyle; + if (Ext.isArray(c)) { + c = c[0] + } + d.push({ + name: b.getTitle() || b.getYField() || b.getId(), + mark: (Ext.isObject(c) ? c.stops && c.stops[0].color : c) || a.strokeStyle || "black", + disabled: b.getHidden(), + series: b.getId(), + index: 0 + }) + }, + getXRange: function() { + return [this.dataRange[0], this.dataRange[2]] + }, + getYRange: function() { + return [this.dataRange[1], this.dataRange[3]] + } +}); +Ext.define("Ext.chart.series.StackedCartesian", { + extend: "Ext.chart.series.Cartesian", + config: { + stacked: true, + splitStacks: true, + fullStack: false, + fullStackTotal: 100, + hidden: [] + }, + spriteAnimationCount: 0, + themeColorCount: function() { + var b = this, + a = b.getYField(); + return Ext.isArray(a) ? a.length : 1 + }, + updateStacked: function() { + this.processData() + }, + updateSplitStacks: function() { + this.processData() + }, + coordinateY: function() { + return this.coordinateStacked("Y", 1, 2) + }, + coordinateStacked: function(D, e, m) { + var F = this, + f = F.getStore(), + r = f.getData().items, + B = r.length, + c = F["get" + D + "Axis"](), + x = F.getHidden(), + a = F.getSplitStacks(), + z = F.getFullStack(), + l = F.getFullStackTotal(), + p = { + min: 0, + max: 0 + }, + n = F["fieldCategory" + D], + C = [], + o = [], + E = [], + h, A = F.getStacked(), + g = F.getSprites(), + q = [], + w, v, u, s, H, y, b, d, G, t; + if (!g.length) { + return + } + for (w = 0; w < n.length; w++) { + d = n[w]; + s = F.getFields([d]); + H = s.length; + for (v = 0; v < B; v++) { + C[v] = 0; + o[v] = 0; + E[v] = 0 + } + for (v = 0; v < H; v++) { + if (!x[v]) { + q[v] = F.coordinateData(r, s[v], c) + } + } + if (A && z) { + y = []; + if (a) { + b = [] + } + for (v = 0; v < B; v++) { + y[v] = 0; + if (a) { + b[v] = 0 + } + for (u = 0; u < H; u++) { + G = q[u]; + if (!G) { + continue + } + G = G[v]; + if (G >= 0 || !a) { + y[v] += G + } else { + if (G < 0) { + b[v] += G + } + } + } + } + } + for (v = 0; v < H; v++) { + t = {}; + if (x[v]) { + t["dataStart" + d] = C; + t["data" + d] = C; + g[v].setAttributes(t); + continue + } + G = q[v]; + if (A) { + h = []; + for (u = 0; u < B; u++) { + if (!G[u]) { + G[u] = 0 + } + if (G[u] >= 0 || !a) { + if (z && y[u]) { + G[u] *= l / y[u] + } + C[u] = o[u]; + o[u] += G[u]; + h[u] = o[u] + } else { + if (z && b[u]) { + G[u] *= l / b[u] + } + C[u] = E[u]; + E[u] += G[u]; + h[u] = E[u] + } + } + t["dataStart" + d] = C; + t["data" + d] = h; + F.getRangeOfData(C, p); + F.getRangeOfData(h, p) + } else { + t["dataStart" + d] = C; + t["data" + d] = G; + F.getRangeOfData(G, p) + } + g[v].setAttributes(t) + } + } + F.dataRange[e] = p.min; + F.dataRange[e + m] = p.max; + t = {}; + t["dataMin" + D] = p.min; + t["dataMax" + D] = p.max; + for (w = 0; w < g.length; w++) { + g[w].setAttributes(t) + } + }, + getFields: function(f) { + var e = this, + a = [], + c, b, d; + for (b = 0, d = f.length; b < d; b++) { + c = e["get" + f[b] + "Field"](); + if (Ext.isArray(c)) { + a.push.apply(a, c) + } else { + a.push(c) + } + } + return a + }, + updateLabelOverflowPadding: function(a) { + this.getLabel().setAttributes({ + labelOverflowPadding: a + }) + }, + getSprites: function() { + var k = this, + j = k.getChart(), + c = k.getAnimation() || j && j.getAnimation(), + f = k.getFields(k.fieldCategoryY), + b = k.getItemInstancing(), + h = k.sprites, + l, e = k.getHidden(), + g = false, + d, a = f.length; + if (!j) { + return [] + } + for (d = 0; d < a; d++) { + l = h[d]; + if (!l) { + l = k.createSprite(); + l.setAttributes({ + zIndex: -d + }); + l.setField(f[d]); + g = true; + e.push(false); + if (b) { + l.itemsMarker.getTemplate().setAttributes(k.getStyleByIndex(d)) + } else { + l.setAttributes(k.getStyleByIndex(d)) + } + } + if (c) { + if (b) { + l.itemsMarker.getTemplate().setAnimation(c) + } + l.setAnimation(c) + } + } + if (g) { + k.updateHidden(e) + } + return h + }, + getItemForPoint: function(k, j) { + if (this.getSprites()) { + var h = this, + b, g, m, a = h.getItemInstancing(), + f = h.getSprites(), + l = h.getStore(), + c = h.getHidden(), + n, d, e; + for (b = 0, g = f.length; b < g; b++) { + if (!c[b]) { + m = f[b]; + d = m.getIndexNearPoint(k, j); + if (d !== -1) { + e = h.getYField(); + n = { + series: h, + index: d, + category: a ? "items" : "markers", + record: l.getData().items[d], + field: typeof e === "string" ? e : e[b], + sprite: m + }; + return n + } + } + } + return null + } + }, + provideLegendInfo: function(e) { + var g = this, + f = g.getSprites(), + h = g.getTitle(), + j = g.getYField(), + d = g.getHidden(), + k = f.length === 1, + b, l, c, a; + for (c = 0; c < f.length; c++) { + b = g.getStyleByIndex(c); + l = b.fillStyle; + if (h) { + if (Ext.isArray(h)) { + a = h[c] + } else { + if (k) { + a = h + } + } + } else { + if (Ext.isArray(j)) { + a = j[c] + } else { + a = g.getId() + } + } + e.push({ + name: a, + mark: (Ext.isObject(l) ? l.stops && l.stops[0].color : l) || b.strokeStyle || "black", + disabled: d[c], + series: g.getId(), + index: c + }) + } + }, + onSpriteAnimationStart: function(a) { + this.spriteAnimationCount++; + if (this.spriteAnimationCount === 1) { + this.fireEvent("animationstart") + } + }, + onSpriteAnimationEnd: function(a) { + this.spriteAnimationCount--; + if (this.spriteAnimationCount === 0) { + this.fireEvent("animationend") + } + } +}); +Ext.define("Ext.chart.series.sprite.Series", { + extend: "Ext.draw.sprite.Sprite", + mixins: { + markerHolder: "Ext.chart.MarkerHolder" + }, + inheritableStatics: { + def: { + processors: { + dataMinX: "number", + dataMaxX: "number", + dataMinY: "number", + dataMaxY: "number", + rangeX: "data", + rangeY: "data", + dataX: "data", + dataY: "data" + }, + defaults: { + dataMinX: 0, + dataMaxX: 1, + dataMinY: 0, + dataMaxY: 1, + rangeX: null, + rangeY: null, + dataX: null, + dataY: null + }, + triggers: { + dataX: "bbox", + dataY: "bbox", + dataMinX: "bbox", + dataMaxX: "bbox", + dataMinY: "bbox", + dataMaxY: "bbox" + } + } + }, + config: { + store: null, + series: null, + field: null + } +}); +Ext.define("Ext.chart.series.sprite.Cartesian", { + extend: "Ext.chart.series.sprite.Series", + inheritableStatics: { + def: { + processors: { + labels: "default", + labelOverflowPadding: "number", + selectionTolerance: "number", + flipXY: "bool", + renderer: "default", + visibleMinX: "number", + visibleMinY: "number", + visibleMaxX: "number", + visibleMaxY: "number", + innerWidth: "number", + innerHeight: "number" + }, + defaults: { + labels: null, + labelOverflowPadding: 10, + selectionTolerance: 20, + flipXY: false, + renderer: null, + transformFillStroke: false, + visibleMinX: 0, + visibleMinY: 0, + visibleMaxX: 1, + visibleMaxY: 1, + innerWidth: 1, + innerHeight: 1 + }, + triggers: { + dataX: "dataX,bbox", + dataY: "dataY,bbox", + visibleMinX: "panzoom", + visibleMinY: "panzoom", + visibleMaxX: "panzoom", + visibleMaxY: "panzoom", + innerWidth: "panzoom", + innerHeight: "panzoom" + }, + updaters: { + dataX: function(a) { + this.processDataX(); + this.scheduleUpdater(a, "dataY", ["dataY"]) + }, + dataY: function() { + this.processDataY() + }, + panzoom: function(c) { + var e = c.visibleMaxX - c.visibleMinX, + d = c.visibleMaxY - c.visibleMinY, + b = c.flipXY ? c.innerHeight : c.innerWidth, + g = !c.flipXY ? c.innerHeight : c.innerWidth, + a = this.getSurface(), + f = a ? a.getInherited().rtl : false; + if (f && !c.flipXY) { + c.translationX = b + c.visibleMinX * b / e + } else { + c.translationX = -c.visibleMinX * b / e + } + c.translationY = -c.visibleMinY * g / d; + c.scalingX = (f && !c.flipXY ? -1 : 1) * b / e; + c.scalingY = g / d; + c.scalingCenterX = 0; + c.scalingCenterY = 0; + this.applyTransformations(true) + } + } + } + }, + processDataY: Ext.emptyFn, + processDataX: Ext.emptyFn, + updatePlainBBox: function(b) { + var a = this.attr; + b.x = a.dataMinX; + b.y = a.dataMinY; + b.width = a.dataMaxX - a.dataMinX; + b.height = a.dataMaxY - a.dataMinY + }, + binarySearch: function(d) { + var b = this.attr.dataX, + f = 0, + a = b.length; + if (d <= b[0]) { + return f + } + if (d >= b[a - 1]) { + return a - 1 + } + while (f + 1 < a) { + var c = (f + a) >> 1, + e = b[c]; + if (e === d) { + return c + } else { + if (e < d) { + f = c + } else { + a = c + } + } + } + return f + }, + render: function(b, c, g) { + var f = this, + a = f.attr, + e = a.inverseMatrix.clone(); + e.appendMatrix(b.inverseMatrix); + if (a.dataX === null || a.dataX === undefined) { + return + } + if (a.dataY === null || a.dataY === undefined) { + return + } + if (e.getXX() * e.getYX() || e.getXY() * e.getYY()) { + console.log("Cartesian Series sprite does not support rotation/sheering"); + return + } + var d = e.transformList([ + [g[0] - 1, g[3] + 1], + [g[0] + g[2] + 1, -1] + ]); + d = d[0].concat(d[1]); + f.renderClipped(b, c, d, g) + }, + renderClipped: Ext.emptyFn, + getIndexNearPoint: function(f, e) { + var w = this, + q = w.attr.matrix, + h = w.attr.dataX, + g = w.attr.dataY, + k = w.attr.selectionTolerance, + t, r, c = -1, + j = q.clone().prependMatrix(w.surfaceMatrix).inverse(), + u = j.transformPoint([f, e]), + b = j.transformPoint([f - k, e - k]), + n = j.transformPoint([f + k, e + k]), + a = Math.min(b[0], n[0]), + s = Math.max(b[0], n[0]), + l = Math.min(b[1], n[1]), + d = Math.max(b[1], n[1]), + m, v, o, p; + for (o = 0, p = h.length; o < p; o++) { + m = h[o]; + v = g[o]; + if (m > a && m < s && v > l && v < d) { + if (c === -1 || (Math.abs(m - u[0]) < t) && (Math.abs(v - u[1]) < r)) { + t = Math.abs(m - u[0]); + r = Math.abs(v - u[1]); + c = o + } + } + } + return c + } +}); +Ext.define("Ext.chart.series.sprite.StackedCartesian", { + extend: "Ext.chart.series.sprite.Cartesian", + inheritableStatics: { + def: { + processors: { + groupCount: "number", + groupOffset: "number", + dataStartY: "data" + }, + defaults: { + selectionTolerance: 20, + groupCount: 1, + groupOffset: 0, + dataStartY: null + }, + triggers: { + dataStartY: "dataY,bbox" + } + } + }, + getIndexNearPoint: function(e, d) { + var o = this, + q = o.attr.matrix, + h = o.attr.dataX, + f = o.attr.dataY, + u = o.attr.dataStartY, + l = o.attr.selectionTolerance, + s = 0.5, + r = Infinity, + b = -1, + k = q.clone().prependMatrix(this.surfaceMatrix).inverse(), + t = k.transformPoint([e, d]), + a = k.transformPoint([e - l, d - l]), + n = k.transformPoint([e + l, d + l]), + m = Math.min(a[1], n[1]), + c = Math.max(a[1], n[1]), + j, g; + for (var p = 0; p < h.length; p++) { + if (Math.min(u[p], f[p]) <= c && m <= Math.max(u[p], f[p])) { + j = Math.abs(h[p] - t[0]); + g = Math.max(-Math.min(f[p] - t[1], t[1] - u[p]), 0); + if (j < s && g <= r) { + s = j; + r = g; + b = p + } + } + } + return b + } +}); +Ext.define("Ext.chart.series.sprite.Area", { + alias: "sprite.areaSeries", + extend: "Ext.chart.series.sprite.StackedCartesian", + inheritableStatics: { + def: { + processors: { + step: "bool" + }, + defaults: { + step: false + } + } + }, + renderClipped: function(q, s, A) { + var B = this, + p = B.attr, + l = p.dataX, + j = p.dataY, + C = p.dataStartY, + t = p.matrix, + h, g, v, f, d, z, w, e = t.elements[0], + m = t.elements[4], + o = t.elements[3], + k = t.elements[5], + c = B.surfaceMatrix, + n = {}, + r = Math.min(A[0], A[2]), + u = Math.max(A[0], A[2]), + b = Math.max(0, this.binarySearch(r)), + a = Math.min(l.length - 1, this.binarySearch(u) + 1); + s.beginPath(); + z = l[b] * e + m; + w = j[b] * o + k; + s.moveTo(z, w); + if (p.step) { + d = w; + for (v = b; v <= a; v++) { + h = l[v] * e + m; + g = j[v] * o + k; + s.lineTo(h, d); + s.lineTo(h, d = g) + } + } else { + for (v = b; v <= a; v++) { + h = l[v] * e + m; + g = j[v] * o + k; + s.lineTo(h, g) + } + } + if (C) { + if (p.step) { + f = l[a] * e + m; + for (v = a; v >= b; v--) { + h = l[v] * e + m; + g = C[v] * o + k; + s.lineTo(f, g); + s.lineTo(f = h, g) + } + } else { + for (v = a; v >= b; v--) { + h = l[v] * e + m; + g = C[v] * o + k; + s.lineTo(h, g) + } + } + } else { + s.lineTo(l[a] * e + m, g); + s.lineTo(l[a] * e + m, k); + s.lineTo(z, k); + s.lineTo(z, j[v] * o + k) + } + if (p.transformFillStroke) { + p.matrix.toContext(s) + } + s.fill(); + if (p.transformFillStroke) { + p.inverseMatrix.toContext(s) + } + s.beginPath(); + s.moveTo(z, w); + if (p.step) { + for (v = b; v <= a; v++) { + h = l[v] * e + m; + g = j[v] * o + k; + s.lineTo(h, d); + s.lineTo(h, d = g); + n.translationX = c.x(h, g); + n.translationY = c.y(h, g); + B.putMarker("markers", n, v, !p.renderer) + } + } else { + for (v = b; v <= a; v++) { + h = l[v] * e + m; + g = j[v] * o + k; + s.lineTo(h, g); + n.translationX = c.x(h, g); + n.translationY = c.y(h, g); + B.putMarker("markers", n, v, !p.renderer) + } + } + if (p.transformFillStroke) { + p.matrix.toContext(s) + } + s.stroke() + } +}); +Ext.define("Ext.chart.series.Area", { + extend: "Ext.chart.series.StackedCartesian", + alias: "series.area", + type: "area", + seriesType: "areaSeries", + requires: ["Ext.chart.series.sprite.Area"], + config: { + splitStacks: false + } +}); +Ext.define("Ext.chart.series.sprite.Bar", { + alias: "sprite.barSeries", + extend: "Ext.chart.series.sprite.StackedCartesian", + inheritableStatics: { + def: { + processors: { + minBarWidth: "number", + maxBarWidth: "number", + minGapWidth: "number", + radius: "number", + inGroupGapWidth: "number" + }, + defaults: { + minBarWidth: 2, + maxBarWidth: 100, + minGapWidth: 5, + inGroupGapWidth: 3, + radius: 0 + } + } + }, + drawLabel: function(k, i, s, h, o) { + var q = this, + n = q.attr, + f = q.getMarker("labels"), + d = f.getTemplate(), + l = q.labelCfg || (q.labelCfg = {}), + c = q.surfaceMatrix, + j = n.labelOverflowPadding, + b = d.attr.display, + m = d.attr.orientation, + g, e, a, r, t, p; + l.x = c.x(i, h); + l.y = c.y(i, h); + if (!n.flipXY) { + l.rotationRads = -Math.PI * 0.5 + } else { + l.rotationRads = 0 + } + l.calloutVertical = !n.flipXY; + switch (m) { + case "horizontal": + l.rotationRads = 0; + l.calloutVertical = false; + break; + case "vertical": + l.rotationRads = -Math.PI * 0.5; + l.calloutVertical = true; + break + } + l.text = k; + if (d.attr.renderer) { + p = [k, f, l, { + store: q.getStore() + }, o]; + r = Ext.callback(d.attr.renderer, null, p, 0, q.getSeries()); + if (typeof r === "string") { + l.text = r + } else { + if (typeof r === "object") { + if ("text" in r) { + l.text = r.text + } + t = true + } + } + } + a = q.getMarkerBBox("labels", o, true); + if (!a) { + q.putMarker("labels", l, o); + a = q.getMarkerBBox("labels", o, true) + } + e = (a.width / 2 + j); + if (s > h) { + e = -e + } + if ((m === "horizontal" && n.flipXY) || (m === "vertical" && !n.flipXY) || !m) { + g = (b === "insideStart") ? s + e : h - e + } else { + g = (b === "insideStart") ? s + j * 2 : h - j * 2 + } + l.x = c.x(i, g); + l.y = c.y(i, g); + g = (b === "insideStart") ? s - e : h + e; + l.calloutPlaceX = c.x(i, g); + l.calloutPlaceY = c.y(i, g); + g = (b === "insideStart") ? s : h; + l.calloutStartX = c.x(i, g); + l.calloutStartY = c.y(i, g); + if (s > h) { + e = -e + } + if (Math.abs(h - s) <= e * 2 || b === "outside") { + l.callout = 1 + } else { + l.callout = 0 + } + if (t) { + Ext.apply(l, r) + } + q.putMarker("labels", l, o) + }, + drawBar: function(l, b, d, c, h, k, a, e) { + var g = this, + j = {}, + f = g.attr.renderer, + i; + j.x = c; + j.y = h; + j.width = k - c; + j.height = a - h; + j.radius = g.attr.radius; + if (f) { + i = Ext.callback(f, null, [g, j, { + store: g.getStore() + }, e], 0, g.getSeries()); + Ext.apply(j, i) + } + g.putMarker("items", j, e, !f) + }, + renderClipped: function(G, u, F, C) { + if (this.cleanRedraw) { + return + } + var q = this, + o = q.attr, + w = o.dataX, + v = o.dataY, + H = o.labels, + n = o.dataStartY, + m = o.groupCount, + E = o.groupOffset - (m - 1) * 0.5, + z = o.inGroupGapWidth, + t = u.lineWidth, + D = o.matrix, + B = D.elements[0], + j = D.elements[3], + e = D.elements[4], + d = G.roundPixel(D.elements[5]) - 1, + J = (B < 0 ? -1 : 1) * B - o.minGapWidth, + k = (Math.min(J, o.maxBarWidth) - z * (m - 1)) / m, + A = G.roundPixel(Math.max(o.minBarWidth, k)), + c = q.surfaceMatrix, + g, I, b, h, K, a, l = 0.5 * o.lineWidth, + L = Math.min(F[0], F[2]), + x = Math.max(F[0], F[2]), + y = Math.max(0, Math.floor(L)), + p = Math.min(w.length - 1, Math.ceil(x)), + f = H && q.getMarker("labels"), + s, r; + for (K = y; K <= p; K++) { + s = n ? n[K] : 0; + r = v[K]; + a = w[K] * B + e + E * (A + z); + g = G.roundPixel(a - A / 2) + l; + h = G.roundPixel(r * j + d + t); + I = G.roundPixel(a + A / 2) - l; + b = G.roundPixel(s * j + d + t); + q.drawBar(u, G, F, g, h - l, I, b - l, K); + if (f && H[K] != null) { + q.drawLabel(H[K], a, b, h, K) + } + q.putMarker("markers", { + translationX: c.x(a, h), + translationY: c.y(a, h) + }, K, true) + } + }, + getIndexNearPoint: function(l, k) { + var m = this, + g = m.attr, + h = g.dataX, + a = m.getSurface(), + b = a.getRect() || [0, 0, 0, 0], + j = b[3], + e, d, c, n, f = -1; + if (g.flipXY) { + e = j - k; + if (a.getInherited().rtl) { + d = b[2] - l + } else { + d = l + } + } else { + e = l; + d = j - k + } + for (c = 0; c < h.length; c++) { + n = m.getMarkerBBox("items", c); + if (Ext.draw.Draw.isPointInBBox(e, d, n)) { + f = c; + break + } + } + return f + } +}); +Ext.define("Ext.chart.series.Bar", { + extend: "Ext.chart.series.StackedCartesian", + alias: "series.bar", + type: "bar", + seriesType: "barSeries", + requires: ["Ext.chart.series.sprite.Bar", "Ext.draw.sprite.Rect"], + config: { + itemInstancing: { + type: "rect", + fx: { + customDurations: { + x: 0, + y: 0, + width: 0, + height: 0, + radius: 0 + } + } + } + }, + getItemForPoint: function(a, f) { + if (this.getSprites()) { + var d = this, + c = d.getChart(), + e = c.getInnerPadding(), + b = c.getInherited().rtl; + arguments[0] = a + (b ? e.right : -e.left); + arguments[1] = f + e.bottom; + return d.callParent(arguments) + } + }, + updateXAxis: function(a) { + a.setLabelInSpan(true); + this.callParent(arguments) + }, + updateHidden: function(a) { + this.callParent(arguments); + this.updateStacked() + }, + updateStacked: function(c) { + var e = this, + g = e.getSprites(), + d = g.length, + f = [], + a = {}, + b; + for (b = 0; b < d; b++) { + if (!g[b].attr.hidden) { + f.push(g[b]) + } + } + d = f.length; + if (e.getStacked()) { + a.groupCount = 1; + a.groupOffset = 0; + for (b = 0; b < d; b++) { + f[b].setAttributes(a) + } + } else { + a.groupCount = f.length; + for (b = 0; b < d; b++) { + a.groupOffset = b; + f[b].setAttributes(a) + } + } + e.callParent(arguments) + } +}); +Ext.define("Ext.chart.series.sprite.Bar3D", { + extend: "Ext.chart.series.sprite.Bar", + alias: "sprite.bar3dSeries", + requires: ["Ext.draw.gradient.Linear"], + inheritableStatics: { + def: { + processors: { + depthWidthRatio: "number", + saturationFactor: "number", + brightnessFactor: "number", + colorSpread: "number" + }, + defaults: { + depthWidthRatio: 1 / 3, + saturationFactor: 1, + brightnessFactor: 1, + colorSpread: 1, + transformFillStroke: true + }, + triggers: { + groupCount: "panzoom" + }, + updaters: { + panzoom: function(c) { + var g = this, + e = c.visibleMaxX - c.visibleMinX, + d = c.visibleMaxY - c.visibleMinY, + b = c.flipXY ? c.innerHeight : c.innerWidth, + h = !c.flipXY ? c.innerHeight : c.innerWidth, + a = g.getSurface(), + f = a ? a.getInherited().rtl : false; + if (f && !c.flipXY) { + c.translationX = b + c.visibleMinX * b / e + } else { + c.translationX = -c.visibleMinX * b / e + } + c.translationY = -c.visibleMinY * (h - g.depth) / d; + c.scalingX = (f && !c.flipXY ? -1 : 1) * b / e; + c.scalingY = (h - g.depth) / d; + c.scalingCenterX = 0; + c.scalingCenterY = 0; + g.applyTransformations(true) + } + } + } + }, + config: { + showStroke: false + }, + depth: 0, + drawBar: function(p, b, d, c, l, o, a, h) { + var k = this, + i = k.attr, + n = {}, + j = i.renderer, + m, g, f, e; + n.x = (c + o) * 0.5; + n.y = l; + n.width = (o - c) * 0.75; + n.height = a - l; + n.depth = g = n.width * i.depthWidthRatio; + n.orientation = i.flipXY ? "horizontal" : "vertical"; + n.saturationFactor = i.saturationFactor; + n.brightnessFactor = i.brightnessFactor; + n.colorSpread = i.colorSpread; + if (g !== k.depth) { + k.depth = g; + f = k.getSeries(); + f.fireEvent("depthchange", f, g) + } + if (j) { + e = [k, n, { + store: k.getStore() + }, h]; + m = Ext.callback(j, null, e, 0, k.getSeries()); + Ext.apply(n, m) + } + k.putMarker("items", n, h, !j) + } +}); +Ext.define("Ext.chart.series.sprite.Box", { + extend: "Ext.draw.sprite.Sprite", + alias: "sprite.box", + type: "box", + inheritableStatics: { + def: { + processors: { + x: "number", + y: "number", + width: "number", + height: "number", + depth: "number", + orientation: "enums(vertical,horizontal)", + showStroke: "bool", + saturationFactor: "number", + brightnessFactor: "number", + colorSpread: "number" + }, + triggers: { + x: "bbox", + y: "bbox", + width: "bbox", + height: "bbox", + depth: "bbox", + orientation: "bbox" + }, + defaults: { + x: 0, + y: 0, + width: 8, + height: 8, + depth: 8, + orientation: "vertical", + showStroke: false, + saturationFactor: 1, + brightnessFactor: 1, + colorSpread: 1, + lineJoin: "bevel" + } + } + }, + constructor: function(a) { + this.callParent([a]); + this.topGradient = new Ext.draw.gradient.Linear({}); + this.rightGradient = new Ext.draw.gradient.Linear({}); + this.frontGradient = new Ext.draw.gradient.Linear({}) + }, + updatePlainBBox: function(d) { + var c = this.attr, + b = c.x, + g = c.y, + e = c.width, + a = c.height, + f = c.depth; + d.x = b - e * 0.5; + d.width = e + f; + if (a > 0) { + d.y = g; + d.height = a + f + } else { + d.y = g + f; + d.height = a - f + } + }, + render: function(l, m) { + var u = this, + k = u.attr, + r = k.x, + j = k.y, + f = j + k.height, + i = j < f, + e = k.width * 0.5, + v = k.depth, + d = k.orientation === "horizontal", + g = k.globalAlpha < 1, + c = k.fillStyle, + n = Ext.draw.Color.create(c.isGradient ? c.getStops()[0].color : c), + h = k.saturationFactor, + o = k.brightnessFactor, + t = k.colorSpread, + b = n.getHSV(), + a = {}, + s, q, p; + if (!k.showStroke) { + m.strokeStyle = Ext.draw.Color.RGBA_NONE + } + if (i) { + p = j; + j = f; + f = p + } + u.topGradient.setDegrees(d ? 0 : 80); + u.topGradient.setStops([{ + offset: 0, + color: Ext.draw.Color.fromHSV(b[0], Ext.Number.constrain(b[1] * h, 0, 1), Ext.Number.constrain((0.5 + t * 0.1) * o, 0, 1)) + }, { + offset: 1, + color: Ext.draw.Color.fromHSV(b[0], Ext.Number.constrain(b[1] * h, 0, 1), Ext.Number.constrain((0.5 - t * 0.11) * o, 0, 1)) + }]); + u.rightGradient.setDegrees(d ? 45 : 90); + u.rightGradient.setStops([{ + offset: 0, + color: Ext.draw.Color.fromHSV(b[0], Ext.Number.constrain(b[1] * h, 0, 1), Ext.Number.constrain((0.5 - t * 0.14) * o, 0, 1)) + }, { + offset: 1, + color: Ext.draw.Color.fromHSV(b[0], Ext.Number.constrain(b[1] * (1 + t * 0.4) * h, 0, 1), Ext.Number.constrain((0.5 - t * 0.32) * o, 0, 1)) + }]); + if (d) { + u.frontGradient.setDegrees(0) + } else { + u.frontGradient.setRadians(Math.atan2(j - f, e * 2)) + } + u.frontGradient.setStops([{ + offset: 0, + color: Ext.draw.Color.fromHSV(b[0], Ext.Number.constrain(b[1] * (1 - t * 0.1) * h, 0, 1), Ext.Number.constrain((0.5 + t * 0.1) * o, 0, 1)) + }, { + offset: 1, + color: Ext.draw.Color.fromHSV(b[0], Ext.Number.constrain(b[1] * (1 + t * 0.1) * h, 0, 1), Ext.Number.constrain((0.5 - t * 0.23) * o, 0, 1)) + }]); + if (g || i) { + m.beginPath(); + m.moveTo(r - e, f); + m.lineTo(r - e + v, f + v); + m.lineTo(r + e + v, f + v); + m.lineTo(r + e, f); + m.closePath(); + a.x = r - e; + a.y = j; + a.width = e + v; + a.height = v; + m.fillStyle = (d ? u.rightGradient : u.topGradient).generateGradient(m, a); + m.fillStroke(k) + } + if (g) { + m.beginPath(); + m.moveTo(r - e, j); + m.lineTo(r - e + v, j + v); + m.lineTo(r - e + v, f + v); + m.lineTo(r - e, f); + m.closePath(); + a.x = r + e; + a.y = f; + a.width = v; + a.height = j + v - f; + m.fillStyle = (d ? u.topGradient : u.rightGradient).generateGradient(m, a); + m.fillStroke(k) + } + q = l.roundPixel(j); + m.beginPath(); + m.moveTo(r - e, q); + m.lineTo(r - e + v, j + v); + m.lineTo(r + e + v, j + v); + m.lineTo(r + e, q); + m.closePath(); + a.x = r - e; + a.y = j; + a.width = e + v; + a.height = v; + m.fillStyle = (d ? u.rightGradient : u.topGradient).generateGradient(m, a); + m.fillStroke(k); + s = l.roundPixel(r + e); + m.beginPath(); + m.moveTo(s, l.roundPixel(j)); + m.lineTo(r + e + v, j + v); + m.lineTo(r + e + v, f + v); + m.lineTo(s, f); + m.closePath(); + a.x = r + e; + a.y = f; + a.width = v; + a.height = j + v - f; + m.fillStyle = (d ? u.topGradient : u.rightGradient).generateGradient(m, a); + m.fillStroke(k); + s = l.roundPixel(r + e); + q = l.roundPixel(j); + m.beginPath(); + m.moveTo(r - e, f); + m.lineTo(r - e, q); + m.lineTo(s, q); + m.lineTo(s, f); + m.closePath(); + a.x = r - e; + a.y = f; + a.width = e * 2; + a.height = j - f; + m.fillStyle = u.frontGradient.generateGradient(m, a); + m.fillStroke(k) + } +}); +Ext.define("Ext.chart.series.Bar3D", { + extend: "Ext.chart.series.Bar", + requires: ["Ext.chart.series.sprite.Bar3D", "Ext.chart.series.sprite.Box"], + alias: "series.bar3d", + type: "bar3d", + seriesType: "bar3dSeries", + config: { + itemInstancing: { + type: "box", + fx: { + customDurations: { + x: 0, + y: 0, + width: 0, + height: 0, + depth: 0 + } + } + }, + highlightCfg: { + opacity: 0.8 + } + }, + getSprites: function() { + var c = this.callParent(arguments), + b, d, a; + for (a = 0; a < c.length; a++) { + b = c[a]; + d = b.attr.zIndex; + if (d < 0) { + b.setAttributes({ + zIndex: -d + }) + } + if (b.setSeries) { + b.setSeries(this) + } + } + return c + }, + getDepth: function() { + var a = this.getSprites()[0]; + return a ? (a.depth || 0) : 0 + }, + getItemForPoint: function(m, k) { + if (this.getSprites()) { + var j = this, + b, o, a = j.getItemInstancing(), + h = j.getSprites(), + n = j.getStore(), + c = j.getHidden(), + g = j.getChart(), + l = g.getInnerPadding(), + f = g.getInherited().rtl, + p, d, e; + m = m + (f ? l.right : -l.left); + k = k + l.bottom; + for (b = h.length - 1; b >= 0; b--) { + if (!c[b]) { + o = h[b]; + d = o.getIndexNearPoint(m, k); + if (d !== -1) { + e = j.getYField(); + p = { + series: j, + index: d, + category: a ? "items" : "markers", + record: n.getData().items[d], + field: typeof e === "string" ? e : e[b], + sprite: o + }; + return p + } + } + } + return null + } + } +}); +Ext.define("Ext.draw.LimitedCache", { + config: { + limit: 40, + feeder: function() { + return 0 + }, + scope: null + }, + cache: null, + constructor: function(a) { + this.cache = {}; + this.cache.list = []; + this.cache.tail = 0; + this.initConfig(a) + }, + get: function(e) { + var c = this.cache, + b = this.getLimit(), + a = this.getFeeder(), + d = this.getScope() || this; + if (c[e]) { + return c[e].value + } + if (c.list[c.tail]) { + delete c[c.list[c.tail].cacheId] + } + c[e] = c.list[c.tail] = { + value: a.apply(d, Array.prototype.slice.call(arguments, 1)), + cacheId: e + }; + c.tail++; + if (c.tail === b) { + c.tail = 0 + } + return c[e].value + }, + clear: function() { + this.cache = {}; + this.cache.list = []; + this.cache.tail = 0 + } +}); +Ext.define("Ext.draw.SegmentTree", { + config: { + strategy: "double" + }, + time: function(m, l, n, c, E, d, e) { + var f = 0, + o, A, s = new Date(n[m.startIdx[0]]), + x = new Date(n[m.endIdx[l - 1]]), + D = Ext.Date, + u = [ + [D.MILLI, 1, "ms1", null], + [D.MILLI, 2, "ms2", "ms1"], + [D.MILLI, 5, "ms5", "ms1"], + [D.MILLI, 10, "ms10", "ms5"], + [D.MILLI, 50, "ms50", "ms10"], + [D.MILLI, 100, "ms100", "ms50"], + [D.MILLI, 500, "ms500", "ms100"], + [D.SECOND, 1, "s1", "ms500"], + [D.SECOND, 10, "s10", "s1"], + [D.SECOND, 30, "s30", "s10"], + [D.MINUTE, 1, "mi1", "s10"], + [D.MINUTE, 5, "mi5", "mi1"], + [D.MINUTE, 10, "mi10", "mi5"], + [D.MINUTE, 30, "mi30", "mi10"], + [D.HOUR, 1, "h1", "mi30"], + [D.HOUR, 6, "h6", "h1"], + [D.HOUR, 12, "h12", "h6"], + [D.DAY, 1, "d1", "h12"], + [D.DAY, 7, "d7", "d1"], + [D.MONTH, 1, "mo1", "d1"], + [D.MONTH, 3, "mo3", "mo1"], + [D.MONTH, 6, "mo6", "mo3"], + [D.YEAR, 1, "y1", "mo3"], + [D.YEAR, 5, "y5", "y1"], + [D.YEAR, 10, "y10", "y5"], + [D.YEAR, 100, "y100", "y10"] + ], + z, b, k = f, + F = l, + j = false, + r = m.startIdx, + h = m.endIdx, + w = m.minIdx, + C = m.maxIdx, + a = m.open, + y = m.close, + g = m.minX, + q = m.minY, + p = m.maxX, + B = m.maxY, + v, t; + for (z = 0; l > f + 1 && z < u.length; z++) { + s = new Date(n[r[0]]); + b = u[z]; + s = D.align(s, b[0], b[1]); + if (D.diff(s, x, b[0]) > n.length * 2 * b[1]) { + continue + } + if (b[3] && m.map["time_" + b[3]]) { + o = m.map["time_" + b[3]][0]; + A = m.map["time_" + b[3]][1] + } else { + o = k; + A = F + } + f = l; + t = s; + j = true; + r[l] = r[o]; + h[l] = h[o]; + w[l] = w[o]; + C[l] = C[o]; + a[l] = a[o]; + y[l] = y[o]; + g[l] = g[o]; + q[l] = q[o]; + p[l] = p[o]; + B[l] = B[o]; + t = Ext.Date.add(t, b[0], b[1]); + for (v = o + 1; v < A; v++) { + if (n[h[v]] < +t) { + h[l] = h[v]; + y[l] = y[v]; + if (B[v] > B[l]) { + B[l] = B[v]; + p[l] = p[v]; + C[l] = C[v] + } + if (q[v] < q[l]) { + q[l] = q[v]; + g[l] = g[v]; + w[l] = w[v] + } + } else { + l++; + r[l] = r[v]; + h[l] = h[v]; + w[l] = w[v]; + C[l] = C[v]; + a[l] = a[v]; + y[l] = y[v]; + g[l] = g[v]; + q[l] = q[v]; + p[l] = p[v]; + B[l] = B[v]; + t = Ext.Date.add(t, b[0], b[1]) + } + } + if (l > f) { + m.map["time_" + b[2]] = [f, l] + } + } + }, + "double": function(h, u, j, a, t, b, c) { + var e = 0, + k, f = 1, + n, d, v, g, s, l, m, r, q, p, o; + while (u > e + 1) { + k = e; + e = u; + f += f; + for (n = k; n < e; n += 2) { + if (n === e - 1) { + d = h.startIdx[n]; + v = h.endIdx[n]; + g = h.minIdx[n]; + s = h.maxIdx[n]; + l = h.open[n]; + m = h.close[n]; + r = h.minX[n]; + q = h.minY[n]; + p = h.maxX[n]; + o = h.maxY[n] + } else { + d = h.startIdx[n]; + v = h.endIdx[n + 1]; + l = h.open[n]; + m = h.close[n]; + if (h.minY[n] <= h.minY[n + 1]) { + g = h.minIdx[n]; + r = h.minX[n]; + q = h.minY[n] + } else { + g = h.minIdx[n + 1]; + r = h.minX[n + 1]; + q = h.minY[n + 1] + } + if (h.maxY[n] >= h.maxY[n + 1]) { + s = h.maxIdx[n]; + p = h.maxX[n]; + o = h.maxY[n] + } else { + s = h.maxIdx[n + 1]; + p = h.maxX[n + 1]; + o = h.maxY[n + 1] + } + } + h.startIdx[u] = d; + h.endIdx[u] = v; + h.minIdx[u] = g; + h.maxIdx[u] = s; + h.open[u] = l; + h.close[u] = m; + h.minX[u] = r; + h.minY[u] = q; + h.maxX[u] = p; + h.maxY[u] = o; + u++ + } + h.map["double_" + f] = [e, u] + } + }, + none: Ext.emptyFn, + aggregateData: function(h, a, r, c, d) { + var b = h.length, + e = [], + s = [], + f = [], + q = [], + j = [], + p = [], + n = [], + o = [], + m = [], + k = [], + g = { + startIdx: e, + endIdx: s, + minIdx: f, + maxIdx: q, + open: j, + minX: p, + minY: n, + maxX: o, + maxY: m, + close: k + }, + l; + for (l = 0; l < b; l++) { + e[l] = l; + s[l] = l; + f[l] = l; + q[l] = l; + j[l] = a[l]; + p[l] = h[l]; + n[l] = c[l]; + o[l] = h[l]; + m[l] = r[l]; + k[l] = d[l] + } + g.map = { + original: [0, b] + }; + if (b) { + this[this.getStrategy()](g, b, h, a, r, c, d) + } + return g + }, + binarySearchMin: function(c, g, a, e) { + var b = this.dataX; + if (e <= b[c.startIdx[0]]) { + return g + } + if (e >= b[c.startIdx[a - 1]]) { + return a - 1 + } + while (g + 1 < a) { + var d = (g + a) >> 1, + f = b[c.startIdx[d]]; + if (f === e) { + return d + } else { + if (f < e) { + g = d + } else { + a = d + } + } + } + return g + }, + binarySearchMax: function(c, g, a, e) { + var b = this.dataX; + if (e <= b[c.endIdx[0]]) { + return g + } + if (e >= b[c.endIdx[a - 1]]) { + return a - 1 + } + while (g + 1 < a) { + var d = (g + a) >> 1, + f = b[c.endIdx[d]]; + if (f === e) { + return d + } else { + if (f < e) { + g = d + } else { + a = d + } + } + } + return a + }, + constructor: function(a) { + this.initConfig(a) + }, + setData: function(d, a, b, c, e) { + if (!b) { + e = c = b = a + } + this.dataX = d; + this.dataOpen = a; + this.dataHigh = b; + this.dataLow = c; + this.dataClose = e; + if (d.length === b.length && d.length === c.length) { + this.cache = this.aggregateData(d, a, b, c, e) + } + }, + getAggregation: function(d, k, i) { + if (!this.cache) { + return null + } + var c = Infinity, + g = this.dataX[this.dataX.length - 1] - this.dataX[0], + l = this.cache.map, + m = l.original, + a, e, j, b, f, h; + for (a in l) { + e = l[a]; + j = e[1] - e[0] - 1; + b = g / j; + if (i <= b && b < c) { + m = e; + c = b + } + } + f = Math.max(this.binarySearchMin(this.cache, m[0], m[1], d), m[0]); + h = Math.min(this.binarySearchMax(this.cache, m[0], m[1], k) + 1, m[1]); + return { + data: this.cache, + start: f, + end: h + } + } +}); +Ext.define("Ext.chart.series.sprite.Aggregative", { + extend: "Ext.chart.series.sprite.Cartesian", + requires: ["Ext.draw.LimitedCache", "Ext.draw.SegmentTree"], + inheritableStatics: { + def: { + processors: { + dataHigh: "data", + dataLow: "data", + dataClose: "data" + }, + aliases: { + dataOpen: "dataY" + }, + defaults: { + dataHigh: null, + dataLow: null, + dataClose: null + } + } + }, + config: { + aggregator: {} + }, + applyAggregator: function(b, a) { + return Ext.factory(b, Ext.draw.SegmentTree, a) + }, + constructor: function() { + this.callParent(arguments) + }, + processDataY: function() { + var d = this, + b = d.attr, + e = b.dataHigh, + a = b.dataLow, + f = b.dataClose, + c = b.dataY; + d.callParent(arguments); + if (b.dataX && c && c.length > 0) { + if (e) { + d.getAggregator().setData(b.dataX, b.dataY, e, a, f) + } else { + d.getAggregator().setData(b.dataX, b.dataY) + } + } + }, + getGapWidth: function() { + return 1 + }, + renderClipped: function(b, c, g, f) { + var e = this, + d = Math.min(g[0], g[2]), + a = Math.max(g[0], g[2]), + h = e.getAggregator() && e.getAggregator().getAggregation(d, a, (a - d) / f[2] * e.getGapWidth()); + if (h) { + e.dataStart = h.data.startIdx[h.start]; + e.dataEnd = h.data.endIdx[h.end - 1]; + e.renderAggregates(h.data, h.start, h.end, b, c, g, f) + } + } +}); +Ext.define("Ext.chart.series.sprite.CandleStick", { + alias: "sprite.candlestickSeries", + extend: "Ext.chart.series.sprite.Aggregative", + inheritableStatics: { + def: { + processors: { + raiseStyle: function(b, a) { + return Ext.merge({}, a || {}, b) + }, + dropStyle: function(b, a) { + return Ext.merge({}, a || {}, b) + }, + barWidth: "number", + padding: "number", + ohlcType: "enums(candlestick,ohlc)" + }, + defaults: { + raiseStyle: { + strokeStyle: "green", + fillStyle: "green" + }, + dropStyle: { + strokeStyle: "red", + fillStyle: "red" + }, + planar: false, + barWidth: 15, + padding: 3, + lineJoin: "miter", + miterLimit: 5, + ohlcType: "candlestick" + }, + triggers: { + raiseStyle: "raiseStyle", + dropStyle: "dropStyle" + }, + updaters: { + raiseStyle: function() { + this.raiseTemplate && this.raiseTemplate.setAttributes(this.attr.raiseStyle) + }, + dropStyle: function() { + this.dropTemplate && this.dropTemplate.setAttributes(this.attr.dropStyle) + } + } + } + }, + candlestick: function(i, c, a, e, h, f, b) { + var d = Math.min(c, h), + g = Math.max(c, h); + i.moveTo(f, e); + i.lineTo(f, g); + i.moveTo(f + b, g); + i.lineTo(f + b, d); + i.lineTo(f - b, d); + i.lineTo(f - b, g); + i.closePath(); + i.moveTo(f, a); + i.lineTo(f, d) + }, + ohlc: function(b, d, e, a, f, c, g) { + b.moveTo(c, e); + b.lineTo(c, a); + b.moveTo(c, d); + b.lineTo(c - g, d); + b.moveTo(c, f); + b.lineTo(c + g, f) + }, + constructor: function() { + this.callParent(arguments); + this.raiseTemplate = new Ext.draw.sprite.Rect({ + parent: this + }); + this.dropTemplate = new Ext.draw.sprite.Rect({ + parent: this + }) + }, + getGapWidth: function() { + var a = this.attr, + b = a.barWidth, + c = a.padding; + return b + c + }, + renderAggregates: function(d, c, b, t, u, z) { + var D = this, + s = this.attr, + j = s.dataX, + v = s.matrix, + e = v.getXX(), + r = v.getYY(), + l = v.getDX(), + h = v.getDY(), + o = s.barWidth / e, + C, k = s.ohlcType, + f = Math.round(o * 0.5 * e), + a = d.open, + y = d.close, + B = d.maxY, + p = d.minY, + q = d.startIdx, + m, g, E, n, A, x, w = s.lineWidth * t.devicePixelRatio / 2; + w -= Math.floor(w); + u.save(); + C = this.raiseTemplate; + C.useAttributes(u, z); + u.beginPath(); + for (x = c; x < b; x++) { + if (a[x] <= y[x]) { + m = Math.round(a[x] * r + h) + w; + g = Math.round(B[x] * r + h) + w; + E = Math.round(p[x] * r + h) + w; + n = Math.round(y[x] * r + h) + w; + A = Math.round(j[q[x]] * e + l) + w; + D[k](u, m, g, E, n, A, f) + } + } + u.fillStroke(C.attr); + u.restore(); + u.save(); + C = this.dropTemplate; + C.useAttributes(u, z); + u.beginPath(); + for (x = c; x < b; x++) { + if (a[x] > y[x]) { + m = Math.round(a[x] * r + h) + w; + g = Math.round(B[x] * r + h) + w; + E = Math.round(p[x] * r + h) + w; + n = Math.round(y[x] * r + h) + w; + A = Math.round(j[q[x]] * e + l) + w; + D[k](u, m, g, E, n, A, f) + } + } + u.fillStroke(C.attr); + u.restore() + } +}); +Ext.define("Ext.chart.series.CandleStick", { + extend: "Ext.chart.series.Cartesian", + requires: ["Ext.chart.series.sprite.CandleStick"], + alias: "series.candlestick", + type: "candlestick", + seriesType: "candlestickSeries", + config: { + openField: null, + highField: null, + lowField: null, + closeField: null + }, + fieldCategoryY: ["Open", "High", "Low", "Close"], + themeColorCount: function() { + return 2 + } +}); +Ext.define("Ext.chart.series.Polar", { + extend: "Ext.chart.series.Series", + config: { + rotation: 0, + radius: null, + center: [0, 0], + offsetX: 0, + offsetY: 0, + showInLegend: true, + xField: null, + yField: null, + angleField: null, + radiusField: null, + xAxis: null, + yAxis: null + }, + directions: ["X", "Y"], + fieldCategoryX: ["X"], + fieldCategoryY: ["Y"], + deprecatedConfigs: { + field: "angleField", + lengthField: "radiusField" + }, + constructor: function(b) { + var c = this, + a = c.getConfigurator(), + e = a.configs, + d; + if (b) { + for (d in c.deprecatedConfigs) { + if (d in b && !(b in e)) { + Ext.raise("'" + d + "' config has been deprecated. Please use the '" + c.deprecatedConfigs[d] + "' config instead.") + } + } + } + c.callParent([b]) + }, + getXField: function() { + return this.getAngleField() + }, + updateXField: function(a) { + this.setAngleField(a) + }, + getYField: function() { + return this.getRadiusField() + }, + updateYField: function(a) { + this.setRadiusField(a) + }, + applyXAxis: function(a, b) { + return this.getChart().getAxis(a) || b + }, + applyYAxis: function(a, b) { + return this.getChart().getAxis(a) || b + }, + getXRange: function() { + return [this.dataRange[0], this.dataRange[2]] + }, + getYRange: function() { + return [this.dataRange[1], this.dataRange[3]] + }, + themeColorCount: function() { + var c = this, + a = c.getStore(), + b = a && a.getCount() || 0; + return b + }, + isStoreDependantColorCount: true, + getDefaultSpriteConfig: function() { + return { + type: this.seriesType, + renderer: this.getRenderer(), + centerX: 0, + centerY: 0, + rotationCenterX: 0, + rotationCenterY: 0 + } + }, + applyRotation: function(a) { + return Ext.draw.sprite.AttributeParser.angle(a) + }, + updateRotation: function(a) { + var b = this.getSprites(); + if (b && b[0]) { + b[0].setAttributes({ + baseRotation: a + }) + } + } +}); +Ext.define("Ext.chart.series.Gauge", { + alias: "series.gauge", + extend: "Ext.chart.series.Polar", + type: "gauge", + seriesType: "pieslice", + requires: ["Ext.draw.sprite.Sector"], + config: { + needle: false, + needleLength: 90, + needleWidth: 4, + donut: 30, + showInLegend: false, + value: null, + colors: null, + sectors: null, + minimum: 0, + maximum: 100, + rotation: 0, + totalAngle: Math.PI / 2, + rect: [0, 0, 1, 1], + center: [0.5, 0.75], + radius: 0.5, + wholeDisk: false + }, + coordinateX: function() { + return this.coordinate("X", 0, 2) + }, + coordinateY: function() { + return this.coordinate("Y", 1, 2) + }, + updateNeedle: function(b) { + var a = this, + d = a.getSprites(), + c = a.valueToAngle(a.getValue()); + if (d && d.length) { + d[0].setAttributes({ + startAngle: (b ? c : 0), + endAngle: c, + strokeOpacity: (b ? 1 : 0), + lineWidth: (b ? a.getNeedleWidth() : 0) + }); + a.doUpdateStyles() + } + }, + themeColorCount: function() { + var c = this, + a = c.getStore(), + b = a && a.getCount() || 0; + return b + (c.getNeedle() ? 0 : 1) + }, + updateColors: function(a, b) { + var f = this, + h = f.getSectors(), + j = h && h.length, + e = f.getSprites(), + c = Ext.Array.clone(a), + g = a && a.length, + d; + if (!g || !a[0]) { + return + } + for (d = 0; d < j; d++) { + c[d + 1] = h[d].color || c[d + 1] || a[d % g] + } + if (e.length) { + e[0].setAttributes({ + strokeStyle: c[0] + }) + } + this.setSubStyle({ + fillStyle: c, + strokeStyle: c + }); + this.doUpdateStyles() + }, + updateRect: function(f) { + var d = this.getWholeDisk(), + c = d ? Math.PI : this.getTotalAngle() / 2, + g = this.getDonut() / 100, + e, b, a; + if (c <= Math.PI / 2) { + e = 2 * Math.sin(c); + b = 1 - g * Math.cos(c) + } else { + e = 2; + b = 1 - Math.cos(c) + } + a = Math.min(f[2] / e, f[3] / b); + this.setRadius(a); + this.setCenter([f[2] / 2, a + (f[3] - b * a) / 2]) + }, + updateCenter: function(a) { + this.setStyle({ + centerX: a[0], + centerY: a[1], + rotationCenterX: a[0], + rotationCenterY: a[1] + }); + this.doUpdateStyles() + }, + updateRotation: function(a) { + this.setStyle({ + rotationRads: a - (this.getTotalAngle() + Math.PI) / 2 + }); + this.doUpdateStyles() + }, + doUpdateShape: function(b, f) { + var a, d = this.getSectors(), + c = (d && d.length) || 0, + e = this.getNeedleLength() / 100; + a = [b * e, b]; + while (c--) { + a.push(b) + } + this.setSubStyle({ + endRho: a, + startRho: b / 100 * f + }); + this.doUpdateStyles() + }, + updateRadius: function(a) { + var b = this.getDonut(); + this.doUpdateShape(a, b) + }, + updateDonut: function(b) { + var a = this.getRadius(); + this.doUpdateShape(a, b) + }, + valueToAngle: function(a) { + a = this.applyValue(a); + return this.getTotalAngle() * (a - this.getMinimum()) / (this.getMaximum() - this.getMinimum()) + }, + applyValue: function(a) { + return Math.min(this.getMaximum(), Math.max(a, this.getMinimum())) + }, + updateValue: function(b) { + var a = this, + c = a.getNeedle(), + e = a.valueToAngle(b), + d = a.getSprites(); + d[0].rendererData.value = b; + d[0].setAttributes({ + startAngle: (c ? e : 0), + endAngle: e + }); + a.doUpdateStyles() + }, + processData: function() { + var f = this, + j = f.getStore(), + a, d, h, b, g, e = j && j.first(), + c, i; + if (e) { + c = f.getXField(); + if (c) { + i = e.get(c) + } + } + if (a = f.getXAxis()) { + d = a.getMinimum(); + h = a.getMaximum(); + b = a.getSprites()[0].fx; + g = b.getDuration(); + b.setDuration(0); + if (Ext.isNumber(d)) { + f.setMinimum(d) + } else { + a.setMinimum(f.getMinimum()) + } + if (Ext.isNumber(h)) { + f.setMaximum(h) + } else { + a.setMaximum(f.getMaximum()) + } + b.setDuration(g) + } + if (!Ext.isNumber(i)) { + i = f.getMinimum() + } + f.setValue(i) + }, + getDefaultSpriteConfig: function() { + return { + type: this.seriesType, + renderer: this.getRenderer(), + fx: { + customDurations: { + translationX: 0, + translationY: 0, + rotationCenterX: 0, + rotationCenterY: 0, + centerX: 0, + centerY: 0, + startRho: 0, + endRho: 0, + baseRotation: 0 + } + } + } + }, + normalizeSectors: function(f) { + var d = this, + c = (f && f.length) || 0, + b, e, g, a; + if (c) { + for (b = 0; b < c; b++) { + e = f[b]; + if (typeof e === "number") { + f[b] = { + start: (b > 0 ? f[b - 1].end : d.getMinimum()), + end: Math.min(e, d.getMaximum()) + }; + if (b == (c - 1) && f[b].end < d.getMaximum()) { + f[b + 1] = { + start: f[b].end, + end: d.getMaximum() + } + } + } else { + if (typeof e.start === "number") { + g = Math.max(e.start, d.getMinimum()) + } else { + g = (b > 0 ? f[b - 1].end : d.getMinimum()) + } + if (typeof e.end === "number") { + a = Math.min(e.end, d.getMaximum()) + } else { + a = d.getMaximum() + } + f[b].start = g; + f[b].end = a + } + } + } else { + f = [{ + start: d.getMinimum(), + end: d.getMaximum() + }] + } + return f + }, + getSprites: function() { + var j = this, + m = j.getStore(), + l = j.getValue(), + c, g; + if (!m && !Ext.isNumber(l)) { + return [] + } + var h = j.getChart(), + b = j.getAnimation() || h && h.getAnimation(), + f = j.sprites, + k = 0, + o, n, e, d, a = []; + if (f && f.length) { + f[0].setAnimation(b); + return f + } + d = { + store: m, + field: j.getXField(), + angleField: j.getXField(), + value: l, + series: j + }; + o = j.createSprite(); + o.setAttributes({ + zIndex: 10 + }, true); + o.rendererData = d; + o.rendererIndex = k++; + a.push(j.getNeedleWidth()); + j.getLabel().getTemplate().setField(true); + n = j.normalizeSectors(j.getSectors()); + for (c = 0, g = n.length; c < g; c++) { + e = { + startAngle: j.valueToAngle(n[c].start), + endAngle: j.valueToAngle(n[c].end), + label: n[c].label, + fillStyle: n[c].color, + strokeOpacity: 0, + doCallout: false, + labelOverflowPadding: -1 + }; + Ext.apply(e, n[c].style); + o = j.createSprite(); + o.rendererData = d; + o.rendererIndex = k++; + o.setAttributes(e, true); + a.push(e.lineWidth) + } + j.setSubStyle({ + lineWidth: a + }); + j.doUpdateStyles(); + return f + } +}); +Ext.define("Ext.chart.series.sprite.Line", { + alias: "sprite.lineSeries", + extend: "Ext.chart.series.sprite.Aggregative", + inheritableStatics: { + def: { + processors: { + smooth: "bool", + fillArea: "bool", + step: "bool", + preciseStroke: "bool", + xAxis: "default", + yCap: "default" + }, + defaults: { + smooth: false, + fillArea: false, + step: false, + preciseStroke: true, + xAxis: null, + yCap: Math.pow(2, 20), + yJump: 50 + }, + triggers: { + dataX: "dataX,bbox,smooth", + dataY: "dataY,bbox,smooth", + smooth: "smooth" + }, + updaters: { + smooth: function(a) { + var c = a.dataX, + b = a.dataY; + if (a.smooth && c && b && c.length > 2 && b.length > 2) { + this.smoothX = Ext.draw.Draw.spline(c); + this.smoothY = Ext.draw.Draw.spline(b) + } else { + delete this.smoothX; + delete this.smoothY + } + } + } + } + }, + list: null, + updatePlainBBox: function(d) { + var b = this.attr, + c = Math.min(0, b.dataMinY), + a = Math.max(0, b.dataMaxY); + d.x = b.dataMinX; + d.y = c; + d.width = b.dataMaxX - b.dataMinX; + d.height = a - c + }, + drawStrip: function(a, c) { + a.moveTo(c[0], c[1]); + for (var b = 2, d = c.length; b < d; b += 2) { + a.lineTo(c[b], c[b + 1]) + } + }, + drawStraightStroke: function(p, q, e, d, u, h) { + var w = this, + o = w.attr, + n = o.renderer, + g = o.step, + a = true, + l = { + type: "line", + smooth: false, + step: g + }, + m = [], + l, z, v, f, k, j, t, c, s, b, r; + for (r = 3; r < u.length; r += 3) { + t = u[r - 3]; + c = u[r - 2]; + k = u[r]; + j = u[r + 1]; + s = u[r + 3]; + b = u[r + 4]; + if (n) { + l.x = k; + l.y = j; + l.x0 = t; + l.y0 = c; + v = [w, l, w.rendererData, e + r / 3]; + z = Ext.callback(n, null, v, 0, w.getSeries()) + } + if (Ext.isNumber(k + j + t + c)) { + if (a) { + q.beginPath(); + q.moveTo(t, c); + m.push(t, c); + f = t; + a = false + } + } else { + continue + } + if (g) { + q.lineTo(k, c); + m.push(k, c) + } + q.lineTo(k, j); + m.push(k, j); + if (z || !(Ext.isNumber(s + b))) { + q.save(); + Ext.apply(q, z); + if (o.fillArea) { + q.lineTo(k, h); + q.lineTo(f, h); + q.closePath(); + q.fill() + } + q.beginPath(); + w.drawStrip(q, m); + m = []; + q.stroke(); + q.restore(); + q.beginPath(); + a = true + } + } + }, + calculateScale: function(c, a) { + var b = 0, + d = c; + while (d < a && c > 0) { + b++; + d += c >> b + } + return Math.pow(2, b > 0 ? b - 1 : b) + }, + drawSmoothStroke: function(u, v, c, b, C, f) { + var G = this, + t = G.attr, + d = t.step, + z = t.matrix, + s = t.renderer, + e = z.getXX(), + p = z.getYY(), + m = z.getDX(), + k = z.getDY(), + r = G.smoothX, + q = G.smoothY, + I = G.calculateScale(t.dataX.length, b), + o, F, n, E, h, g, B, a, A, w, H, D, l = { + type: "line", + smooth: true, + step: d + }; + v.beginPath(); + v.moveTo(r[c * 3] * e + m, q[c * 3] * p + k); + for (A = 0, w = c * 3 + 1; A < C.length - 3; A += 3, w += 3 * I) { + o = r[w] * e + m; + F = q[w] * p + k; + n = r[w + 1] * e + m; + E = q[w + 1] * p + k; + h = u.roundPixel(C[A + 3]); + g = C[A + 4]; + B = u.roundPixel(C[A]); + a = C[A + 1]; + if (s) { + l.x0 = B; + l.y0 = a; + l.cx1 = o; + l.cy1 = F; + l.cx2 = n; + l.cy2 = E; + l.x = h; + l.y = g; + D = [G, l, G.rendererData, c + A / 3 + 1]; + H = Ext.callback(s, null, D, 0, G.getSeries()); + v.save(); + Ext.apply(v, H) + } + if (t.fillArea) { + v.moveTo(B, a); + v.bezierCurveTo(o, F, n, E, h, g); + v.lineTo(h, f); + v.lineTo(B, f); + v.lineTo(B, a); + v.closePath(); + v.fill(); + v.beginPath() + } + v.moveTo(B, a); + v.bezierCurveTo(o, F, n, E, h, g); + v.stroke(); + v.moveTo(B, a); + v.closePath(); + if (s) { + v.restore() + } + v.beginPath(); + v.moveTo(h, g) + } + v.beginPath() + }, + drawLabel: function(k, i, h, o, a) { + var q = this, + n = q.attr, + e = q.getMarker("labels"), + d = e.getTemplate(), + m = q.labelCfg || (q.labelCfg = {}), + c = q.surfaceMatrix, + g, f, j = n.labelOverflowPadding, + l, b, r, p, s; + m.x = c.x(i, h); + m.y = c.y(i, h); + if (n.flipXY) { + m.rotationRads = Math.PI * 0.5 + } else { + m.rotationRads = 0 + } + m.text = k; + if (d.attr.renderer) { + p = [k, e, m, q.rendererData, o]; + r = Ext.callback(d.attr.renderer, null, p, 0, q.getSeries()); + if (typeof r === "string") { + m.text = r + } else { + if (typeof r === "object") { + if ("text" in r) { + m.text = r.text + } + s = true + } + } + } + b = q.getMarkerBBox("labels", o, true); + if (!b) { + q.putMarker("labels", m, o); + b = q.getMarkerBBox("labels", o, true) + } + l = b.height / 2; + g = i; + switch (d.attr.display) { + case "under": + f = h - l - j; + break; + case "rotate": + g += j; + f = h - j; + m.rotationRads = -Math.PI / 4; + break; + default: + f = h + l + j + } + m.x = c.x(g, f); + m.y = c.y(g, f); + if (s) { + Ext.apply(m, r) + } + q.putMarker("labels", m, o) + }, + drawMarker: function(j, h, d) { + var g = this, + e = g.attr, + f = e.renderer, + c = g.surfaceMatrix, + b = {}, + i, a; + if (f && g.getMarker("markers")) { + b.type = "marker"; + b.x = j; + b.y = h; + a = [g, b, g.rendererData, d]; + i = Ext.callback(f, null, a, 0, g.getSeries()); + if (i) { + Ext.apply(b, i) + } + } + b.translationX = c.x(j, h); + b.translationY = c.y(j, h); + delete b.x; + delete b.y; + g.putMarker("markers", b, d, !f) + }, + drawStroke: function(a, c, h, b, f, e) { + var d = this, + g = d.attr.smooth && d.smoothX && d.smoothY; + if (g) { + d.drawSmoothStroke(a, c, h, b, f, e) + } else { + d.drawStraightStroke(a, c, h, b, f, e) + } + }, + renderAggregates: function(B, w, l, N, o, I, D) { + var m = this, + k = m.attr, + s = k.dataX, + r = k.dataY, + h = k.labels, + v = k.xAxis, + a = k.yCap, + g = k.smooth && m.smoothX && m.smoothY, + d = h && m.getMarker("labels"), + t = m.getMarker("markers"), + E = k.matrix, + u = N.devicePixelRatio, + C = E.getXX(), + f = E.getYY(), + c = E.getDX(), + b = E.getDY(), + q = m.list || (m.list = []), + F = B.minX, + e = B.maxX, + j = B.minY, + P = B.maxY, + U = B.startIdx, + S = true, + Q, T, L, K, R, G; + m.rendererData = { + store: m.getStore() + }; + q.length = 0; + for (R = w; R < l; R++) { + var O = F[R], + p = e[R], + M = j[R], + n = P[R]; + if (O < p) { + q.push(O * C + c, M * f + b, U[R]); + q.push(p * C + c, n * f + b, U[R]) + } else { + if (O > p) { + q.push(p * C + c, n * f + b, U[R]); + q.push(O * C + c, M * f + b, U[R]) + } else { + q.push(p * C + c, n * f + b, U[R]) + } + } + } + if (q.length) { + for (R = 0; R < q.length; R += 3) { + L = q[R]; + K = q[R + 1]; + if (Ext.isNumber(L + K)) { + if (K > a) { + K = a + } else { + if (K < -a) { + K = -a + } + } + q[R + 1] = K + } else { + S = false; + continue + } + G = q[R + 2]; + if (t) { + m.drawMarker(L, K, G) + } + if (d && h[G]) { + m.drawLabel(h[G], L, K, G, D) + } + } + m.isContinuousLine = S; + if (g && !S) { + Ext.raise("Line smoothing in only supported for gapless data, where all data points are finite numbers.") + } + if (v) { + T = v.getAlignment() === "vertical"; + if (Ext.isNumber(v.floatingAtCoord)) { + Q = (T ? D[2] : D[3]) - v.floatingAtCoord + } else { + Q = T ? D[0] : D[1] + } + } else { + Q = k.flipXY ? D[0] : D[1] + } + if (k.preciseStroke) { + if (k.fillArea) { + o.fill() + } + if (k.transformFillStroke) { + k.inverseMatrix.toContext(o) + } + m.drawStroke(N, o, w, l, q, Q); + if (k.transformFillStroke) { + k.matrix.toContext(o) + } + o.stroke() + } else { + m.drawStroke(N, o, w, l, q, Q); + if (S && g && k.fillArea && !k.renderer) { + var A = s[s.length - 1] * C + c + u, + z = r[r.length - 1] * f + b, + J = s[0] * C + c - u, + H = r[0] * f + b; + o.lineTo(A, z); + o.lineTo(A, Q - k.lineWidth); + o.lineTo(J, Q - k.lineWidth); + o.lineTo(J, H) + } + if (k.transformFillStroke) { + k.matrix.toContext(o) + } + if (k.fillArea) { + o.fillStroke(k, true) + } else { + o.stroke(true) + } + } + } + } +}); +Ext.define("Ext.chart.series.Line", { + extend: "Ext.chart.series.Cartesian", + alias: "series.line", + type: "line", + seriesType: "lineSeries", + requires: ["Ext.chart.series.sprite.Line"], + config: { + selectionTolerance: 20, + smooth: false, + step: false, + fill: undefined, + aggregator: { + strategy: "double" + } + }, + defaultSmoothness: 3, + overflowBuffer: 1, + themeMarkerCount: function() { + return 1 + }, + getDefaultSpriteConfig: function() { + var d = this, + e = d.callParent(arguments), + c = Ext.apply({}, d.getStyle()), + b, a = false; + if (typeof d.config.fill != "undefined") { + if (d.config.fill) { + a = true; + if (typeof c.fillStyle == "undefined") { + if (typeof c.strokeStyle == "undefined") { + b = d.getStyleWithTheme(); + c.fillStyle = b.fillStyle; + c.strokeStyle = b.strokeStyle + } else { + c.fillStyle = c.strokeStyle + } + } + } + } else { + if (c.fillStyle) { + a = true + } + } + if (!a) { + delete c.fillStyle + } + c = Ext.apply(e || {}, c); + return Ext.apply(c, { + fillArea: a, + step: d.config.step, + smooth: d.config.smooth, + selectionTolerance: d.config.selectionTolerance + }) + }, + updateStep: function(b) { + var a = this.getSprites()[0]; + if (a && a.attr.step !== b) { + a.setAttributes({ + step: b + }) + } + }, + updateFill: function(b) { + var a = this.getSprites()[0]; + if (a && a.attr.fillArea !== b) { + a.setAttributes({ + fillArea: b + }) + } + }, + updateSmooth: function(a) { + var b = this.getSprites()[0]; + if (b && b.attr.smooth !== a) { + b.setAttributes({ + smooth: a + }) + } + } +}); +Ext.define("Ext.chart.series.sprite.PieSlice", { + extend: "Ext.draw.sprite.Sector", + mixins: { + markerHolder: "Ext.chart.MarkerHolder" + }, + alias: "sprite.pieslice", + inheritableStatics: { + def: { + processors: { + doCallout: "bool", + label: "string", + rotateLabels: "bool", + labelOverflowPadding: "number", + renderer: "default" + }, + defaults: { + doCallout: true, + rotateLabels: true, + label: "", + labelOverflowPadding: 10, + renderer: null + } + } + }, + config: { + rendererData: null, + rendererIndex: 0, + series: null + }, + setGradientBBox: function(q, k) { + var j = this, + i = j.attr, + g = (i.fillStyle && i.fillStyle.isGradient) || (i.strokeStyle && i.strokeStyle.isGradient); + if (g && !i.constrainGradients) { + var b = j.getMidAngle(), + d = i.margin, + e = i.centerX, + c = i.centerY, + a = i.endRho, + l = i.matrix, + o = l.getScaleX(), + n = l.getScaleY(), + m = o * a, + f = n * a, + p = { + width: m + m, + height: f + f + }; + if (d) { + e += d * Math.cos(b); + c += d * Math.sin(b) + } + p.x = l.x(e, c) - m; + p.y = l.y(e, c) - f; + q.setGradientBBox(p) + } else { + j.callParent([q, k]) + } + }, + render: function(b, c, g, f) { + var e = this, + a = e.attr, + h = {}, + d; + if (a.renderer) { + h = { + type: "sector", + text: a.text, + centerX: a.centerX, + centerY: a.centerY, + margin: a.margin, + startAngle: Math.min(a.startAngle, a.endAngle), + endAngle: Math.max(a.startAngle, a.endAngle), + startRho: Math.min(a.startRho, a.endRho), + endRho: Math.max(a.startRho, a.endRho) + }; + d = Ext.callback(a.renderer, null, [e, h, e.rendererData, e.rendererIndex], 0, e.getSeries()); + e.setAttributes(d); + e.useAttributes(c, g) + } + e.callParent([b, c, g, f]); + if (a.label && e.getMarker("labels")) { + e.placeLabel() + } + }, + placeLabel: function() { + var z = this, + s = z.attr, + r = s.attributeId, + t = Math.min(s.startAngle, s.endAngle), + p = Math.max(s.startAngle, s.endAngle), + k = (t + p) * 0.5, + n = s.margin, + h = s.centerX, + g = s.centerY, + f = Math.sin(k), + c = Math.cos(k), + v = Math.min(s.startRho, s.endRho) + n, + m = Math.max(s.startRho, s.endRho) + n, + l = (v + m) * 0.5, + b = z.surfaceMatrix, + o = z.labelCfg || (z.labelCfg = {}), + e = z.getMarker("labels"), + d = e.getTemplate(), + a = d.getCalloutLine(), + q = a && a.length || 40, + u, j, i, A, w; + b.appendMatrix(s.matrix); + o.text = s.label; + j = h + c * l; + i = g + f * l; + o.x = b.x(j, i); + o.y = b.y(j, i); + j = h + c * m; + i = g + f * m; + o.calloutStartX = b.x(j, i); + o.calloutStartY = b.y(j, i); + j = h + c * (m + q); + i = g + f * (m + q); + o.calloutPlaceX = b.x(j, i); + o.calloutPlaceY = b.y(j, i); + if (!s.rotateLabels) { + o.rotationRads = 0 + } else { + switch (d.attr.orientation) { + case "horizontal": + o.rotationRads = k + Math.atan2(b.y(1, 0) - b.y(0, 0), b.x(1, 0) - b.x(0, 0)) + Math.PI / 2; + break; + case "vertical": + o.rotationRads = k + Math.atan2(b.y(1, 0) - b.y(0, 0), b.x(1, 0) - b.x(0, 0)); + break + } + } + o.calloutColor = (a && a.color) || z.attr.fillStyle; + if (a) { + if (a.width) { + o.calloutWidth = a.width + } + } else { + o.calloutHasLine = false + } + o.globalAlpha = s.globalAlpha * s.fillOpacity; + o.hidden = (s.startAngle == s.endAngle); + if (d.attr.renderer) { + w = [z.attr.label, e, o, z.rendererData, z.rendererIndex]; + A = Ext.callback(d.attr.renderer, null, w, 0, z.getSeries()); + if (typeof A === "string") { + o.text = A + } else { + Ext.apply(o, A) + } + } + z.putMarker("labels", o, r); + u = z.getMarkerBBox("labels", r, true); + if (u) { + if (s.doCallout) { + if (d.attr.display === "outside") { + z.putMarker("labels", { + callout: 1 + }, r) + } else { + if (d.attr.display === "inside") { + z.putMarker("labels", { + callout: 0 + }, r) + } else { + z.putMarker("labels", { + callout: 1 - z.sliceContainsLabel(s, u) + }, r) + } + } + } else { + z.putMarker("labels", { + globalAlpha: z.sliceContainsLabel(s, u) + }, r) + } + } + }, + sliceContainsLabel: function(d, f) { + var e = d.labelOverflowPadding, + h = (d.endRho + d.startRho) / 2, + g = h + (f.width + e) / 2, + i = h - (f.width + e) / 2, + j, c, b, a; + if (e < 0) { + return 1 + } + if (f.width + e * 2 > (d.endRho - d.startRho)) { + return 0 + } + c = Math.sqrt(d.endRho * d.endRho - g * g); + b = Math.sqrt(d.endRho * d.endRho - i * i); + j = Math.abs(d.endAngle - d.startAngle); + a = (j > Math.PI / 2 ? i : Math.abs(Math.tan(j / 2)) * i); + if (f.height + e * 2 > Math.min(c, b, a) * 2) { + return 0 + } + return 1 + } +}); +Ext.define("Ext.chart.series.Pie", { + extend: "Ext.chart.series.Polar", + requires: ["Ext.chart.series.sprite.PieSlice"], + type: "pie", + alias: "series.pie", + seriesType: "pieslice", + config: { + donut: 0, + rotation: 0, + clockwise: true, + totalAngle: 2 * Math.PI, + hidden: [], + radiusFactor: 100, + highlightCfg: { + margin: 20 + }, + style: {} + }, + directions: ["X"], + applyLabel: function(a, b) { + if (Ext.isObject(a) && !Ext.isString(a.orientation)) { + Ext.apply(a = Ext.Object.chain(a), { + orientation: "vertical" + }) + } + return this.callParent([a, b]) + }, + updateLabelData: function() { + var h = this, + j = h.getStore(), + g = j.getData().items, + e = h.getSprites(), + a = h.getLabel().getTemplate().getField(), + d = h.getHidden(), + b, f, c, k; + if (e.length && a) { + c = []; + for (b = 0, f = g.length; b < f; b++) { + c.push(g[b].get(a)) + } + for (b = 0, f = e.length; b < f; b++) { + k = e[b]; + k.setAttributes({ + label: c[b] + }); + k.putMarker("labels", { + hidden: d[b] + }, k.attr.attributeId) + } + } + }, + coordinateX: function() { + var t = this, + f = t.getStore(), + q = f.getData().items, + c = q.length, + b = t.getXField(), + e = t.getYField(), + l, a = 0, + m, k, s = 0, + o = t.getHidden(), + d = [], + p, g = 0, + h = t.getTotalAngle(), + r = t.getClockwise() ? 1 : -1, + j = t.getSprites(), + n; + if (!j) { + return + } + for (p = 0; p < c; p++) { + l = Math.abs(Number(q[p].get(b))) || 0; + k = e && Math.abs(Number(q[p].get(e))) || 0; + if (!o[p]) { + a += l; + if (k > s) { + s = k + } + } + d[p] = a; + if (p >= o.length) { + o[p] = false + } + } + o.length = c; + t.maxY = s; + if (a !== 0) { + m = h / a + } + for (p = 0; p < c; p++) { + j[p].setAttributes({ + startAngle: g, + endAngle: g = (m ? r * d[p] * m : 0), + globalAlpha: 1 + }) + } + if (c < t.sprites.length) { + for (p = c; p < t.sprites.length; p++) { + n = t.sprites[p]; + n.getMarker("labels").clear(n.getId()); + n.releaseMarker("labels"); + n.destroy() + } + t.sprites.length = c + } + for (p = c; p < t.sprites.length; p++) { + j[p].setAttributes({ + startAngle: h, + endAngle: h, + globalAlpha: 0 + }) + } + t.getChart().refreshLegendStore() + }, + updateCenter: function(a) { + this.setStyle({ + translationX: a[0] + this.getOffsetX(), + translationY: a[1] + this.getOffsetY() + }); + this.doUpdateStyles() + }, + updateRadius: function(a) { + this.setStyle({ + startRho: a * this.getDonut() * 0.01, + endRho: a * this.getRadiusFactor() * 0.01 + }); + this.doUpdateStyles() + }, + getStyleByIndex: function(c) { + var g = this, + j = g.getStore(), + k = j.getAt(c), + f = g.getYField(), + d = g.getRadius(), + a = {}, + e, b, h; + if (k) { + h = f && Math.abs(Number(k.get(f))) || 0; + e = d * g.getDonut() * 0.01; + b = d * g.getRadiusFactor() * 0.01; + a = g.callParent([c]); + a.startRho = e; + a.endRho = g.maxY ? (e + (b - e) * h / g.maxY) : b + } + return a + }, + updateDonut: function(b) { + var a = this.getRadius(); + this.setStyle({ + startRho: a * b * 0.01, + endRho: a * this.getRadiusFactor() * 0.01 + }); + this.doUpdateStyles() + }, + rotationOffset: -Math.PI / 2, + updateRotation: function(a) { + this.setStyle({ + rotationRads: a + this.rotationOffset + }); + this.doUpdateStyles() + }, + updateTotalAngle: function(a) { + this.processData() + }, + getSprites: function() { + var k = this, + h = k.getChart(), + n = k.getStore(); + if (!h || !n) { + return [] + } + k.getColors(); + k.getSubStyle(); + var j = n.getData().items, + b = j.length, + d = k.getAnimation() || h && h.getAnimation(), + g = k.sprites, + o, l = 0, + f, e, c = false, + m = k.getLabel(), + a = m.getTemplate(); + f = { + store: n, + field: k.getXField(), + angleField: k.getXField(), + radiusField: k.getYField(), + series: k + }; + for (e = 0; e < b; e++) { + o = g[e]; + if (!o) { + o = k.createSprite(); + if (k.getHighlight()) { + o.config.highlight = k.getHighlight(); + o.addModifier("highlight", true) + } + if (a.getField()) { + a.setAttributes({ + labelOverflowPadding: k.getLabelOverflowPadding() + }); + a.fx.setCustomDurations({ + callout: 200 + }) + } + o.setAttributes(k.getStyleByIndex(e)); + o.rendererData = f; + o.rendererIndex = l++; + c = true + } + o.setAnimation(d) + } + if (c) { + k.doUpdateStyles() + } + return k.sprites + }, + betweenAngle: function(d, f, c) { + var e = Math.PI * 2, + g = this.rotationOffset; + if (!this.getClockwise()) { + d *= -1; + f *= -1; + c *= -1; + f -= g; + c -= g + } else { + f += g; + c += g + } + d -= f; + c -= f; + d %= e; + c %= e; + d += e; + c += e; + d %= e; + c %= e; + return d < c || c === 0 + }, + getItemForAngle: function(a) { + var h = this, + f = h.getSprites(), + d; + a %= Math.PI * 2; + while (a < 0) { + a += Math.PI * 2 + } + if (f) { + var j = h.getStore(), + g = j.getData().items, + c = h.getHidden(), + b = 0, + e = j.getCount(); + for (; b < e; b++) { + if (!c[b]) { + d = f[b].attr; + if (d.startAngle <= a && d.endAngle >= a) { + return { + series: h, + sprite: f[b], + index: b, + record: g[b], + field: h.getXField() + } + } + } + } + } + return null + }, + getItemForPoint: function(f, e) { + var t = this, + c = t.getSprites(); + if (c) { + var s = t.getCenter(), + q = t.getOffsetX(), + p = t.getOffsetY(), + j = f - s[0] + q, + h = e - s[1] + p, + b = t.getStore(), + g = t.getDonut(), + o = b.getData().items, + r = Math.atan2(h, j) - t.getRotation(), + a = Math.sqrt(j * j + h * h), + l = t.getRadius() * g * 0.01, + m = t.getHidden(), + n, d, k; + for (n = 0, d = o.length; n < d; n++) { + if (!m[n]) { + k = c[n].attr; + if (a >= l + k.margin && a <= k.endRho + k.margin) { + if (t.betweenAngle(r, k.startAngle, k.endAngle)) { + return { + series: t, + sprite: c[n], + index: n, + record: o[n], + field: t.getXField() + } + } + } + } + } + return null + } + }, + provideLegendInfo: function(f) { + var h = this, + j = h.getStore(); + if (j) { + var g = j.getData().items, + b = h.getLabel().getTemplate().getField(), + c = h.getXField(), + e = h.getHidden(), + d, a, k; + for (d = 0; d < g.length; d++) { + a = h.getStyleByIndex(d); + k = a.fillStyle; + if (Ext.isObject(k)) { + k = k.stops && k.stops[0].color + } + f.push({ + name: b ? String(g[d].get(b)) : c + " " + d, + mark: k || a.strokeStyle || "black", + disabled: e[d], + series: h.getId(), + index: d + }) + } + } + } +}); +Ext.define("Ext.chart.series.sprite.Pie3DPart", { + extend: "Ext.draw.sprite.Path", + mixins: { + markerHolder: "Ext.chart.MarkerHolder" + }, + alias: "sprite.pie3dPart", + inheritableStatics: { + def: { + processors: { + centerX: "number", + centerY: "number", + startAngle: "number", + endAngle: "number", + startRho: "number", + endRho: "number", + margin: "number", + thickness: "number", + bevelWidth: "number", + distortion: "number", + baseColor: "color", + colorSpread: "number", + baseRotation: "number", + part: "enums(top,bottom,start,end,innerFront,innerBack,outerFront,outerBack)", + label: "string" + }, + aliases: { + rho: "endRho" + }, + triggers: { + centerX: "path,bbox", + centerY: "path,bbox", + startAngle: "path,partZIndex", + endAngle: "path,partZIndex", + startRho: "path", + endRho: "path,bbox", + margin: "path,bbox", + thickness: "path", + distortion: "path", + baseRotation: "path,partZIndex", + baseColor: "partZIndex,partColor", + colorSpread: "partColor", + part: "path,partZIndex", + globalAlpha: "canvas,alpha" + }, + defaults: { + centerX: 0, + centerY: 0, + startAngle: Math.PI * 2, + endAngle: Math.PI * 2, + startRho: 0, + endRho: 150, + margin: 0, + thickness: 35, + distortion: 0.5, + baseRotation: 0, + baseColor: "white", + colorSpread: 1, + miterLimit: 1, + bevelWidth: 5, + strokeOpacity: 0, + part: "top", + label: "" + }, + updaters: { + alpha: "alphaUpdater", + partColor: "partColorUpdater", + partZIndex: "partZIndexUpdater" + } + } + }, + bevelParams: [], + constructor: function(a) { + this.callParent([a]); + this.bevelGradient = new Ext.draw.gradient.Linear({ + stops: [{ + offset: 0, + color: "rgba(255,255,255,0)" + }, { + offset: 0.7, + color: "rgba(255,255,255,0.6)" + }, { + offset: 1, + color: "rgba(255,255,255,0)" + }] + }) + }, + alphaUpdater: function(a) { + var d = this, + c = a.globalAlpha, + b = d.oldOpacity; + if (c !== b && (c === 1 || b === 1)) { + d.scheduleUpdater(a, "path", ["globalAlpha"]); + d.oldOpacity = c + } + }, + partColorUpdater: function(a) { + var d = Ext.draw.Color.fly(a.baseColor), + b = d.toString(), + e = a.colorSpread, + c; + switch (a.part) { + case "top": + c = new Ext.draw.gradient.Radial({ + start: { + x: 0, + y: 0, + r: 0 + }, + end: { + x: 0, + y: 0, + r: 1 + }, + stops: [{ + offset: 0, + color: d.createLighter(0.1 * e) + }, { + offset: 1, + color: d.createDarker(0.1 * e) + }] + }); + break; + case "bottom": + c = new Ext.draw.gradient.Radial({ + start: { + x: 0, + y: 0, + r: 0 + }, + end: { + x: 0, + y: 0, + r: 1 + }, + stops: [{ + offset: 0, + color: d.createDarker(0.2 * e) + }, { + offset: 1, + color: d.toString() + }] + }); + break; + case "outerFront": + case "outerBack": + c = new Ext.draw.gradient.Linear({ + stops: [{ + offset: 0, + color: d.createDarker(0.15 * e).toString() + }, { + offset: 0.3, + color: b + }, { + offset: 0.8, + color: d.createLighter(0.2 * e).toString() + }, { + offset: 1, + color: d.createDarker(0.25 * e).toString() + }] + }); + break; + case "start": + c = new Ext.draw.gradient.Linear({ + stops: [{ + offset: 0, + color: d.createDarker(0.1 * e).toString() + }, { + offset: 1, + color: d.createLighter(0.2 * e).toString() + }] + }); + break; + case "end": + c = new Ext.draw.gradient.Linear({ + stops: [{ + offset: 0, + color: d.createDarker(0.1 * e).toString() + }, { + offset: 1, + color: d.createLighter(0.2 * e).toString() + }] + }); + break; + case "innerFront": + case "innerBack": + c = new Ext.draw.gradient.Linear({ + stops: [{ + offset: 0, + color: d.createDarker(0.1 * e).toString() + }, { + offset: 0.2, + color: d.createLighter(0.2 * e).toString() + }, { + offset: 0.7, + color: b + }, { + offset: 1, + color: d.createDarker(0.1 * e).toString() + }] + }); + break + } + a.fillStyle = c; + a.canvasAttributes.fillStyle = c + }, + partZIndexUpdater: function(a) { + var c = Ext.draw.sprite.AttributeParser.angle, + e = a.baseRotation, + d = a.startAngle, + b = a.endAngle, + f; + switch (a.part) { + case "top": + a.zIndex = 5; + break; + case "outerFront": + d = c(d + e); + b = c(b + e); + if (d >= 0 && b < 0) { + f = Math.sin(d) + } else { + if (d <= 0 && b > 0) { + f = Math.sin(b) + } else { + if (d >= 0 && b > 0) { + if (d > b) { + f = 0 + } else { + f = Math.max(Math.sin(d), Math.sin(b)) + } + } else { + f = 1 + } + } + } + a.zIndex = 4 + f; + break; + case "outerBack": + a.zIndex = 1; + break; + case "start": + a.zIndex = 4 + Math.sin(c(d + e)); + break; + case "end": + a.zIndex = 4 + Math.sin(c(b + e)); + break; + case "innerFront": + a.zIndex = 2; + break; + case "innerBack": + a.zIndex = 4 + Math.sin(c((d + b) / 2 + e)); + break; + case "bottom": + a.zIndex = 0; + break + } + a.dirtyZIndex = true + }, + updatePlainBBox: function(k) { + var f = this.attr, + a = f.part, + b = f.baseRotation, + e = f.centerX, + d = f.centerY, + j, c, i, h, g, l; + if (a === "start") { + c = f.startAngle + b + } else { + if (a === "end") { + c = f.endAngle + b + } + } + if (Ext.isNumber(c)) { + g = Math.sin(c); + l = Math.cos(c); + i = Math.min(e + l * f.startRho, e + l * f.endRho); + h = d + g * f.startRho * f.distortion; + k.x = i; + k.y = h; + k.width = l * (f.endRho - f.startRho); + k.height = f.thickness + g * (f.endRho - f.startRho) * 2; + return + } + if (a === "innerFront" || a === "innerBack") { + j = f.startRho + } else { + j = f.endRho + } + k.width = j * 2; + k.height = j * f.distortion * 2 + f.thickness; + k.x = f.centerX - j; + k.y = f.centerY - j * f.distortion + }, + updateTransformedBBox: function(a) { + if (this.attr.part === "start" || this.attr.part === "end") { + return this.callParent(arguments) + } + return this.updatePlainBBox(a) + }, + updatePath: function(a) { + if (!this.attr.globalAlpha) { + return + } + if (this.attr.endAngle < this.attr.startAngle) { + return + } + this[this.attr.part + "Renderer"](a) + }, + render: function(b, c) { + var d = this, + a = d.attr; + if (!a.globalAlpha) { + return + } + d.callParent([b, c]); + d.bevelRenderer(b, c); + if (a.label && d.getMarker("labels")) { + d.placeLabel() + } + }, + placeLabel: function() { + var z = this, + u = z.attr, + t = u.attributeId, + p = u.margin, + c = u.distortion, + i = u.centerX, + h = u.centerY, + j = u.baseRotation, + v = u.startAngle + j, + r = u.endAngle + j, + m = (v + r) / 2, + w = u.startRho + p, + o = u.endRho + p, + n = (w + o) / 2, + a = Math.sin(m), + b = Math.cos(m), + e = z.surfaceMatrix, + g = z.getMarker("labels"), + f = g.getTemplate(), + d = f.getCalloutLine(), + s = d && d.length || 40, + q = {}, + l, k; + e.appendMatrix(u.matrix); + q.text = u.label; + l = i + b * n; + k = h + a * n * c; + q.x = e.x(l, k); + q.y = e.y(l, k); + l = i + b * o; + k = h + a * o * c; + q.calloutStartX = e.x(l, k); + q.calloutStartY = e.y(l, k); + l = i + b * (o + s); + k = h + a * (o + s) * c; + q.calloutPlaceX = e.x(l, k); + q.calloutPlaceY = e.y(l, k); + q.calloutWidth = 2; + z.putMarker("labels", q, t); + z.putMarker("labels", { + callout: 1 + }, t) + }, + bevelRenderer: function(b, c) { + var f = this, + a = f.attr, + e = a.bevelWidth, + g = f.bevelParams, + d; + for (d = 0; d < g.length; d++) { + c.beginPath(); + c.ellipse.apply(c, g[d]); + c.save(); + c.lineWidth = e; + c.strokeOpacity = e ? 1 : 0; + c.strokeGradient = f.bevelGradient; + c.stroke(a); + c.restore() + } + }, + lidRenderer: function(o, m) { + var k = this.attr, + g = k.margin, + c = k.distortion, + i = k.centerX, + h = k.centerY, + f = k.baseRotation, + j = k.startAngle + f, + e = k.endAngle + f, + d = (j + e) / 2, + l = k.startRho, + b = k.endRho, + n = Math.sin(e), + a = Math.cos(e); + i += Math.cos(d) * g; + h += Math.sin(d) * g * c; + o.ellipse(i, h + m, l, l * c, 0, j, e, false); + o.lineTo(i + a * b, h + m + n * b * c); + o.ellipse(i, h + m, b, b * c, 0, e, j, true); + o.closePath() + }, + topRenderer: function(a) { + this.lidRenderer(a, 0) + }, + bottomRenderer: function(b) { + var a = this.attr; + if (a.globalAlpha < 1 || a.shadowColor !== Ext.draw.Color.RGBA_NONE) { + this.lidRenderer(b, a.thickness) + } + }, + sideRenderer: function(l, s) { + var o = this.attr, + k = o.margin, + g = o.centerX, + f = o.centerY, + e = o.distortion, + h = o.baseRotation, + p = o.startAngle + h, + m = o.endAngle + h, + a = o.thickness, + q = o.startRho, + j = o.endRho, + r = (s === "start" && p) || (s === "end" && m), + b = Math.sin(r), + d = Math.cos(r), + c = o.globalAlpha < 1, + n = s === "start" && d < 0 || s === "end" && d > 0 || c, + i; + if (n) { + i = (p + m) / 2; + g += Math.cos(i) * k; + f += Math.sin(i) * k * e; + l.moveTo(g + d * q, f + b * q * e); + l.lineTo(g + d * j, f + b * j * e); + l.lineTo(g + d * j, f + b * j * e + a); + l.lineTo(g + d * q, f + b * q * e + a); + l.closePath() + } + }, + startRenderer: function(a) { + this.sideRenderer(a, "start") + }, + endRenderer: function(a) { + this.sideRenderer(a, "end") + }, + rimRenderer: function(q, e, o, j) { + var w = this, + s = w.attr, + p = s.margin, + h = s.centerX, + g = s.centerY, + d = s.distortion, + i = s.baseRotation, + t = Ext.draw.sprite.AttributeParser.angle, + u = s.startAngle + i, + r = s.endAngle + i, + k = t((u + r) / 2), + a = s.thickness, + b = s.globalAlpha < 1, + c, n, v; + w.bevelParams = []; + u = t(u); + r = t(r); + h += Math.cos(k) * p; + g += Math.sin(k) * p * d; + c = u >= 0 && r >= 0; + n = u <= 0 && r <= 0; + + function l() { + q.ellipse(h, g + a, e, e * d, 0, Math.PI, u, true); + q.lineTo(h + Math.cos(u) * e, g + Math.sin(u) * e * d); + v = [h, g, e, e * d, 0, u, Math.PI, false]; + if (!o) { + w.bevelParams.push(v) + } + q.ellipse.apply(q, v); + q.closePath() + } + + function f() { + q.ellipse(h, g + a, e, e * d, 0, 0, r, false); + q.lineTo(h + Math.cos(r) * e, g + Math.sin(r) * e * d); + v = [h, g, e, e * d, 0, r, 0, true]; + if (!o) { + w.bevelParams.push(v) + } + q.ellipse.apply(q, v); + q.closePath() + } + + function x() { + q.ellipse(h, g + a, e, e * d, 0, Math.PI, r, false); + q.lineTo(h + Math.cos(r) * e, g + Math.sin(r) * e * d); + v = [h, g, e, e * d, 0, r, Math.PI, true]; + if (o) { + w.bevelParams.push(v) + } + q.ellipse.apply(q, v); + q.closePath() + } + + function m() { + q.ellipse(h, g + a, e, e * d, 0, u, 0, false); + q.lineTo(h + e, g); + v = [h, g, e, e * d, 0, 0, u, true]; + if (o) { + w.bevelParams.push(v) + } + q.ellipse.apply(q, v); + q.closePath() + } + if (j) { + if (!o || b) { + if (u >= 0 && r < 0) { + l() + } else { + if (u <= 0 && r > 0) { + f() + } else { + if (u <= 0 && r < 0) { + if (u > r) { + q.ellipse(h, g + a, e, e * d, 0, 0, Math.PI, false); + q.lineTo(h - e, g); + v = [h, g, e, e * d, 0, Math.PI, 0, true]; + if (!o) { + w.bevelParams.push(v) + } + q.ellipse.apply(q, v); + q.closePath() + } + } else { + if (u > r) { + l(); + f() + } else { + v = [h, g, e, e * d, 0, u, r, false]; + if (c && !o || n && o) { + w.bevelParams.push(v) + } + q.ellipse.apply(q, v); + q.lineTo(h + Math.cos(r) * e, g + Math.sin(r) * e * d + a); + q.ellipse(h, g + a, e, e * d, 0, r, u, true); + q.closePath() + } + } + } + } + } + } else { + if (o || b) { + if (u >= 0 && r < 0) { + x() + } else { + if (u <= 0 && r > 0) { + m() + } else { + if (u <= 0 && r < 0) { + if (u > r) { + x(); + m() + } else { + q.ellipse(h, g + a, e, e * d, 0, u, r, false); + q.lineTo(h + Math.cos(r) * e, g + Math.sin(r) * e * d); + v = [h, g, e, e * d, 0, r, u, true]; + if (o) { + w.bevelParams.push(v) + } + q.ellipse.apply(q, v); + q.closePath() + } + } else { + if (u > r) { + q.ellipse(h, g + a, e, e * d, 0, -Math.PI, 0, false); + q.lineTo(h + e, g); + v = [h, g, e, e * d, 0, 0, -Math.PI, true]; + if (o) { + w.bevelParams.push(v) + } + q.ellipse.apply(q, v); + q.closePath() + } + } + } + } + } + } + }, + innerFrontRenderer: function(a) { + this.rimRenderer(a, this.attr.startRho, true, true) + }, + innerBackRenderer: function(a) { + this.rimRenderer(a, this.attr.startRho, true, false) + }, + outerFrontRenderer: function(a) { + this.rimRenderer(a, this.attr.endRho, false, true) + }, + outerBackRenderer: function(a) { + this.rimRenderer(a, this.attr.endRho, false, false) + } +}); +Ext.define("Ext.draw.PathUtil", function() { + var a = Math.abs, + c = Math.pow, + e = Math.cos, + b = Math.acos, + d = Math.sqrt, + f = Math.PI; + return { + singleton: true, + requires: ["Ext.draw.overrides.Path", "Ext.draw.overrides.sprite.Path", "Ext.draw.overrides.sprite.Instancing", "Ext.draw.overrides.Surface"], + cubicRoots: function(m) { + var z = m[0], + x = m[1], + w = m[2], + v = m[3]; + if (z === 0) { + return this.quadraticRoots(x, w, v) + } + var s = x / z, + r = w / z, + q = v / z, + k = (3 * r - c(s, 2)) / 9, + j = (9 * s * r - 27 * q - 2 * c(s, 3)) / 54, + p = c(k, 3) + c(j, 2), + n = [], + h, g, o, l, u, y = Ext.Number.sign; + if (p >= 0) { + h = y(j + d(p)) * c(a(j + d(p)), 1 / 3); + g = y(j - d(p)) * c(a(j - d(p)), 1 / 3); + n[0] = -s / 3 + (h + g); + n[1] = -s / 3 - (h + g) / 2; + n[2] = n[1]; + o = a(d(3) * (h - g) / 2); + if (o !== 0) { + n[1] = -1; + n[2] = -1 + } + } else { + l = b(j / d(-c(k, 3))); + n[0] = 2 * d(-k) * e(l / 3) - s / 3; + n[1] = 2 * d(-k) * e((l + 2 * f) / 3) - s / 3; + n[2] = 2 * d(-k) * e((l + 4 * f) / 3) - s / 3 + } + for (u = 0; u < 3; u++) { + if (n[u] < 0 || n[u] > 1) { + n[u] = -1 + } + } + return n + }, + quadraticRoots: function(h, g, n) { + var m, l, k, j; + if (h === 0) { + return this.linearRoot(g, n) + } + m = g * g - 4 * h * n; + if (m === 0) { + k = [-g / (2 * h)] + } else { + if (m > 0) { + l = d(m); + k = [(-g - l) / (2 * h), (-g + l) / (2 * h)] + } else { + return [] + } + } + for (j = 0; j < k.length; j++) { + if (k[j] < 0 || k[j] > 1) { + k[j] = -1 + } + } + return k + }, + linearRoot: function(h, g) { + var i = -g / h; + if (h === 0 || i < 0 || i > 1) { + return [] + } + return [i] + }, + bezierCoeffs: function(h, g, k, j) { + var i = []; + i[0] = -h + 3 * g - 3 * k + j; + i[1] = 3 * h - 6 * g + 3 * k; + i[2] = -3 * h + 3 * g; + i[3] = h; + return i + }, + cubicLineIntersections: function(I, G, F, E, l, k, j, h, M, p, K, n) { + var u = [], + N = [], + D = p - n, + z = K - M, + y = M * (n - p) - p * (K - M), + L = this.bezierCoeffs(I, G, F, E), + J = this.bezierCoeffs(l, k, j, h), + H, x, w, v, g, q, o, m; + u[0] = D * L[0] + z * J[0]; + u[1] = D * L[1] + z * J[1]; + u[2] = D * L[2] + z * J[2]; + u[3] = D * L[3] + z * J[3] + y; + x = this.cubicRoots(u); + for (H = 0; H < x.length; H++) { + v = x[H]; + if (v < 0 || v > 1) { + continue + } + g = v * v; + q = g * v; + o = L[0] * q + L[1] * g + L[2] * v + L[3]; + m = J[0] * q + J[1] * g + J[2] * v + J[3]; + if ((K - M) !== 0) { + w = (o - M) / (K - M) + } else { + w = (m - p) / (n - p) + } + if (!(w < 0 || w > 1)) { + N.push([o, m]) + } + } + return N + }, + splitCubic: function(g, q, p, o, m) { + var j = m * m, + n = m * j, + i = m - 1, + h = i * i, + k = i * h, + l = n * o - 3 * j * i * p + 3 * m * h * q - k * g; + return [ + [g, m * q - i * g, j * p - 2 * m * i * q + h * g, l], + [l, j * o - 2 * m * i * p + h * q, m * o - i * p, o] + ] + }, + cubicDimension: function(p, o, l, k) { + var j = 3 * (-p + 3 * (o - l) + k), + i = 6 * (p - 2 * o + l), + h = -3 * (p - o), + q, n, g = Math.min(p, k), + m = Math.max(p, k), + r; + if (j === 0) { + if (i === 0) { + return [g, m] + } else { + q = -h / i; + if (0 < q && q < 1) { + n = this.interpolateCubic(p, o, l, k, q); + g = Math.min(g, n); + m = Math.max(m, n) + } + } + } else { + r = i * i - 4 * j * h; + if (r >= 0) { + r = d(r); + q = (r - i) / 2 / j; + if (0 < q && q < 1) { + n = this.interpolateCubic(p, o, l, k, q); + g = Math.min(g, n); + m = Math.max(m, n) + } + if (r > 0) { + q -= r / j; + if (0 < q && q < 1) { + n = this.interpolateCubic(p, o, l, k, q); + g = Math.min(g, n); + m = Math.max(m, n) + } + } + } + } + return [g, m] + }, + interpolateCubic: function(h, g, l, k, i) { + if (i === 0) { + return h + } + if (i === 1) { + return k + } + var j = (1 - i) / i; + return i * i * i * (k + j * (3 * l + j * (3 * g + j * h))) + }, + cubicsIntersections: function(r, q, p, o, A, z, y, v, g, F, E, D, m, l, k, i) { + var C = this, + x = C.cubicDimension(r, q, p, o), + B = C.cubicDimension(A, z, y, v), + n = C.cubicDimension(g, F, E, D), + s = C.cubicDimension(m, l, k, i), + j, h, u, t, w = []; + if (x[0] > n[1] || x[1] < n[0] || B[0] > s[1] || B[1] < s[0]) { + return [] + } + if (a(A - z) < 1 && a(y - v) < 1 && a(r - o) < 1 && a(q - p) < 1 && a(m - l) < 1 && a(k - i) < 1 && a(g - D) < 1 && a(F - E) < 1) { + return [ + [(r + o) * 0.5, (A + z) * 0.5] + ] + } + j = C.splitCubic(r, q, p, o, 0.5); + h = C.splitCubic(A, z, y, v, 0.5); + u = C.splitCubic(g, F, E, D, 0.5); + t = C.splitCubic(m, l, k, i, 0.5); + w.push.apply(w, C.cubicsIntersections.apply(C, j[0].concat(h[0], u[0], t[0]))); + w.push.apply(w, C.cubicsIntersections.apply(C, j[0].concat(h[0], u[1], t[1]))); + w.push.apply(w, C.cubicsIntersections.apply(C, j[1].concat(h[1], u[0], t[0]))); + w.push.apply(w, C.cubicsIntersections.apply(C, j[1].concat(h[1], u[1], t[1]))); + return w + }, + linesIntersection: function(k, p, j, o, h, n, q, m) { + var l = (j - k) * (m - n) - (o - p) * (q - h), + i, g; + if (l === 0) { + return null + } + i = ((q - h) * (p - n) - (k - h) * (m - n)) / l; + g = ((j - k) * (p - n) - (o - p) * (k - h)) / l; + if (i >= 0 && i <= 1 && g >= 0 && g <= 1) { + return [k + i * (j - k), p + i * (o - p)] + } + return null + }, + pointOnLine: function(j, m, h, l, g, n) { + var k, i; + if (a(h - j) < a(l - m)) { + i = j; + j = m; + m = i; + i = h; + h = l; + l = i; + i = g; + g = n; + n = i + } + k = (g - j) / (h - j); + if (k < 0 || k > 1) { + return false + } + return a(m + k * (l - m) - n) < 4 + }, + pointOnCubic: function(w, u, s, r, l, k, h, g, p, o) { + var C = this, + B = C.bezierCoeffs(w, u, s, r), + A = C.bezierCoeffs(l, k, h, g), + z, v, n, m, q; + B[3] -= p; + A[3] -= o; + n = C.cubicRoots(B); + m = C.cubicRoots(A); + for (z = 0; z < n.length; z++) { + q = n[z]; + for (v = 0; v < m.length; v++) { + if (q >= 0 && q <= 1 && a(q - m[v]) < 0.05) { + return true + } + } + } + return false + } + } +}); +Ext.define("Ext.chart.series.Pie3D", { + extend: "Ext.chart.series.Polar", + requires: ["Ext.chart.series.sprite.Pie3DPart", "Ext.draw.PathUtil"], + type: "pie3d", + seriesType: "pie3d", + alias: "series.pie3d", + isPie3D: true, + config: { + rect: [0, 0, 0, 0], + thickness: 35, + distortion: 0.5, + donut: false, + hidden: [], + highlightCfg: { + margin: 20 + }, + shadow: false + }, + rotationOffset: -Math.PI / 2, + setField: function(a) { + return this.setXField(a) + }, + getField: function() { + return this.getXField() + }, + updateRotation: function(a) { + this.setStyle({ + baseRotation: a + this.rotationOffset + }); + this.doUpdateStyles() + }, + updateDistortion: function() { + this.setRadius() + }, + updateThickness: function() { + this.setRadius() + }, + updateColors: function(a) { + this.setSubStyle({ + baseColor: a + }) + }, + applyShadow: function(a) { + if (a === true) { + a = { + shadowColor: "rgba(0,0,0,0.8)", + shadowBlur: 30 + } + } else { + if (!Ext.isObject(a)) { + a = { + shadowColor: Ext.draw.Color.RGBA_NONE + } + } + } + return a + }, + updateShadow: function(g) { + var e = this, + f = e.getSprites(), + d = e.spritesPerSlice, + c = f && f.length, + b, a; + for (b = 1; b < c; b += d) { + a = f[b]; + if (a.attr.part = "bottom") { + a.setAttributes(g) + } + } + }, + getStyleByIndex: function(b) { + var d = this.callParent([b]), + c = this.getStyle(), + a = d.fillStyle || d.fill || d.color, + e = c.strokeStyle || c.stroke; + if (a) { + d.baseColor = a; + delete d.fillStyle; + delete d.fill; + delete d.color + } + if (e) { + d.strokeStyle = e + } + return d + }, + doUpdateStyles: function() { + var g = this, + h = g.getSprites(), + f = g.spritesPerSlice, + e = h && h.length, + c = 0, + b = 0, + a, d; + for (; c < e; c += f, b++) { + d = g.getStyleByIndex(b); + for (a = 0; a < f; a++) { + h[c + a].setAttributes(d) + } + } + }, + coordinateX: function() { + var w = this, + m = w.getChart(), + u = m && m.getAnimation(), + f = w.getStore(), + t = f.getData().items, + d = t.length, + b = w.getXField(), + p = w.getRotation(), + s = w.getHidden(), + n, c = 0, + h, e = [], + k = w.getSprites(), + a = k.length, + l = w.spritesPerSlice, + g = 0, + o = Math.PI * 2, + v = 1e-10, + r, q; + for (r = 0; r < d; r++) { + n = Math.abs(Number(t[r].get(b))) || 0; + if (!s[r]) { + c += n + } + e[r] = c; + if (r >= s.length) { + s[r] = false + } + } + s.length = d; + if (c === 0) { + return + } + h = 2 * Math.PI / c; + for (r = 0; r < d; r++) { + e[r] *= h + } + for (r = 0; r < a; r++) { + k[r].setAnimation(u) + } + for (r = 0; r < d; r++) { + for (q = 0; q < l; q++) { + k[r * l + q].setAttributes({ + startAngle: g, + endAngle: e[r] - v, + globalAlpha: 1, + baseRotation: p + }) + } + g = e[r] + } + for (r *= l; r < a; r++) { + k[r].setAnimation(u); + k[r].setAttributes({ + startAngle: o, + endAngle: o, + globalAlpha: 0, + baseRotation: p + }) + } + }, + updateLabelData: function() { + var l = this, + m = l.getStore(), + k = m.getData().items, + h = l.getSprites(), + b = l.getLabel().getTemplate().getField(), + f = l.getHidden(), + a = l.spritesPerSlice, + d, c, g, e, n; + if (h.length && b) { + e = []; + for (d = 0, g = k.length; d < g; d++) { + e.push(k[d].get(b)) + } + for (d = 0, c = 0, g = h.length; d < g; d += a, c++) { + n = h[d]; + n.setAttributes({ + label: e[c] + }); + n.putMarker("labels", { + hidden: f[c] + }, n.attr.attributeId) + } + } + }, + applyRadius: function() { + var f = this, + d = f.getChart(), + h = d.getInnerPadding(), + e = d.getMainRect() || [0, 0, 1, 1], + c = e[2] - h * 2, + a = e[3] - h * 2 - f.getThickness(), + g = c / 2, + b = g * f.getDistortion(); + if (b > a / 2) { + return a / (f.getDistortion() * 2) + } else { + return g + } + }, + getSprites: function() { + var y = this, + e = y.getStore(); + if (!e) { + return [] + } + var n = y.getChart(), + p = y.getSurface(), + t = e.getData().items, + l = y.spritesPerSlice, + a = t.length, + v = y.getAnimation() || n && n.getAnimation(), + x = y.getCenter(), + w = y.getOffsetX(), + u = y.getOffsetY(), + b = y.getRadius(), + q = y.getRotation(), + d = y.getHighlight(), + c = { + centerX: x[0] + w, + centerY: x[1] + u - y.getThickness() / 2, + endRho: b, + startRho: b * y.getDonut() / 100, + thickness: y.getThickness(), + distortion: y.getDistortion() + }, + k = y.sprites, + h = y.getLabel(), + f = h.getTemplate(), + m, g, o, s, r; + for (s = 0; s < a; s++) { + g = Ext.apply({}, this.getStyleByIndex(s), c); + if (!k[s * l]) { + for (r = 0; r < y.partNames.length; r++) { + o = p.add({ + type: "pie3dPart", + part: y.partNames[r] + }); + if (r === 0 && f.getField()) { + o.bindMarker("labels", h) + } + o.fx.setDurationOn("baseRotation", q); + if (d) { + o.config.highlight = d; + o.addModifier("highlight", true) + } + o.setAttributes(g); + k.push(o) + } + } else { + m = k.slice(s * l, (s + 1) * l); + for (r = 0; r < m.length; r++) { + o = m[r]; + if (v) { + o.setAnimation(v) + } + o.setAttributes(g) + } + } + } + return k + }, + betweenAngle: function(d, f, c) { + var e = Math.PI * 2, + g = this.rotationOffset; + f += g; + c += g; + d -= f; + c -= f; + d %= e; + c %= e; + d += e; + c += e; + d %= e; + c %= e; + return d < c || c === 0 + }, + getItemForPoint: function(k, j) { + var h = this, + g = h.getSprites(); + if (g) { + var l = h.getStore(), + b = l.getData().items, + a = h.spritesPerSlice, + e = h.getHidden(), + c, f, m, d; + for (c = 0, f = b.length; c < f; c++) { + if (!e[c]) { + d = c * a; + m = g[d]; + if (m.hitTest([k, j])) { + return { + series: h, + sprite: g.slice(d, d + a), + index: c, + record: b[c], + category: "sprites", + field: h.getXField() + } + } + } + } + return null + } + }, + provideLegendInfo: function(f) { + var h = this, + k = h.getStore(); + if (k) { + var g = k.getData().items, + b = h.getLabel().getTemplate().getField(), + j = h.getField(), + e = h.getHidden(), + d, a, c; + for (d = 0; d < g.length; d++) { + a = h.getStyleByIndex(d); + c = a.baseColor; + f.push({ + name: b ? String(g[d].get(b)) : j + " " + d, + mark: c || "black", + disabled: e[d], + series: h.getId(), + index: d + }) + } + } + } +}, function() { + var b = this.prototype, + a = Ext.chart.series.sprite.Pie3DPart.def.getInitialConfig().processors.part; + b.partNames = a.replace(/^enums\(|\)/g, "").split(","); + b.spritesPerSlice = b.partNames.length +}); +Ext.define("Ext.chart.series.sprite.Polar", { + extend: "Ext.chart.series.sprite.Series", + inheritableStatics: { + def: { + processors: { + centerX: "number", + centerY: "number", + startAngle: "number", + endAngle: "number", + startRho: "number", + endRho: "number", + baseRotation: "number", + labels: "default", + labelOverflowPadding: "number" + }, + defaults: { + centerX: 0, + centerY: 0, + startAngle: 0, + endAngle: Math.PI, + startRho: 0, + endRho: 150, + baseRotation: 0, + labels: null, + labelOverflowPadding: 10 + }, + triggers: { + centerX: "bbox", + centerY: "bbox", + startAngle: "bbox", + endAngle: "bbox", + startRho: "bbox", + endRho: "bbox", + baseRotation: "bbox" + } + } + }, + updatePlainBBox: function(b) { + var a = this.attr; + b.x = a.centerX - a.endRho; + b.y = a.centerY + a.endRho; + b.width = a.endRho * 2; + b.height = a.endRho * 2 + } +}); +Ext.define("Ext.chart.series.sprite.Radar", { + alias: "sprite.radar", + extend: "Ext.chart.series.sprite.Polar", + getDataPointXY: function(d) { + var u = this, + n = u.attr, + f = n.centerX, + e = n.centerY, + o = n.matrix, + t = n.dataMinX, + s = n.dataMaxX, + k = n.dataX, + j = n.dataY, + l = n.endRho, + p = n.startRho, + g = n.baseRotation, + i, h, m, c, b, a, q; + if (n.rangeY) { + q = n.rangeY[1] + } else { + q = n.dataMaxY + } + c = (k[d] - t) / (s - t + 1) * 2 * Math.PI + g; + m = j[d] / q * (l - p) + p; + b = f + Math.cos(c) * m; + a = e + Math.sin(c) * m; + i = o.x(b, a); + h = o.y(b, a); + return [i, h] + }, + render: function(a, l) { + var h = this, + f = h.attr, + g = f.dataX, + b = g.length, + e = h.surfaceMatrix, + d = {}, + c, k, j, m; + l.beginPath(); + for (c = 0; c < b; c++) { + m = h.getDataPointXY(c); + k = m[0]; + j = m[1]; + if (c === 0) { + l.moveTo(k, j) + } + l.lineTo(k, j); + d.translationX = e.x(k, j); + d.translationY = e.y(k, j); + h.putMarker("markers", d, c, true) + } + l.closePath(); + l.fillStroke(f) + } +}); +Ext.define("Ext.chart.series.Radar", { + extend: "Ext.chart.series.Polar", + type: "radar", + seriesType: "radar", + alias: "series.radar", + requires: ["Ext.chart.series.sprite.Radar"], + themeColorCount: function() { + return 1 + }, + isStoreDependantColorCount: false, + themeMarkerCount: function() { + return 1 + }, + updateAngularAxis: function(a) { + a.processData(this) + }, + updateRadialAxis: function(a) { + a.processData(this) + }, + coordinateX: function() { + return this.coordinate("X", 0, 2) + }, + coordinateY: function() { + return this.coordinate("Y", 1, 2) + }, + updateCenter: function(a) { + this.setStyle({ + translationX: a[0] + this.getOffsetX(), + translationY: a[1] + this.getOffsetY() + }); + this.doUpdateStyles() + }, + updateRadius: function(a) { + this.setStyle({ + endRho: a + }); + this.doUpdateStyles() + }, + updateRotation: function(a) { + this.setStyle({ + rotationRads: a + }); + this.doUpdateStyles() + }, + updateTotalAngle: function(a) { + this.processData() + }, + getItemForPoint: function(k, j) { + var h = this, + m = h.sprites && h.sprites[0], + f = m.attr, + g = f.dataX, + a = g.length, + l = h.getStore(), + e = h.getMarker(), + b, o, p, d, n, c; + if (h.getHidden()) { + return null + } + if (m && e) { + c = m.getMarker("markers"); + for (d = 0; d < a; d++) { + n = c.getBBoxFor(d); + b = (n.width + n.height) * 0.25; + p = m.getDataPointXY(d); + if (Math.abs(p[0] - k) < b && Math.abs(p[1] - j) < b) { + o = { + series: h, + sprite: m, + index: d, + category: "markers", + record: l.getData().items[d], + field: h.getYField() + }; + return o + } + } + } + return h.callParent(arguments) + }, + getDefaultSpriteConfig: function() { + var a = this.callParent(), + b = { + customDurations: { + translationX: 0, + translationY: 0, + rotationRads: 0, + dataMinX: 0, + dataMaxX: 0 + } + }; + if (a.fx) { + Ext.apply(a.fx, b) + } else { + a.fx = b + } + return a + }, + getSprites: function() { + var d = this, + c = d.getChart(), + e = d.getAnimation() || c && c.getAnimation(), + b = d.sprites[0], + a; + if (!c) { + return [] + } + if (!b) { + b = d.createSprite() + } + if (e) { + a = b.getMarker("markers"); + if (a) { + a.getTemplate().setAnimation(e) + } + b.setAnimation(e) + } + return d.sprites + }, + provideLegendInfo: function(d) { + var b = this, + a = b.getSubStyleWithTheme(), + c = a.fillStyle; + if (Ext.isArray(c)) { + c = c[0] + } + d.push({ + name: b.getTitle() || b.getYField() || b.getId(), + mark: (Ext.isObject(c) ? c.stops && c.stops[0].color : c) || a.strokeStyle || "black", + disabled: b.getHidden(), + series: b.getId(), + index: 0 + }) + } +}); +Ext.define("Ext.chart.series.sprite.Scatter", { + alias: "sprite.scatterSeries", + extend: "Ext.chart.series.sprite.Cartesian", + renderClipped: function(r, s, w, u) { + if (this.cleanRedraw) { + return + } + var C = this, + q = C.attr, + l = q.dataX, + h = q.dataY, + z = q.labels, + j = C.getSeries(), + b = z && C.getMarker("labels"), + t = C.attr.matrix, + c = t.getXX(), + p = t.getYY(), + m = t.getDX(), + k = t.getDY(), + n = {}, + D, B, d = r.getInherited().rtl && !q.flipXY ? -1 : 1, + a, A, o, e, g, f, v; + if (q.flipXY) { + a = u[1] - c * d; + A = u[1] + u[3] + c * d; + o = u[0] - p; + e = u[0] + u[2] + p + } else { + a = u[0] - c * d; + A = u[0] + u[2] + c * d; + o = u[1] - p; + e = u[1] + u[3] + p + } + for (v = 0; v < l.length; v++) { + g = l[v]; + f = h[v]; + g = g * c + m; + f = f * p + k; + if (a <= g && g <= A && o <= f && f <= e) { + if (q.renderer) { + n = { + type: "items", + translationX: g, + translationY: f + }; + B = [C, n, { + store: C.getStore() + }, v]; + D = Ext.callback(q.renderer, null, B, 0, j); + n = Ext.apply(n, D) + } else { + n.translationX = g; + n.translationY = f + } + C.putMarker("items", n, v, !q.renderer); + if (b && z[v]) { + C.drawLabel(z[v], g, f, v, u) + } + } + } + }, + drawLabel: function(j, h, g, p, a) { + var r = this, + m = r.attr, + d = r.getMarker("labels"), + c = d.getTemplate(), + l = r.labelCfg || (r.labelCfg = {}), + b = r.surfaceMatrix, + f, e, i = m.labelOverflowPadding, + o = m.flipXY, + k, n, s, q; + l.text = j; + n = r.getMarkerBBox("labels", p, true); + if (!n) { + r.putMarker("labels", l, p); + n = r.getMarkerBBox("labels", p, true) + } + if (o) { + l.rotationRads = Math.PI * 0.5 + } else { + l.rotationRads = 0 + } + k = n.height / 2; + f = h; + switch (c.attr.display) { + case "under": + e = g - k - i; + break; + case "rotate": + f += i; + e = g - i; + l.rotationRads = -Math.PI / 4; + break; + default: + e = g + k + i + } + l.x = b.x(f, e); + l.y = b.y(f, e); + if (c.attr.renderer) { + q = [j, d, l, { + store: r.getStore() + }, p]; + s = Ext.callback(c.attr.renderer, null, q, 0, r.getSeries()); + if (typeof s === "string") { + l.text = s + } else { + Ext.apply(l, s) + } + } + r.putMarker("labels", l, p) + } +}); +Ext.define("Ext.chart.series.Scatter", { + extend: "Ext.chart.series.Cartesian", + alias: "series.scatter", + type: "scatter", + seriesType: "scatterSeries", + requires: ["Ext.chart.series.sprite.Scatter"], + config: { + itemInstancing: { + fx: { + customDurations: { + translationX: 0, + translationY: 0 + } + } + } + }, + themeMarkerCount: function() { + return 1 + }, + applyMarker: function(b, a) { + this.getItemInstancing(); + this.setItemInstancing(b); + return this.callParent(arguments) + }, + provideLegendInfo: function(d) { + var b = this, + a = b.getMarkerStyleByIndex(0), + c = a.fillStyle; + d.push({ + name: b.getTitle() || b.getYField() || b.getId(), + mark: (Ext.isObject(c) ? c.stops && c.stops[0].color : c) || a.strokeStyle || "black", + disabled: b.getHidden(), + series: b.getId(), + index: 0 + }) + } +}); +Ext.define("Ext.chart.theme.Blue", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.blue", "chart.theme.Blue"], + config: { + baseColor: "#4d7fe6" + } +}); +Ext.define("Ext.chart.theme.BlueGradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.blue-gradients", "chart.theme.Blue:gradients"], + config: { + baseColor: "#4d7fe6", + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Category1", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category1", "chart.theme.Category1"], + config: { + colors: ["#f0a50a", "#c20024", "#2044ba", "#810065", "#7eae29"] + } +}); +Ext.define("Ext.chart.theme.Category1Gradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category1-gradients", "chart.theme.Category1:gradients"], + config: { + colors: ["#f0a50a", "#c20024", "#2044ba", "#810065", "#7eae29"], + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Category2", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category2", "chart.theme.Category2"], + config: { + colors: ["#6d9824", "#87146e", "#2a9196", "#d39006", "#1e40ac"] + } +}); +Ext.define("Ext.chart.theme.Category2Gradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category2-gradients", "chart.theme.Category2:gradients"], + config: { + colors: ["#6d9824", "#87146e", "#2a9196", "#d39006", "#1e40ac"], + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Category3", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category3", "chart.theme.Category3"], + config: { + colors: ["#fbbc29", "#ce2e4e", "#7e0062", "#158b90", "#57880e"] + } +}); +Ext.define("Ext.chart.theme.Category3Gradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category3-gradients", "chart.theme.Category3:gradients"], + config: { + colors: ["#fbbc29", "#ce2e4e", "#7e0062", "#158b90", "#57880e"], + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Category4", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category4", "chart.theme.Category4"], + config: { + colors: ["#ef5773", "#fcbd2a", "#4f770d", "#1d3eaa", "#9b001f"] + } +}); +Ext.define("Ext.chart.theme.Category4Gradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category4-gradients", "chart.theme.Category4:gradients"], + config: { + colors: ["#ef5773", "#fcbd2a", "#4f770d", "#1d3eaa", "#9b001f"], + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Category5", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category5", "chart.theme.Category5"], + config: { + colors: ["#7eae29", "#fdbe2a", "#910019", "#27b4bc", "#d74dbc"] + } +}); +Ext.define("Ext.chart.theme.Category5Gradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category5-gradients", "chart.theme.Category5:gradients"], + config: { + colors: ["#7eae29", "#fdbe2a", "#910019", "#27b4bc", "#d74dbc"], + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Category6", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category6", "chart.theme.Category6"], + config: { + colors: ["#44dce1", "#0b2592", "#996e05", "#7fb325", "#b821a1"] + } +}); +Ext.define("Ext.chart.theme.Category6Gradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.category6-gradients", "chart.theme.Category6:gradients"], + config: { + colors: ["#44dce1", "#0b2592", "#996e05", "#7fb325", "#b821a1"], + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.DefaultGradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.default-gradients", "chart.theme.Base:gradients"], + config: { + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Green", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.green", "chart.theme.Green"], + config: { + baseColor: "#b1da5a" + } +}); +Ext.define("Ext.chart.theme.GreenGradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.green-gradients", "chart.theme.Green:gradients"], + config: { + baseColor: "#b1da5a", + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Midnight", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.midnight", "chart.theme.Midnight"], + config: { + colors: ["#A837FF", "#4AC0F2", "#FF4D35", "#FF8809", "#61C102", "#FF37EA"], + chart: { + defaults: { + background: "rgb(52, 52, 53)" + } + }, + axis: { + defaults: { + style: { + strokeStyle: "rgb(224, 224, 227)" + }, + label: { + fillStyle: "rgb(224, 224, 227)" + }, + title: { + fillStyle: "rgb(224, 224, 227)" + }, + grid: { + strokeStyle: "rgb(112, 112, 115)" + } + } + }, + series: { + defaults: { + label: { + fillStyle: "rgb(224, 224, 227)" + } + } + }, + sprites: { + text: { + fillStyle: "rgb(224, 224, 227)" + } + } + } +}); +Ext.define("Ext.chart.theme.Muted", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.muted", "chart.theme.Muted"], + config: { + colors: ["#8ca640", "#974144", "#4091ba", "#8e658e", "#3b8d8b", "#b86465", "#d2af69", "#6e8852", "#3dcc7e", "#a6bed1", "#cbaa4b", "#998baa"] + } +}); +Ext.define("Ext.chart.theme.Purple", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.purple", "chart.theme.Purple"], + config: { + baseColor: "#da5abd" + } +}); +Ext.define("Ext.chart.theme.PurpleGradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.purple-gradients", "chart.theme.Purple:gradients"], + config: { + baseColor: "#da5abd", + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Red", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.red", "chart.theme.Red"], + config: { + baseColor: "#e84b67" + } +}); +Ext.define("Ext.chart.theme.RedGradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.red-gradients", "chart.theme.Red:gradients"], + config: { + baseColor: "#e84b67", + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Sky", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.sky", "chart.theme.Sky"], + config: { + baseColor: "#4ce0e7" + } +}); +Ext.define("Ext.chart.theme.SkyGradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.sky-gradients", "chart.theme.Sky:gradients"], + config: { + baseColor: "#4ce0e7", + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.chart.theme.Yellow", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.yellow", "chart.theme.Yellow"], + config: { + baseColor: "#fec935" + } +}); +Ext.define("Ext.chart.theme.YellowGradients", { + extend: "Ext.chart.theme.Base", + singleton: true, + alias: ["chart.theme.yellow-gradients", "chart.theme.Yellow:gradients"], + config: { + baseColor: "#fec935", + gradients: { + type: "linear", + degrees: 90 + } + } +}); +Ext.define("Ext.draw.Point", { + requires: ["Ext.draw.Draw", "Ext.draw.Matrix"], + isPoint: true, + x: 0, + y: 0, + length: 0, + angle: 0, + angleUnits: "degrees", + statics: { + fly: (function() { + var a = null; + return function(b, c) { + if (!a) { + a = new Ext.draw.Point() + } + a.constructor(b, c); + return a + } + })() + }, + constructor: function(a, c) { + var b = this; + if (typeof a === "number") { + b.x = a; + if (typeof c === "number") { + b.y = c + } else { + b.y = a + } + } else { + if (Ext.isArray(a)) { + b.x = a[0]; + b.y = a[1] + } else { + if (a) { + b.x = a.x; + b.y = a.y + } + } + } + b.calculatePolar() + }, + calculateCartesian: function() { + var b = this, + a = b.length, + c = b.angle; + if (b.angleUnits === "degrees") { + c = Ext.draw.Draw.rad(c) + } + b.x = Math.cos(c) * a; + b.y = Math.sin(c) * a + }, + calculatePolar: function() { + var b = this, + a = b.x, + c = b.y; + b.length = Math.sqrt(a * a + c * c); + b.angle = Math.atan2(c, a); + if (b.angleUnits === "degrees") { + b.angle = Ext.draw.Draw.degrees(b.angle) + } + }, + setX: function(a) { + this.x = a; + this.calculatePolar() + }, + setY: function(a) { + this.y = a; + this.calculatePolar() + }, + set: function(a, b) { + this.constructor(a, b) + }, + setAngle: function(a) { + this.angle = a; + this.calculateCartesian() + }, + setLength: function(a) { + this.length = a; + this.calculateCartesian() + }, + setPolar: function(b, a) { + this.angle = b; + this.length = a; + this.calculateCartesian() + }, + clone: function() { + return new Ext.draw.Point(this.x, this.y) + }, + add: function(a, c) { + var b = Ext.draw.Point.fly(a, c); + return new Ext.draw.Point(this.x + b.x, this.y + b.y) + }, + sub: function(a, c) { + var b = Ext.draw.Point.fly(a, c); + return new Ext.draw.Point(this.x - b.x, this.y - b.y) + }, + mul: function(a) { + return new Ext.draw.Point(this.x * a, this.y * a) + }, + div: function(a) { + return new Ext.draw.Point(this.x / a, this.y / a) + }, + dot: function(a, c) { + var b = Ext.draw.Point.fly(a, c); + return this.x * b.x + this.y * b.y + }, + equals: function(a, c) { + var b = Ext.draw.Point.fly(a, c); + return this.x === b.x && this.y === b.y + }, + rotate: function(f, c) { + var d, e, b, g, a; + if (this.angleUnits === "degrees") { + f = Ext.draw.Draw.rad(f); + d = Math.sin(f); + e = Math.cos(f) + } + if (c) { + b = c.x; + g = c.y + } else { + b = 0; + g = 0 + } + a = Ext.draw.Matrix.fly([e, d, -d, e, b - e * b + g * d, g - e * g + b * -d]).transformPoint(this); + return new Ext.draw.Point(a) + }, + transform: function(a) { + if (a && a.isMatrix) { + return new Ext.draw.Point(a.transformPoint(this)) + } else { + if (arguments.length === 6) { + return new Ext.draw.Point(Ext.draw.Matrix.fly(arguments).transformPoint(this)) + } else { + Ext.raise("Invalid parameters.") + } + } + }, + round: function() { + return new Ext.draw.Point(Math.round(this.x), Math.round(this.y)) + }, + ceil: function() { + return new Ext.draw.Point(Math.ceil(this.x), Math.ceil(this.y)) + }, + floor: function() { + return new Ext.draw.Point(Math.floor(this.x), Math.floor(this.y)) + }, + abs: function(a, b) { + return new Ext.draw.Point(Math.abs(this.x), Math.abs(this.y)) + }, + normalize: function(c) { + var b = this.x, + f = this.y, + a, e, d; + c = c || 1; + if (b === 0) { + a = 0; + e = c * Ext.Number.sign(f) + } else { + d = f / b; + a = c / Math.sqrt(1 + d * d); + e = a * d + } + return new Ext.draw.Point(a, e) + }, + getDistanceToLine: function(c, b) { + if (arguments.length === 4) { + c = new Ext.draw.Point(arguments[0], arguments[1]); + b = new Ext.draw.Point(arguments[2], arguments[3]) + } + var d = b.sub(c).normalize(), + a = c.sub(this); + return a.sub(d.mul(a.dot(d))) + }, + isZero: function() { + return this.x === 0 && this.y === 0 + }, + isNumber: function() { + return Ext.isNumber(this.x + this.y) + } +}); +Ext.define("Ext.draw.plugin.SpriteEvents", { + extend: "Ext.plugin.Abstract", + alias: "plugin.spriteevents", + requires: ["Ext.draw.PathUtil"], + mouseMoveEvents: { + mousemove: true, + mouseover: true, + mouseout: true + }, + spriteMouseMoveEvents: { + spritemousemove: true, + spritemouseover: true, + spritemouseout: true + }, + init: function(a) { + var b = "handleEvent"; + this.drawContainer = a; + a.addElementListener({ + click: b, + dblclick: b, + mousedown: b, + mousemove: b, + mouseup: b, + mouseover: b, + mouseout: b, + priority: 1001, + scope: this + }) + }, + hasSpriteMouseMoveListeners: function() { + var b = this.drawContainer.hasListeners, + a; + for (a in this.spriteMouseMoveEvents) { + if (a in b) { + return true + } + } + return false + }, + hitTestEvent: function(f) { + var b = this.drawContainer.getItems(), + a, d, c; + for (c = b.length - 1; c >= 0; c--) { + a = b.get(c); + d = a.hitTestEvent(f); + if (d) { + return d + } + } + return null + }, + handleEvent: function(f) { + var d = this, + b = d.drawContainer, + g = f.type in d.mouseMoveEvents, + a = d.lastSprite, + c; + if (g && !d.hasSpriteMouseMoveListeners()) { + return + } + c = d.hitTestEvent(f); + if (g && !Ext.Object.equals(c, a)) { + if (a) { + b.fireEvent("spritemouseout", a, f) + } + if (c) { + b.fireEvent("spritemouseover", c, f) + } + } + if (c) { + b.fireEvent("sprite" + f.type, c, f) + } + d.lastSprite = c + } +}); +Ext.define("Ext.chart.TipSurface", { + extend: "Ext.draw.Container", + spriteArray: false, + renderFirst: true, + constructor: function(a) { + this.callParent([a]); + if (a.sprites) { + this.spriteArray = [].concat(a.sprites); + delete a.sprites + } + }, + onRender: function() { + var c = this, + b = 0, + a = 0, + d, e; + this.callParent(arguments); + e = c.spriteArray; + if (c.renderFirst && e) { + c.renderFirst = false; + for (a = e.length; b < a; b++) { + d = c.surface.add(e[b]); + d.setAttributes({ + hidden: false + }, true) + } + } + } +}); +Ext.define("Ext.chart.interactions.ItemInfo", { + extend: "Ext.chart.interactions.Abstract", + type: "iteminfo", + alias: "interaction.iteminfo", + config: { + extjsGestures: { + start: { + event: "click", + handler: "onInfoGesture" + }, + move: { + event: "mousemove", + handler: "onInfoGesture" + }, + end: { + event: "mouseleave", + handler: "onInfoGesture" + } + } + }, + item: null, + onInfoGesture: function(f, a) { + var c = this, + b = c.getItemForEvent(f), + d = b && b.series.tooltip; + if (d) { + d.onMouseMove.call(d, f) + } + if (b !== c.item) { + if (b) { + b.series.showTip(b) + } else { + c.item.series.hideTip(c.item) + } + c.item = b + } + return false + } +}); \ No newline at end of file diff --git a/serverside/jsmod/6.0-4/charts.js.original b/serverside/jsmod/6.0-4/charts.js.original new file mode 100644 index 0000000..2b8dd71 --- /dev/null +++ b/serverside/jsmod/6.0-4/charts.js.original @@ -0,0 +1 @@ +Ext.define("Ext.draw.ContainerBase",{extend:"Ext.panel.Panel",requires:["Ext.window.Window"],previewTitleText:"Chart Preview",previewAltText:"Chart preview",layout:"container",addElementListener:function(){var b=this,a=arguments;if(b.rendered){b.el.on.apply(b.el,a)}else{b.on("render",function(){b.el.on.apply(b.el,a)})}},removeElementListener:function(){var b=this,a=arguments;if(b.rendered){b.el.un.apply(b.el,a)}},afterRender:function(){this.callParent(arguments);this.initAnimator()},getItems:function(){var b=this,a=b.items;if(!a||!a.isMixedCollection){b.initItems()}return b.items},onRender:function(){this.callParent(arguments);this.element=this.el;this.innerElement=this.body},setItems:function(a){this.items=a;return a},setSurfaceSize:function(b,a){this.resizeHandler({width:b,height:a});this.renderFrame()},onResize:function(c,a,b,e){var d=this;d.callParent([c,a,b,e]);d.setBodySize({width:c,height:a})},preview:function(){var a=this.getImage();new Ext.window.Window({title:this.previewTitleText,closeable:true,renderTo:Ext.getBody(),autoShow:true,maximizeable:true,maximized:true,border:true,layout:{type:"hbox",pack:"center",align:"middle"},items:{xtype:"container",items:{xtype:"image",mode:"img",cls:Ext.baseCSSPrefix+"chart-image",alt:this.previewAltText,src:a.data,listeners:{afterrender:function(){var e=this,b=e.imgEl.dom,d=a.type==="svg"?1:(window.devicePixelRatio||1),c;if(!b.naturalWidth||!b.naturalHeight){b.onload=function(){var g=b.naturalWidth,f=b.naturalHeight;e.setWidth(Math.floor(g/d));e.setHeight(Math.floor(f/d))}}else{c=e.getSize();e.setWidth(Math.floor(c.width/d));e.setHeight(Math.floor(c.height/d))}}}}}})},privates:{getTargetEl:function(){return this.innerElement},reattachToBody:function(){var a=this;if(a.pendingDetachSize){a.onBodyResize()}a.pendingDetachSize=false;a.callParent()}}});Ext.define("Ext.draw.SurfaceBase",{extend:"Ext.Widget",getOwnerBody:function(){return this.ownerCt.body},destroy:function(){var a=this;if(a.hasListeners.destroy){a.fireEvent("destroy",a)}a.callParent()}});Ext.define("Ext.draw.Color",{statics:{colorToHexRe:/(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/,rgbToHexRe:/\s*rgb\((\d+),\s*(\d+),\s*(\d+)\)/,rgbaToHexRe:/\s*rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\.\d]+)\)/,hexRe:/\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/,NONE:"none",RGBA_NONE:"rgba(0, 0, 0, 0)"},isColor:true,lightnessFactor:0.2,constructor:function(d,b,a,c){this.setRGB(d,b,a,c)},setRGB:function(e,c,a,d){var b=this;b.r=Math.min(255,Math.max(0,e));b.g=Math.min(255,Math.max(0,c));b.b=Math.min(255,Math.max(0,a));if(d===undefined){b.a=1}else{b.a=Math.min(1,Math.max(0,d))}},getGrayscale:function(){return this.r*0.3+this.g*0.59+this.b*0.11},getHSL:function(){var i=this,a=i.r/255,f=i.g/255,j=i.b/255,k=Math.max(a,f,j),d=Math.min(a,f,j),m=k-d,e,n=0,c=0.5*(k+d);if(d!==k){n=(c<=0.5)?m/(k+d):m/(2-k-d);if(a===k){e=60*(f-j)/m}else{if(f===k){e=120+60*(j-a)/m}else{e=240+60*(a-f)/m}}if(e<0){e+=360}if(e>=360){e-=360}}return[e,n,c]},getHSV:function(){var i=this,a=i.r/255,f=i.g/255,j=i.b/255,k=Math.max(a,f,j),d=Math.min(a,f,j),c=k-d,e,m=0,l=k;if(d!=k){m=l?c/l:0;if(a===k){e=60*(f-j)/c}else{if(f===k){e=60*(j-a)/c+120}else{e=60*(a-f)/c+240}}if(e<0){e+=360}if(e>=360){e-=360}}return[e,m,l]},setHSL:function(g,f,e){var i=this,d=Math.abs,j,b,a;g=(g%360+360)%360;f=f>1?1:f<0?0:f;e=e>1?1:e<0?0:e;if(f===0||g===null){e*=255;i.setRGB(e,e,e)}else{g/=60;j=f*(1-d(2*e-1));b=j*(1-d(g%2-1));a=e-j/2;a*=255;j*=255;b*=255;switch(Math.floor(g)){case 0:i.setRGB(j+a,b+a,a);break;case 1:i.setRGB(b+a,j+a,a);break;case 2:i.setRGB(a,j+a,b+a);break;case 3:i.setRGB(a,b+a,j+a);break;case 4:i.setRGB(b+a,a,j+a);break;case 5:i.setRGB(j+a,a,b+a);break}}return i},setHSV:function(f,e,d){var g=this,i,b,a;f=(f%360+360)%360;e=e>1?1:e<0?0:e;d=d>1?1:d<0?0:d;if(e===0||f===null){d*=255;g.setRGB(d,d,d)}else{f/=60;i=d*e;b=i*(1-Math.abs(f%2-1));a=d-i;a*=255;i*=255;b*=255;switch(Math.floor(f)){case 0:g.setRGB(i+a,b+a,a);break;case 1:g.setRGB(b+a,i+a,a);break;case 2:g.setRGB(a,i+a,b+a);break;case 3:g.setRGB(a,b+a,i+a);break;case 4:g.setRGB(b+a,a,i+a);break;case 5:g.setRGB(i+a,a,b+a);break}}return g},createLighter:function(b){if(!b&&b!==0){b=this.lightnessFactor}var a=this.getHSL();a[2]=Ext.Number.constrain(a[2]+b,0,1);return Ext.draw.Color.fromHSL(a[0],a[1],a[2])},createDarker:function(a){if(!a&&a!==0){a=this.lightnessFactor}return this.createLighter(-a)},toString:function(){var f=this,c=Math.round;if(f.a===1){var e=c(f.r).toString(16),d=c(f.g).toString(16),a=c(f.b).toString(16);e=(e.length===1)?"0"+e:e;d=(d.length===1)?"0"+d:d;a=(a.length===1)?"0"+a:a;return["#",e,d,a].join("")}else{return"rgba("+[c(f.r),c(f.g),c(f.b),f.a===0?0:f.a.toFixed(15)].join(", ")+")"}},toHex:function(b){if(Ext.isArray(b)){b=b[0]}if(!Ext.isString(b)){return""}if(b.substr(0,1)==="#"){return b}var e=Ext.draw.Color.colorToHexRe.exec(b);if(Ext.isArray(e)){var f=parseInt(e[2],10),d=parseInt(e[3],10),a=parseInt(e[4],10),c=a|(d<<8)|(f<<16);return e[1]+"#"+("000000"+c.toString(16)).slice(-6)}else{return""}},setFromString:function(j){var e,h,f,c,d=1,i=parseInt;if(j===Ext.draw.Color.NONE){this.r=this.g=this.b=this.a=0;return this}if((j.length===4||j.length===7)&&j.substr(0,1)==="#"){e=j.match(Ext.draw.Color.hexRe);if(e){h=i(e[1],16)>>0;f=i(e[2],16)>>0;c=i(e[3],16)>>0;if(j.length===4){h+=(h*16);f+=(f*16);c+=(c*16)}}}else{if((e=j.match(Ext.draw.Color.rgbToHexRe))){h=+e[1];f=+e[2];c=+e[3]}else{if((e=j.match(Ext.draw.Color.rgbaToHexRe))){h=+e[1];f=+e[2];c=+e[3];d=+e[4]}else{if(Ext.draw.Color.ColorList.hasOwnProperty(j.toLowerCase())){return this.setFromString(Ext.draw.Color.ColorList[j.toLowerCase()])}}}}if(typeof h==="undefined"){return this}this.r=h;this.g=f;this.b=c;this.a=d;return this}},function(){var a=new this();this.addStatics({fly:function(f,e,c,d){switch(arguments.length){case 1:a.setFromString(f);break;case 3:case 4:a.setRGB(f,e,c,d);break;default:return null}return a},ColorList:{aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgrey:"#d3d3d3",lightgreen:"#90ee90",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"},fromHSL:function(d,c,b){return(new this(0,0,0,0)).setHSL(d,c,b)},fromHSV:function(d,c,b){return(new this(0,0,0,0)).setHSL(d,c,b)},fromString:function(b){return(new this(0,0,0,0)).setFromString(b)},create:function(b){if(b instanceof this){return b}else{if(Ext.isArray(b)){return new Ext.draw.Color(b[0],b[1],b[2],b[3])}else{if(Ext.isString(b)){return Ext.draw.Color.fromString(b)}else{if(arguments.length>2){return new Ext.draw.Color(arguments[0],arguments[1],arguments[2],arguments[3])}else{return new Ext.draw.Color(0,0,0,0)}}}}}})});Ext.define("Ext.draw.sprite.AnimationParser",function(){function a(d,c,b){return d+(c-d)*b}return{singleton:true,attributeRe:/^url\(#([a-zA-Z\-]+)\)$/,requires:["Ext.draw.Color"],color:{parseInitial:function(c,b){if(Ext.isString(c)){c=Ext.draw.Color.create(c)}if(Ext.isString(b)){b=Ext.draw.Color.create(b)}if((c instanceof Ext.draw.Color)&&(b instanceof Ext.draw.Color)){return[[c.r,c.g,c.b,c.a],[b.r,b.g,b.b,b.a]]}else{return[c||b,b||c]}},compute:function(d,c,b){if(!Ext.isArray(d)||!Ext.isArray(c)){return c||d}else{return[a(d[0],c[0],b),a(d[1],c[1],b),a(d[2],c[2],b),a(d[3],c[3],b)]}},serve:function(c){var b=Ext.draw.Color.fly(c[0],c[1],c[2],c[3]);return b.toString()}},number:{parse:function(b){return b===null?null:+b},compute:function(d,c,b){if(!Ext.isNumber(d)||!Ext.isNumber(c)){return c||d}else{return a(d,c,b)}}},angle:{parseInitial:function(c,b){if(b-c>Math.PI){b-=Math.PI*2}else{if(b-c<-Math.PI){b+=Math.PI*2}}return[c,b]},compute:function(d,c,b){if(!Ext.isNumber(d)||!Ext.isNumber(c)){return c||d}else{return a(d,c,b)}}},path:{parseInitial:function(m,n){var c=m.toStripes(),o=n.toStripes(),e,d,k=c.length,p=o.length,h,f,b,g=o[p-1],l=[g[g.length-2],g[g.length-1]];for(e=k;e=1){return l.path}var e=0,f=c.length,d=0,b,k,h,n=l.temp.params,g=0;for(;eMath.min(b.x+b.width,a.x+a.width))||(Math.max(b.y,a.y)-c>Math.min(b.y+b.height,a.y+a.height))},isPointInBBox:function(a,c,b){return !!b&&a>=b.x&&a<=(b.x+b.width)&&c>=b.y&&c<=(b.y+b.height)},spline:function(m){var e,c,k=m.length,b,h,l,f,a=0,g=new Float32Array(m.length),n=new Float32Array(m.length*3-2);g[0]=0;g[k-1]=0;for(e=1;e0;e--){a=3.732050807568877+48.248711305964385/(-13.928203230275537+Math.pow(0.07179676972449123,e));g[e]-=g[e+1]*a}f=m[0];b=f-g[0];for(e=0,c=0;e=d&&h>=s)||(h<=d&&h<=s)){g=j=p}else{g=f((i-e)/k(h-d));if(dp){c-=n}g+=c;j+=c;m=i-r*a(g);l=h+r*b(g);v=i+q*a(j);u=h+q*b(j);if((h>d&&ld)){m+=k(d-l)*(m-i)/(l-h);l=d}if((h>s&&us)){v-=k(s-u)*(v-i)/(u-h);u=s}return{x1:m,y1:l,x2:v,y2:u}},smooth:function(l,j,o){var k=l.length,h,g,c,b,q,p,n,m,f=[],e=[],d,a;for(d=0;d=Math.PI){a-=Math.PI*2}}return a}},data:function(a){if(Ext.isArray(a)){return a.slice()}else{if(a instanceof Float32Array){return new Float32Array(a)}}},bool:function(a){return !!a},color:function(a){if(a instanceof Ext.draw.Color){return a.toString()}else{if(a instanceof Ext.draw.gradient.Gradient){return a}else{if(!a){return Ext.draw.Color.NONE}else{if(Ext.isString(a)){if(a.substr(0,3)==="url"){a=Ext.draw.gradient.GradientDefinition.get(a);if(Ext.isString(a)){return a}}else{return Ext.draw.Color.fly(a).toString()}}}}}if(a.type==="linear"){return Ext.create("Ext.draw.gradient.Linear",a)}else{if(a.type==="radial"){return Ext.create("Ext.draw.gradient.Radial",a)}else{if(a.type==="pattern"){return Ext.create("Ext.draw.gradient.Pattern",a)}else{return Ext.draw.Color.NONE}}}},limited:function(a,b){return function(c){c=+c;return Ext.isNumber(c)?Math.min(Math.max(c,a),b):undefined}},limited01:function(a){a=+a;return Ext.isNumber(a)?Math.min(Math.max(a,0),1):undefined},enums:function(){var d={},a=Array.prototype.slice.call(arguments,0),b,c;for(b=0,c=a.length;b=(7-4*o)/11){return i*i-g((11-6*o-11*q)/4,2)}}},elastic:function(o,i){return g(2,10*--o)*m(20*o*e*(i||1)/3)}},k={},a,f,d;function h(i){return function(o){return g(o,i)}}function n(i,o){k[i+"In"]=function(p){return o(p)};k[i+"Out"]=function(p){return 1-o(1-p)};k[i+"InOut"]=function(p){return(p<=0.5)?o(2*p)/2:(2-o(2*(1-p)))/2}}for(d=0,f=b.length;d-1},empty:function(){return this.animations.length===0},step:function(d){var c=this,f=c.animations,e,a=0,b=f.length;for(;a0},applyEasing:function(a){if(typeof a==="string"){a=Ext.draw.TimingFunctions.easingMap[a]}return a},applyCustomEasings:function(a,e){e=e||{};var g,d,b,h,c,f;for(d in a){g=true;h=a[d];b=d.split(",");if(typeof h==="string"){h=Ext.draw.TimingFunctions.easingMap[h]}for(c=0,f=b.length;c=1){h[a]=f[a];delete f[a];if(d[a].remove){h.removeFromInstance=h.removeFromInstance||{};h.removeFromInstance[a]=true}delete d[a]}else{h[a]=b.serve(b.compute(b.source,b.target,b.easing(i),g[a]));e=true}}g.lastUpdate=c;this.setAnimating(g,e);return h},pushDown:function(a,b){b=this.callParent([a.animationOriginal,b]);return this.setAttrs(a,b)},popUp:function(a,b){a=a.prototype;b=this.setAttrs(a,b);if(this._next){return this._next.popUp(a,b)}else{return Ext.apply(a,b)}},step:function(g){var f=this,c=f.animatingPool.slice(),e=c.length,b=0,a,d;for(;b=e.x&&a<=(e.x+e.width)&&f>=e.y&&f<=(e.y+e.height);if(d){return{sprite:this}}}return null},isVisible:function(){var e=this.attr,f=this.getParent(),g=f&&(f.isSurface||f.isVisible()),d=g&&!e.hidden&&e.globalAlpha,b=Ext.draw.Color.NONE,a=Ext.draw.Color.RGBA_NONE,c=e.fillOpacity&&e.fillStyle!==b&&e.fillStyle!==a,i=e.strokeOpacity&&e.strokeStyle!==b&&e.strokeStyle!==a,h=d&&(c||i);return !!h},repaint:function(){var a=this.getSurface();if(a){a.renderFrame()}},remove:function(){var a=this.getSurface();if(a&&a.isSurface){return a.remove(this)}return null},destroy:function(){var b=this,a=b.topModifier,c;while(a){c=a;a=a.getPrevious();c.destroy()}delete b.attr;b.remove();if(b.fireEvent("beforedestroy",b)!==false){b.fireEvent("destroy",b)}b.callParent()}},function(){this.def=new Ext.draw.sprite.AttributeDefinition(this.def);this.def.spriteClass=this});Ext.define("Ext.draw.Path",{requires:["Ext.draw.Draw"],statics:{pathRe:/,?([achlmqrstvxz]),?/gi,pathRe2:/-/gi,pathSplitRe:/\s|,/g},svgString:"",constructor:function(a){var b=this;b.commands=[];b.params=[];b.cursor=null;b.startX=0;b.startY=0;if(a){b.fromSvgString(a)}},clear:function(){var a=this;a.params.length=0;a.commands.length=0;a.cursor=null;a.startX=0;a.startY=0;a.dirt()},dirt:function(){this.svgString=""},moveTo:function(a,c){var b=this;if(!b.cursor){b.cursor=[a,c]}b.params.push(a,c);b.commands.push("M");b.startX=a;b.startY=c;b.cursor[0]=a;b.cursor[1]=c;b.dirt()},lineTo:function(a,c){var b=this;if(!b.cursor){b.cursor=[a,c];b.params.push(a,c);b.commands.push("M")}else{b.params.push(a,c);b.commands.push("L")}b.cursor[0]=a;b.cursor[1]=c;b.dirt()},bezierCurveTo:function(c,e,b,d,a,g){var f=this;if(!f.cursor){f.moveTo(c,e)}f.params.push(c,e,b,d,a,g);f.commands.push("C");f.cursor[0]=a;f.cursor[1]=g;f.dirt()},quadraticCurveTo:function(b,e,a,d){var c=this;if(!c.cursor){c.moveTo(b,e)}c.bezierCurveTo((2*b+c.cursor[0])/3,(2*e+c.cursor[1])/3,(2*b+a)/3,(2*e+d)/3,a,d)},closePath:function(){var a=this;if(a.cursor){a.cursor=null;a.commands.push("Z");a.dirt()}},arcTo:function(A,f,z,d,j,i,v){var E=this;if(i===undefined){i=j}if(v===undefined){v=0}if(!E.cursor){E.moveTo(A,f);return}if(j===0||i===0){E.lineTo(A,f);return}z-=A;d-=f;var B=E.cursor[0]-A,g=E.cursor[1]-f,C=z*g-d*B,b,a,l,r,k,q,x=Math.sqrt(B*B+g*g),u=Math.sqrt(z*z+d*d),t,e,c;if(C===0){E.lineTo(A,f);return}if(i!==j){b=Math.cos(v);a=Math.sin(v);l=b/j;r=a/i;k=-a/j;q=b/i;var D=l*B+r*g;g=k*B+q*g;B=D;D=l*z+r*d;d=k*z+q*d;z=D}else{B/=j;g/=i;z/=j;d/=i}e=B*u+z*x;c=g*u+d*x;t=1/(Math.sin(Math.asin(Math.abs(C)/(x*u))*0.5)*Math.sqrt(e*e+c*c));e*=t;c*=t;var o=(e*B+c*g)/(B*B+g*g),m=(e*z+c*d)/(z*z+d*d);var n=B*o-e,p=g*o-c,h=z*m-e,y=d*m-c,w=Math.atan2(p,n),s=Math.atan2(y,h);if(C>0){if(s=Math.PI*2){o.ellipse(h,f,c,a,q,n,n+Math.PI,e);o.ellipse(h,f,c,a,q,n+Math.PI,d,e);return}if(!e){if(d=m){s.push(j+u*b+i,h+t*b+f,j*b+u+i,h*b+t+f,u+i,t+f);r+=6;v-=m;C=j;j=u;u=-C;C=h;h=t;t=-C}if(v){g=(0.3294738052815987+0.012120855841304373*v)*v;A=Math.cos(v);a=Math.sin(v);B=A+g*a;c=a-g*A;s.push(j+u*g+i,h+t*g+f,j*B+u*c+i,h*B+t*c+f,j*A+u*a+i,h*A+t*a+f);r+=6}return r},arcSvg:function(j,h,r,m,w,t,c){if(j<0){j=-j}if(h<0){h=-h}var x=this,u=x.cursor[0],f=x.cursor[1],a=(u-t)/2,y=(f-c)/2,d=Math.cos(r),s=Math.sin(r),o=a*d+y*s,v=-a*s+y*d,i=o/j,g=v/h,p=i*i+g*g,e=(u+t)*0.5,b=(f+c)*0.5,l=0,k=0;if(p>=1){p=Math.sqrt(p);j*=p;h*=p}else{p=Math.sqrt(1/p-1);if(m===w){p=-p}l=p*j*g;k=-p*h*i;e+=d*l-s*k;b+=s*l+d*k}var q=Math.atan2((v-k)/h,(o-l)/j),n=Math.atan2((-v-k)/h,(-o-l)/j)-q;if(w){if(n<=0){n+=Math.PI*2}}else{if(n>=0){n-=Math.PI*2}}x.ellipse(e,b,j,h,r,q,q+n,1-w)},fromSvgString:function(e){if(!e){return}var m=this,h,l={a:7,c:6,h:1,l:2,m:2,q:4,s:4,t:2,v:1,z:0,A:7,C:6,H:1,L:2,M:2,Q:4,S:4,T:2,V:1,Z:0},k="",g,f,c=0,b=0,d=false,j,n,a;if(Ext.isString(e)){h=e.replace(Ext.draw.Path.pathRe," $1 ").replace(Ext.draw.Path.pathRe2," -").split(Ext.draw.Path.pathSplitRe)}else{if(Ext.isArray(e)){h=e.join(",").split(Ext.draw.Path.pathSplitRe)}}for(j=0,n=0;j=0){q=Math.sqrt(q);o=(q-g)/2/i;if(00){o-=q/i;if(0k.x){b=k.x}if(hk.y){f=k.y}if(a=f.length||!e.isVisible()){return a}d.attr=f[c];a=d.isVisible(point,options);d.attr=b;return a},render:function(b,l,d,h){var g=this,j=g.getTemplate(),k=g.attr.matrix,c=j.attr,a=g.instances,e,f=g.position;k.toContext(l);j.preRender(b,l,d,h);j.useAttributes(l,h);for(e=0;ee){f=m.substr(e,i-e)}else{continue}}g=f.indexOf("/");if(g>0){f=f.substr(0,g)}else{if(g===0){continue}}if(f!=="normal"&&f!=="inherit"){h=n[f];if(h){l[h]=f}else{if(f.match(Ext.dom.Element.unitRe)){l.fontSize=f}else{l.fontFamily=m.substr(e);break}}}e=i+1}if(!l.fontStyle){l.fontStyle=""}if(!l.fontVariant){l.fontVariant=""}if(!l.fontWeight){l.fontWeight=""}this.setAttributes(l,true)},fontProperties:{fontStyle:true,fontVariant:true,fontWeight:true,fontSize:true,fontFamily:true},setAttributes:function(g,i,e){var f,h;if(g&&g.font){h={};for(f in g){if(!(f in this.fontProperties)){h[f]=g[f]}}g=h}this.callParent([g,i,e])},getBBox:function(g){var h=this,f=h.attr.bbox.plain,e=h.getSurface();if(f.dirty){h.updatePlainBBox(f);f.dirty=false}if(e.getInherited().rtl&&e.getFlipRtlText()){h.updatePlainBBox(f,true)}return h.callParent([g])},rtlAlignments:{start:"end",center:"center",end:"start"},updatePlainBBox:function(k,B){var C=this,w=C.attr,o=w.x,n=w.y,q=[],t=w.font,r=w.text,s=w.textBaseline,l=w.textAlign,u=(B&&C.oldSize)?C.oldSize:(C.oldSize=Ext.draw.TextMeasurer.measureText(r,t)),z=C.getSurface(),p=z.getInherited().rtl,v=p&&z.getFlipRtlText(),h=z.getRect(),f=u.sizes,g=u.height,j=u.width,m=f?f.length:0,e,A=0;switch(s){case"hanging":case"top":break;case"ideographic":case"bottom":n-=g;break;case"alphabetic":n-=g*0.8;break;case"middle":n-=g*0.5;break}if(v){o=h[2]-h[0]-o;l=C.rtlAlignments[l]}switch(l){case"start":if(p){for(;A0&&m>0){a=(Math.sqrt(f*f+m*m)*Math.abs(Math.cos(c-Math.atan(f/m))))/2;k=q.createLinearGradient(d+p*a,b+j*a,d-p*a,b-j*a);for(e=0;e=0;b--){a[b].destroy()}}else{for(;b>=0;b--){c=a[b];c.setParent(null);c.setSurface(null)}}a.length=0;this.map={};this.dirtyZIndex=true},applyItems:function(a){if(this.getItems()){this.removeAll(true)}return Ext.Array.from(this.add(a))},createItem:function(a){return Ext.create(a.xclass||"sprite."+a.type,a)},getBBox:function(f,b){var f=Ext.Array.from(f),c=Infinity,h=-Infinity,g=Infinity,a=-Infinity,j,k,d,e;for(d=0,e=f.length;dk.x){c=k.x}if(hk.y){g=k.y}if(a0){g.isPendingRenderFrame=true;return}var f=g.getRect(),c=g.getBackground(),a=g.getItems(),e,b,d;if(!f){return}g.orderByZIndex();if(g.getDirty()){g.clear();g.clearTransform();if(c){g.renderSprite(c)}for(b=0,d=a.length;b=0;e--){d=g[e];if(d.hitTest){a=d.hitTest(b,c);if(a){return a}}}return null},hitTestEvent:function(b,a){var c=this.getEventXY(b);return this.hitTest(c,a)}});Ext.define("Ext.draw.engine.SvgContext",{requires:["Ext.draw.Color"],toSave:["strokeOpacity","strokeStyle","fillOpacity","fillStyle","globalAlpha","lineWidth","lineCap","lineJoin","lineDash","lineDashOffset","miterLimit","shadowOffsetX","shadowOffsetY","shadowBlur","shadowColor","globalCompositeOperation","position","fillGradient","strokeGradient"],strokeOpacity:1,strokeStyle:"none",fillOpacity:1,fillStyle:"none",lineDash:[],lineDashOffset:0,globalAlpha:1,lineWidth:1,lineCap:"butt",lineJoin:"miter",miterLimit:10,shadowOffsetX:0,shadowOffsetY:0,shadowBlur:0,shadowColor:"none",globalCompositeOperation:"src",urlStringRe:/^url\(#([\w\-]+)\)$/,constructor:function(a){this.surface=a;this.state=[];this.matrix=new Ext.draw.Matrix();this.path=null;this.clear()},clear:function(){this.group=this.surface.mainGroup;this.position=0;this.path=null},getElement:function(a){return this.surface.getSvgElement(this.group,a,this.position++)},removeElement:function(d){var d=Ext.fly(d),h,g,b,f,a,e,c;if(!d){return}if(d.dom.tagName==="g"){a=d.dom.gradients;for(c in a){a[c].destroy()}}else{h=d.getAttribute("fill");g=d.getAttribute("stroke");b=h&&h.match(this.urlStringRe);f=g&&g.match(this.urlStringRe);if(b&&b[1]){e=Ext.fly(b[1]);if(e){e.destroy()}}if(f&&f[1]){e=Ext.fly(f[1]);if(e){e.destroy()}}}d.destroy()},save:function(){var c=this.toSave,e={},d=this.getElement("g"),b,a;for(a=0;athis.position){this.removeElement(c[c.length-1])}for(a=0;athis.position){Ext.fly(a[a.length-1]).destroy()}return"url(#"+this.element.getId()+")"},destroy:function(){var b=this.statics().map,a=this.element;if(a&&a.dom){delete b[a.dom.id];a.destroy()}this.callParent()}});Ext.define("Ext.draw.engine.Svg",{extend:"Ext.draw.Surface",requires:["Ext.draw.engine.SvgContext"],statics:{BBoxTextCache:{}},config:{highPrecision:false},getElementConfig:function(){return{reference:"element",style:{position:"absolute"},children:[{reference:"innerElement",style:{width:"100%",height:"100%",position:"relative"},children:[{tag:"svg",reference:"svgElement",namespace:"http://www.w3.org/2000/svg",width:"100%",height:"100%",version:1.1}]}]}},constructor:function(a){var b=this;b.callParent([a]);b.mainGroup=b.createSvgNode("g");b.defElement=b.createSvgNode("defs");b.svgElement.appendChild(b.mainGroup);b.svgElement.appendChild(b.defElement);b.ctx=new Ext.draw.engine.SvgContext(b)},createSvgNode:function(a){var b=document.createElementNS("http://www.w3.org/2000/svg",a);return Ext.get(b)},getSvgElement:function(d,b,a){var c;if(d.dom.childNodes.length>a){c=d.dom.childNodes[a];if(c.tagName===b){return Ext.get(c)}else{Ext.destroy(c)}}c=Ext.get(this.createSvgNode(b));if(a===0){d.insertFirst(c)}else{c.insertAfter(Ext.fly(d.dom.childNodes[a-1]))}c.cache={};return c},setElementAttributes:function(d,b){var f=d.dom,a=d.cache,c,e;for(c in b){e=b[c];if(a[c]!==e){a[c]=e;f.setAttribute(c,e)}}},getNextDef:function(a){return this.getSvgElement(this.defElement,a,this.defPosition++)},clearTransform:function(){var a=this;a.mainGroup.set({transform:a.matrix.toSvg()})},clear:function(){this.ctx.clear();this.defPosition=0},renderSprite:function(b){var d=this,c=d.getRect(),a=d.ctx;if(b.attr.hidden||b.attr.globalAlpha===0){a.save();a.restore();return}b.element=a.save();b.preRender(this);b.useAttributes(a,c);if(false===b.render(this,a,[0,0,c[2],c[3]])){return false}b.setDirty(false);a.restore()},flatten:function(e,b){var c='',f=Ext.getClassName(this),a,g,d;c+='';for(d=0;d';c+=this.serializeNode(a.svgElement.dom);c+=""}c+="";return{data:"data:image/svg+xml;utf8,"+encodeURIComponent(c),type:"svg"}},serializeNode:function(d){var b="",c,f,a,e;if(d.nodeType===document.TEXT_NODE){return d.nodeValue}b+="<"+d.nodeName;if(d.attributes.length){for(c=0,f=d.attributes.length;c";return b},destroy:function(){var a=this;a.ctx.destroy();a.mainGroup.destroy();delete a.mainGroup;delete a.ctx;a.callParent()},remove:function(a,b){if(a&&a.element){if(this.ctx){this.ctx.removeElement(a.element)}else{a.element.destroy()}a.element=null}this.callParent(arguments)}});Ext.draw||(Ext.draw={});Ext.draw.engine||(Ext.draw.engine={});Ext.draw.engine.excanvas=true;if(!document.createElement("canvas").getContext){(function(){var ab=Math;var n=ab.round;var l=ab.sin;var A=ab.cos;var H=ab.abs;var N=ab.sqrt;var d=10;var f=d/2;var z=+navigator.userAgent.match(/MSIE ([\d.]+)?/)[1];function y(){return this.context_||(this.context_=new D(this))}var t=Array.prototype.slice;function g(j,m,p){var i=t.call(arguments,2);return function(){return j.apply(m,i.concat(t.call(arguments)))}}function af(i){return String(i).replace(/&/g,"&").replace(/"/g,""")}function Y(m,j,i){Ext.onReady(function(){if(!m.namespaces[j]){m.namespaces.add(j,i,"#default#VML")}})}function R(j){Y(j,"g_vml_","urn:schemas-microsoft-com:vml");Y(j,"g_o_","urn:schemas-microsoft-com:office:office");if(!j.styleSheets.ex_canvas_){var i=j.createStyleSheet();i.owningElement.id="ex_canvas_";i.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}R(document);var e={init:function(i){var j=i||document;j.createElement("canvas");j.attachEvent("onreadystatechange",g(this.init_,this,j))},init_:function(p){var m=p.getElementsByTagName("canvas");for(var j=0;j1){m--}if(6*m<1){return j+(i-j)*6*m}else{if(2*m<1){return i}else{if(3*m<2){return j+(i-j)*(2/3-m)*6}else{return j}}}}var C={};function F(j){if(j in C){return C[j]}var ag,Z=1;j=String(j);if(j.charAt(0)=="#"){ag=j}else{if(/^rgb/.test(j)){var p=M(j);var ag="#",ah;for(var m=0;m<3;m++){if(p[m].indexOf("%")!=-1){ah=Math.floor(c(p[m])*255)}else{ah=+p[m]}ag+=k[r(ah,0,255)]}Z=+p[3]}else{if(/^hsl/.test(j)){var p=M(j);ag=I(p);Z=p[3]}else{ag=b[j]||j}}}return C[j]={color:ag,alpha:Z}}var o={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var L={};function E(i){if(L[i]){return L[i]}var p=document.createElement("div");var m=p.style;try{m.font=i}catch(j){}return L[i]={style:m.fontStyle||o.style,variant:m.fontVariant||o.variant,weight:m.fontWeight||o.weight,size:m.fontSize||o.size,family:m.fontFamily||o.family}}function u(m,j){var i={};for(var ah in m){i[ah]=m[ah]}var ag=parseFloat(j.currentStyle.fontSize),Z=parseFloat(m.size);if(typeof m.size=="number"){i.size=m.size}else{if(m.size.indexOf("px")!=-1){i.size=Z}else{if(m.size.indexOf("em")!=-1){i.size=ag*Z}else{if(m.size.indexOf("%")!=-1){i.size=(ag/100)*Z}else{if(m.size.indexOf("pt")!=-1){i.size=Z/0.75}else{i.size=ag}}}}}i.size*=0.981;return i}function ac(i){return i.style+" "+i.variant+" "+i.weight+" "+i.size+"px "+i.family}var s={butt:"flat",round:"round"};function S(i){return s[i]||"square"}function D(i){this.m_=B();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineDash=[];this.lineCap="butt";this.miterLimit=d*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var m="width:"+i.clientWidth+"px;height:"+i.clientHeight+"px;overflow:hidden;position:absolute";var j=i.ownerDocument.createElement("div");j.style.cssText=m;i.appendChild(j);var p=j.cloneNode(false);p.style.backgroundColor="red";p.style.filter="alpha(opacity=0)";i.appendChild(p);this.element_=j;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var q=D.prototype;q.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};q.beginPath=function(){this.currentPath_=[]};q.moveTo=function(j,i){var m=V(this,j,i);this.currentPath_.push({type:"moveTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};q.lineTo=function(j,i){var m=V(this,j,i);this.currentPath_.push({type:"lineTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};q.bezierCurveTo=function(m,j,ak,aj,ai,ag){var i=V(this,ai,ag);var ah=V(this,m,j);var Z=V(this,ak,aj);K(this,ah,Z,i)};function K(i,Z,m,j){i.currentPath_.push({type:"bezierCurveTo",cp1x:Z.x,cp1y:Z.y,cp2x:m.x,cp2y:m.y,x:j.x,y:j.y});i.currentX_=j.x;i.currentY_=j.y}q.quadraticCurveTo=function(ai,m,j,i){var ah=V(this,ai,m);var ag=V(this,j,i);var aj={x:this.currentX_+2/3*(ah.x-this.currentX_),y:this.currentY_+2/3*(ah.y-this.currentY_)};var Z={x:aj.x+(ag.x-this.currentX_)/3,y:aj.y+(ag.y-this.currentY_)/3};K(this,aj,Z,ag)};q.arc=function(al,aj,ak,ag,j,m){ak*=d;var ap=m?"at":"wa";var am=al+A(ag)*ak-f;var ao=aj+l(ag)*ak-f;var i=al+A(j)*ak-f;var an=aj+l(j)*ak-f;if(am==i&&!m){am+=0.125}var Z=V(this,al,aj);var ai=V(this,am,ao);var ah=V(this,i,an);this.currentPath_.push({type:ap,x:Z.x,y:Z.y,radius:ak,xStart:ai.x,yStart:ai.y,xEnd:ah.x,yEnd:ah.y})};q.rect=function(m,j,i,p){this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath()};q.strokeRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.stroke();this.currentPath_=Z};q.fillRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.fill();this.currentPath_=Z};q.createLinearGradient=function(j,p,i,m){var Z=new U("gradient");Z.x0_=j;Z.y0_=p;Z.x1_=i;Z.y1_=m;return Z};q.createRadialGradient=function(p,ag,m,j,Z,i){var ah=new U("gradientradial");ah.x0_=p;ah.y0_=ag;ah.r0_=m;ah.x1_=j;ah.y1_=Z;ah.r1_=i;return ah};q.drawImage=function(an,j){var ah,Z,aj,ar,al,ak,ao,av;var ai=an.runtimeStyle.width;var am=an.runtimeStyle.height;an.runtimeStyle.width="auto";an.runtimeStyle.height="auto";var ag=an.width;var aq=an.height;an.runtimeStyle.width=ai;an.runtimeStyle.height=am;if(arguments.length==3){ah=arguments[1];Z=arguments[2];al=ak=0;ao=aj=ag;av=ar=aq}else{if(arguments.length==5){ah=arguments[1];Z=arguments[2];aj=arguments[3];ar=arguments[4];al=ak=0;ao=ag;av=aq}else{if(arguments.length==9){al=arguments[1];ak=arguments[2];ao=arguments[3];av=arguments[4];ah=arguments[5];Z=arguments[6];aj=arguments[7];ar=arguments[8]}else{throw Error("Invalid number of arguments")}}}var au=V(this,ah,Z);var at=[];var i=10;var p=10;var ap=this.m_;at.push(" ','","");this.element_.insertAdjacentHTML("BeforeEnd",at.join(""))};q.setLineDash=function(i){if(i.length===1){i=i.slice();i[1]=i[0]}this.lineDash=i};q.getLineDash=function(){return this.lineDash};q.stroke=function(ak){var ai=[];var m=10;var al=10;ai.push("aj.x){aj.x=j.x}if(Z.y==null||j.yaj.y){aj.y=j.y}}}ai.push(' ">');if(!ak){w(this,ai)}else{G(this,ai,Z,aj)}ai.push("");this.element_.insertAdjacentHTML("beforeEnd",ai.join(""))};function w(m,ag){var j=F(m.strokeStyle);var p=j.color;var Z=j.alpha*m.globalAlpha;var i=m.lineScale_*m.lineWidth;if(i<1){Z*=i}ag.push("')}function G(aq,ai,aK,ar){var aj=aq.fillStyle;var aB=aq.arcScaleX_;var aA=aq.arcScaleY_;var j=ar.x-aK.x;var p=ar.y-aK.y;if(aj instanceof U){var an=0;var aF={x:0,y:0};var ax=0;var am=1;if(aj.type_=="gradient"){var al=aj.x0_/aB;var m=aj.y0_/aA;var ak=aj.x1_/aB;var aM=aj.y1_/aA;var aJ=V(aq,al,m);var aI=V(aq,ak,aM);var ag=aI.x-aJ.x;var Z=aI.y-aJ.y;an=Math.atan2(ag,Z)*180/Math.PI;if(an<0){an+=360}if(an<0.000001){an=0}}else{var aJ=V(aq,aj.x0_,aj.y0_);aF={x:(aJ.x-aK.x)/j,y:(aJ.y-aK.y)/p};j/=aB*d;p/=aA*d;var aD=ab.max(j,p);ax=2*aj.r0_/aD;am=2*aj.r1_/aD-ax}var av=aj.colors_;av.sort(function(aN,i){return aN.offset-i.offset});var ap=av.length;var au=av[0].color;var at=av[ap-1].color;var az=av[0].alpha*aq.globalAlpha;var ay=av[ap-1].alpha*aq.globalAlpha;var aE=[];for(var aH=0;aH')}else{if(aj instanceof T){if(j&&p){var ah=-aK.x;var aC=-aK.y;ai.push("')}}else{var aL=F(aq.fillStyle);var aw=aL.color;var aG=aL.alpha*aq.globalAlpha;ai.push('')}}}q.fill=function(){this.$stroke(true)};q.closePath=function(){this.currentPath_.push({type:"close"})};function V(j,Z,p){var i=j.m_;return{x:d*(Z*i[0][0]+p*i[1][0]+i[2][0])-f,y:d*(Z*i[0][1]+p*i[1][1]+i[2][1])-f}}q.save=function(){var i={};v(this,i);this.aStack_.push(i);this.mStack_.push(this.m_);this.m_=J(B(),this.m_)};q.restore=function(){if(this.aStack_.length){v(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function h(i){return isFinite(i[0][0])&&isFinite(i[0][1])&&isFinite(i[1][0])&&isFinite(i[1][1])&&isFinite(i[2][0])&&isFinite(i[2][1])}function aa(j,i,p){if(!h(i)){return}j.m_=i;if(p){var Z=i[0][0]*i[1][1]-i[0][1]*i[1][0];j.lineScale_=N(H(Z))}}q.translate=function(m,j){var i=[[1,0,0],[0,1,0],[m,j,1]];aa(this,J(i,this.m_),false)};q.rotate=function(j){var p=A(j);var m=l(j);var i=[[p,m,0],[-m,p,0],[0,0,1]];aa(this,J(i,this.m_),false)};q.scale=function(m,j){this.arcScaleX_*=m;this.arcScaleY_*=j;var i=[[m,0,0],[0,j,0],[0,0,1]];aa(this,J(i,this.m_),true)};q.transform=function(Z,p,ah,ag,j,i){var m=[[Z,p,0],[ah,ag,0],[j,i,1]];aa(this,J(m,this.m_),true)};q.setTransform=function(ag,Z,ai,ah,p,j){var i=[[ag,Z,0],[ai,ah,0],[p,j,1]];aa(this,i,true)};q.drawText_=function(am,ak,aj,ap,ai){var ao=this.m_,at=1000,j=0,ar=at,ah={x:0,y:0},ag=[];var i=u(E(this.font),this.element_);var p=ac(i);var au=this.element_.currentStyle;var Z=this.textAlign.toLowerCase();switch(Z){case"left":case"center":case"right":break;case"end":Z=au.direction=="ltr"?"right":"left";break;case"start":Z=au.direction=="rtl"?"right":"left";break;default:Z="left"}switch(this.textBaseline){case"hanging":case"top":ah.y=i.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":ah.y=-i.size/3;break}switch(Z){case"right":j=at;ar=0.05;break;case"center":j=ar=at/2;break}var aq=V(this,ak+ah.x,aj+ah.y);ag.push('');if(ai){w(this,ag)}else{G(this,ag,{x:-j,y:0},{x:ar,y:i.size})}var an=ao[0][0].toFixed(3)+","+ao[1][0].toFixed(3)+","+ao[0][1].toFixed(3)+","+ao[1][1].toFixed(3)+",0,0";var al=n(aq.x/d)+","+n(aq.y/d);ag.push('','','');this.element_.insertAdjacentHTML("beforeEnd",ag.join(""))};q.fillText=function(m,i,p,j){this.drawText_(m,i,p,j,false)};q.strokeText=function(m,i,p,j){this.drawText_(m,i,p,j,true)};q.measureText=function(m){if(!this.textMeasureEl_){var i='';this.element_.insertAdjacentHTML("beforeEnd",i);this.textMeasureEl_=this.element_.lastChild}var j=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(j.createTextNode(m));return{width:this.textMeasureEl_.offsetWidth}};q.clip=function(){};q.arcTo=function(){};q.createPattern=function(j,i){return new T(j,i)};function U(i){this.type_=i;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}U.prototype.addColorStop=function(j,i){i=F(i);this.colors_.push({offset:j,color:i.color,alpha:i.alpha})};function T(j,i){Q(j);switch(i){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=i;break;default:O("SYNTAX_ERR")}this.src_=j.src;this.width_=j.width;this.height_=j.height}function O(i){throw new P(i)}function Q(i){if(!i||i.nodeType!=1||i.tagName!="IMG"){O("TYPE_MISMATCH_ERR")}if(i.readyState!="complete"){O("INVALID_STATE_ERR")}}function P(i){this.code=this[i];this.message=i+": DOM Exception "+this.code}var X=P.prototype=new Error();X.INDEX_SIZE_ERR=1;X.DOMSTRING_SIZE_ERR=2;X.HIERARCHY_REQUEST_ERR=3;X.WRONG_DOCUMENT_ERR=4;X.INVALID_CHARACTER_ERR=5;X.NO_DATA_ALLOWED_ERR=6;X.NO_MODIFICATION_ALLOWED_ERR=7;X.NOT_FOUND_ERR=8;X.NOT_SUPPORTED_ERR=9;X.INUSE_ATTRIBUTE_ERR=10;X.INVALID_STATE_ERR=11;X.SYNTAX_ERR=12;X.INVALID_MODIFICATION_ERR=13;X.NAMESPACE_ERR=14;X.INVALID_ACCESS_ERR=15;X.VALIDATION_ERR=16;X.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=e;CanvasRenderingContext2D=D;CanvasGradient=U;CanvasPattern=T;DOMException=P})()}Ext.define("Ext.draw.engine.Canvas",{extend:"Ext.draw.Surface",requires:["Ext.draw.engine.excanvas","Ext.draw.Animator","Ext.draw.Color"],config:{highPrecision:false},statics:{contextOverrides:{setGradientBBox:function(a){this.bbox=a},fill:function(){var c=this.fillStyle,a=this.fillGradient,b=this.fillOpacity,d=this.globalAlpha,e=this.bbox;if(c!==Ext.draw.Color.RGBA_NONE&&b!==0){if(a&&e){this.fillStyle=a.generateGradient(this,e)}if(b!==1){this.globalAlpha=d*b}this.$fill();if(b!==1){this.globalAlpha=d}if(a&&e){this.fillStyle=c}}},stroke:function(){var e=this.strokeStyle,c=this.strokeGradient,a=this.strokeOpacity,b=this.globalAlpha,d=this.bbox;if(e!==Ext.draw.Color.RGBA_NONE&&a!==0){if(c&&d){this.strokeStyle=c.generateGradient(this,d)}if(a!==1){this.globalAlpha=b*a}this.$stroke();if(a!==1){this.globalAlpha=b}if(c&&d){this.strokeStyle=e}}},fillStroke:function(d,e){var j=this,i=this.fillStyle,h=this.fillOpacity,f=this.strokeStyle,c=this.strokeOpacity,b=j.shadowColor,a=j.shadowBlur,g=Ext.draw.Color.RGBA_NONE;if(e===undefined){e=d.transformFillStroke}if(!e){d.inverseMatrix.toContext(j)}if(i!==g&&h!==0){j.fill();j.shadowColor=g;j.shadowBlur=0}if(f!==g&&c!==0){j.stroke()}j.shadowColor=b;j.shadowBlur=a},setLineDash:function(a){if(this.$setLineDash){this.$setLineDash(a)}},getLineDash:function(){if(this.$getLineDash){return this.$getLineDash()}},ellipse:function(g,e,c,a,j,b,f,d){var i=Math.cos(j),h=Math.sin(j);this.transform(i*c,h*c,-h*a,i*a,g,e);this.arc(0,0,1,b,f,d);this.transform(i/c,-h/a,h/c,i/a,-(i*g+h*e)/c,(h*g-i*e)/a)},appendPath:function(f){var e=this,c=0,b=0,a=f.commands,g=f.params,d=a.length;e.beginPath();for(;c=D.length){C.createCanvas()}x=D[q].dom;x.style.left=A+"px";x.style.top=z+"px";m=Math.min(n,y-z);if(m*u!==x.height){x.height=m*u;x.style.height=m+"px"}o=Math.min(n,d-A);if(o*u!==x.width){x.width=o*u;x.style.width=o+"px"}C.applyDefaults(C.contexts[q])}}for(q+=1;qB||a.x+a.widthf||a.y+a.height=0},filename:Ext.isString,width:Ext.isNumber,height:Ext.isNumber,scale:Ext.isNumber,pdf:Ext.isObject,jpeg:Ext.isObject},initAnimator:function(){this.frameCallbackId=Ext.draw.Animator.addFrameCallback("renderFrame",this)},applyGradients:function(b){var a=[],c,f,d,e;if(!Ext.isArray(b)){return a}for(c=0,f=b.length;c=0&&c[a[f].type]>h){a[f+1]=a[f];f--}a[f+1]=b}d=a[0].flatten(l,a);if(k==="image"){g=new Image();g.src=d.data;d.data=g;return d}if(k==="stream"){d.data=d.data.replace(/^data:image\/[^;]+/,"data:application/octet-stream");return d}return d},download:function(d){var e=this,a=[],b,c,f;d=Ext.apply({version:2,data:e.getImage().data},d);for(c in d){if(d.hasOwnProperty(c)){f=d[c];if(c in e.supportedOptions){if(e.supportedOptions[c].call(e,f)){a.push({tag:"input",type:"hidden",name:c,value:Ext.String.htmlEncode(Ext.isObject(f)?Ext.JSON.encode(f):f)})}}}}b=Ext.dom.Helper.markup({tag:"html",children:[{tag:"head"},{tag:"body",children:[{tag:"form",method:"POST",action:d.url||e.defaultDownloadServerUrl,children:a},{tag:"script",type:"text/javascript",children:'document.getElementsByTagName("form")[0].submit();'}]}]});window.open("","ImageDownload_"+Date.now()).document.write(b)},destroy:function(){var a=this.frameCallbackId;if(a){Ext.draw.Animator.removeFrameCallback(a)}this.callParent()}},function(){if(location.search.match("svg")){Ext.draw.Container.prototype.engine="Ext.draw.engine.Svg"}else{if((Ext.os.is.BlackBerry&&Ext.os.version.getMajor()===10)||(Ext.browser.is.AndroidStock4&&(Ext.os.version.getMinor()===1||Ext.os.version.getMinor()===2||Ext.os.version.getMinor()===3))){Ext.draw.Container.prototype.engine="Ext.draw.engine.Svg"}}});Ext.define("Ext.chart.theme.Base",{mixins:{factoryable:"Ext.mixin.Factoryable"},requires:["Ext.draw.Color"],factoryConfig:{type:"chart.theme"},isTheme:true,config:{baseColor:null,colors:undefined,gradients:null,chart:{defaults:{background:"white"}},axis:{defaults:{label:{x:0,y:0,textBaseline:"middle",textAlign:"center",fontSize:"default",fontFamily:"default",fontWeight:"default",fillStyle:"black"},title:{fillStyle:"black",fontSize:"default*1.23",fontFamily:"default",fontWeight:"default"},style:{strokeStyle:"black"},grid:{strokeStyle:"rgb(221, 221, 221)"}},top:{style:{textPadding:5}},bottom:{style:{textPadding:5}}},series:{defaults:{label:{fillStyle:"black",strokeStyle:"none",fontFamily:"default",fontWeight:"default",fontSize:"default*1.077",textBaseline:"middle",textAlign:"center"},labelOverflowPadding:5}},sprites:{text:{fontSize:"default",fontWeight:"default",fontFamily:"default",fillStyle:"black"}},seriesThemes:undefined,markerThemes:{type:["circle","cross","plus","square","triangle","diamond"]},useGradients:false,background:null},colorDefaults:["#94ae0a","#115fa6","#a61120","#ff8809","#ffd13e","#a61187","#24ad9a","#7c7474","#a66111"],constructor:function(a){this.initConfig(a);this.resolveDefaults()},defaultRegEx:/^default([+\-/\*]\d+(?:\.\d+)?)?$/,defaultOperators:{"*":function(b,a){return b*a},"+":function(b,a){return b+a},"-":function(b,a){return b-a}},resolveDefaults:function(){var a=this;Ext.onReady(function(){var f=Ext.clone(a.getSprites()),e=Ext.clone(a.getAxis()),d=Ext.clone(a.getSeries()),g,c,b;if(!a.superclass.defaults){g=Ext.getBody().createChild({tag:"div",cls:"x-component"});a.superclass.defaults={fontFamily:g.getStyle("fontFamily"),fontWeight:g.getStyle("fontWeight"),fontSize:parseFloat(g.getStyle("fontSize")),fontVariant:g.getStyle("fontVariant"),fontStyle:g.getStyle("fontStyle")};g.destroy()}a.replaceDefaults(f.text);a.setSprites(f);for(c in e){b=e[c];a.replaceDefaults(b.label);a.replaceDefaults(b.title)}a.setAxis(e);for(c in d){b=d[c];a.replaceDefaults(b.label)}a.setSeries(d)})},replaceDefaults:function(h){var e=this,g=e.superclass.defaults,a=e.defaultRegEx,d,f,c,b;if(Ext.isObject(h)){for(d in g){c=a.exec(h[d]);if(c){f=g[d];c=c[1];if(c){b=e.defaultOperators[c.charAt(0)];f=Math.round(b(f,parseFloat(c.substr(1))))}h[d]=f}}}},applyBaseColor:function(c){var a,b;if(c){a=c.isColor?c:Ext.draw.Color.fromString(c);b=a.getHSL()[2];if(b<0.15){a=a.createLighter(0.3)}else{if(b<0.3){a=a.createLighter(0.15)}else{if(b>0.85){a=a.createDarker(0.3)}else{if(b>0.7){a=a.createDarker(0.15)}}}}this.setColors([a.createDarker(0.3).toString(),a.createDarker(0.15).toString(),a.toString(),a.createLighter(0.12).toString(),a.createLighter(0.24).toString(),a.createLighter(0.31).toString()])}return c},applyColors:function(a){return a||this.colorDefaults},updateUseGradients:function(a){if(a){this.updateGradients({type:"linear",degrees:90})}},updateBackground:function(a){if(a){var b=this.getChart();b.defaults.background=a;this.setChart(b)}},updateGradients:function(a){var c=this.getColors(),e=[],h,b,d,f,g;if(Ext.isObject(a)){for(f=0,g=c&&c.length||0;fMath.PI){n-=Math.PI*2}if(k){n=n*(1-d)-Math.PI/2*d;j=c;c=m;m=j}else{n=n*(1-d)}h.rotationRads=n;h.x=g*(1-d)+b*d;h.y=f*(1-d)+a*d;p=b-g;o=a-f;if(Math.abs(o*c)>Math.abs(p*m)){if(o>0){h.calloutEndX=h.x-(m/2)*(p/o)*d;h.calloutEndY=h.y-(m/2)*d}else{h.calloutEndX=h.x+(m/2)*(p/o)*d;h.calloutEndY=h.y+(m/2)*d}}else{if(p>0){h.calloutEndX=h.x-c/2;h.calloutEndY=h.y-(c/2)*(o/p)*d}else{h.calloutEndX=h.x+c/2;h.calloutEndY=h.y+(c/2)*(o/p)*d}}if(h.calloutStartX&&h.calloutStartY){h.calloutHasLine=(p>0&&h.calloutStartXh.calloutEndX)||(o>0&&h.calloutStartYh.calloutEndY)}else{h.calloutHasLine=true}}return h},pushDown:function(a,b){b=this.callParent([a.calloutOriginal,b]);return this.setAttrs(a,b)},popUp:function(a,b){a=a.prototype;b=this.setAttrs(a,b);if(this._next){return this._next.popUp(a,b)}else{return Ext.apply(a,b)}}});Ext.define("Ext.chart.label.Label",{extend:"Ext.draw.sprite.Text",requires:["Ext.chart.label.Callout"],inheritableStatics:{def:{processors:{callout:"limited01",calloutHasLine:"bool",calloutPlaceX:"number",calloutPlaceY:"number",calloutStartX:"number",calloutStartY:"number",calloutEndX:"number",calloutEndY:"number",calloutColor:"color",calloutWidth:"number",calloutVertical:"bool",labelOverflowPadding:"number",display:"enums(none,under,over,rotate,insideStart,insideEnd,inside,outside)",orientation:"enums(horizontal,vertical)",renderer:"default"},defaults:{callout:0,calloutHasLine:true,calloutPlaceX:0,calloutPlaceY:0,calloutStartX:0,calloutStartY:0,calloutEndX:0,calloutEndY:0,calloutWidth:1,calloutVertical:false,calloutColor:"black",labelOverflowPadding:5,display:"none",orientation:"",renderer:null},triggers:{callout:"transform",calloutPlaceX:"transform",calloutPlaceY:"transform",labelOverflowPadding:"transform",calloutRotation:"transform",display:"hidden"},updaters:{hidden:function(a){a.hidden=a.display==="none"}}}},config:{fx:{customDurations:{callout:200}},field:null,calloutLine:true},applyCalloutLine:function(a){if(a){return Ext.apply({},a)}},prepareModifiers:function(){this.callParent(arguments);this.calloutModifier=new Ext.chart.label.Callout({sprite:this});this.fx.setNext(this.calloutModifier);this.calloutModifier.setNext(this.topModifier)},render:function(b,c){var e=this,a=e.attr,d=a.calloutColor;c.save();c.globalAlpha*=a.callout;if(c.globalAlpha>0&&a.calloutHasLine){if(d&&d.isGradient){d=d.getStops()[0].color}c.strokeStyle=d;c.fillStyle=d;c.lineWidth=a.calloutWidth;c.beginPath();c.moveTo(e.attr.calloutStartX,e.attr.calloutStartY);c.lineTo(e.attr.calloutEndX,e.attr.calloutEndY);c.stroke();c.beginPath();c.arc(e.attr.calloutStartX,e.attr.calloutStartY,1*a.calloutWidth,0,2*Math.PI,true);c.fill();c.beginPath();c.arc(e.attr.calloutEndX,e.attr.calloutEndY,1*a.calloutWidth,0,2*Math.PI,true);c.fill()}c.restore();Ext.draw.sprite.Text.prototype.render.apply(e,arguments)}});Ext.define("Ext.chart.series.Series",{requires:["Ext.chart.Markers","Ext.chart.label.Label","Ext.tip.ToolTip"],mixins:["Ext.mixin.Observable","Ext.mixin.Bindable"],isSeries:true,defaultBindProperty:"store",type:null,seriesType:"sprite",identifiablePrefix:"ext-line-",observableType:"series",darkerStrokeRatio:0.15,config:{chart:null,title:null,renderer:null,showInLegend:true,triggerAfterDraw:false,style:{},subStyle:{},themeStyle:{},colors:null,useDarkerStrokeColor:true,store:null,label:{},labelOverflowPadding:null,showMarkers:true,marker:null,markerSubStyle:null,itemInstancing:null,background:null,highlightItem:null,surface:null,overlaySurface:null,hidden:false,highlight:false,highlightCfg:{merge:function(a){return a},$value:{fillStyle:"yellow",strokeStyle:"red"}},animation:null,tooltip:null},directions:[],sprites:null,themeColorCount:function(){return 1},isStoreDependantColorCount:false,themeMarkerCount:function(){return 0},getFields:function(f){var e=this,a=[],c,b,d;for(b=0,d=f.length;b0){if(!Ext.isBoolean(h)||!h){for(d=0;da){a=f}}b.min=d;b.max=a},updateLabelData:function(){var h=this,l=h.getStore(),g=l.getData().items,f=h.getSprites(),a=h.getLabel().getTemplate(),n=Ext.Array.from(a.getField()),c,b,e,d,m,k;if(!f.length||!n.length){return}for(c=0;c=0){return d}}}}}},onChartDetached:function(a){var b=this;b.fireEvent("chartdetached",a,b);a.un("storechange","onStoreChange",b)},onChartAttached:function(a){var b=this;b.setBackground(b.getBackground());b.fireEvent("chartattached",a,b);a.on("storechange","onStoreChange",b);b.processData()},updateOverlaySurface:function(a){var b=this;if(a){if(b.getLabel()){b.getOverlaySurface().add(b.getLabel())}}},applyLabel:function(a,b){if(!b){b=new Ext.chart.Markers({zIndex:10});b.setTemplate(new Ext.chart.label.Label(a))}else{b.getTemplate().setAttributes(a)}return b},createItemInstancingSprite:function(c,b){var e=this,f=new Ext.chart.Markers(),a,d;f.setAttributes({zIndex:Number.MAX_VALUE});a=Ext.apply({},b);if(e.getHighlight()){a.highlight=e.getHighlight();a.modifiers=["highlight"]}f.setTemplate(a);d=f.getTemplate();d.setAttributes(e.getStyle());d.fx.on("animationstart","onSpriteAnimationStart",this);d.fx.on("animationend","onSpriteAnimationEnd",this);c.bindMarker("items",f);e.getSurface().add(f);return f},getDefaultSpriteConfig:function(){return{type:this.seriesType,renderer:this.getRenderer()}},updateRenderer:function(c){var b=this,a=b.getChart(),d;if(a&&a.isInitializing){return}d=b.getSprites();if(d.length){d[0].setAttributes({renderer:c||null});if(a&&!a.isInitializing){a.redraw()}}},updateShowMarkers:function(a){var d=this.getSprites(),b=d&&d[0],c=b&&b.getMarker("markers");if(c){c.getTemplate().setAttributes({hidden:!a})}},createSprite:function(){var f=this,a=f.getSurface(),e=f.getItemInstancing(),d=a.add(f.getDefaultSpriteConfig()),b=f.getMarker(),g,c;d.setAttributes(f.getStyle());d.setSeries(f);if(e){d.itemsMarker=f.createItemInstancingSprite(d,e)}if(d.bindMarker){if(b){g=new Ext.chart.Markers();c=Ext.Object.merge({},b);if(f.getHighlight()){c.highlight=f.getHighlight();c.modifiers=["highlight"]}g.setTemplate(c);g.getTemplate().fx.setCustomDurations({translationX:0,translationY:0});d.dataMarker=g;d.bindMarker("markers",g);f.getOverlaySurface().add(g)}if(f.getLabel().getTemplate().getField()){d.bindMarker("labels",f.getLabel())}}if(d.setStore){d.setStore(f.getStore())}d.fx.on("animationstart","onSpriteAnimationStart",f);d.fx.on("animationend","onSpriteAnimationEnd",f);f.sprites.push(d);return d},getSprites:Ext.emptyFn,onDataChanged:function(){var d=this,c=d.getChart(),b=c&&c.getStore(),a=d.getStore();if(a!==b){d.processData()}},isXType:function(a){return a==="series"},getItemId:function(){return this.getId()},applyThemeStyle:function(e,a){var b=this,d,c;d=e&&e.subStyle&&e.subStyle.fillStyle;c=d&&e.subStyle.strokeStyle;if(d&&!c){e.subStyle.strokeStyle=b.getStrokeColorsFromFillColors(d)}d=e&&e.markerSubStyle&&e.markerSubStyle.fillStyle;c=d&&e.markerSubStyle.strokeStyle;if(d&&!c){e.markerSubStyle.strokeStyle=b.getStrokeColorsFromFillColors(d)}return Ext.apply(a||{},e)},applyStyle:function(c,b){var a=Ext.ClassManager.get(Ext.ClassManager.getNameByAlias("sprite."+this.seriesType));if(a&&a.def){c=a.def.normalize(c)}return Ext.apply({},c,b)},applySubStyle:function(b,c){var a=Ext.ClassManager.get(Ext.ClassManager.getNameByAlias("sprite."+this.seriesType));if(a&&a.def){b=a.def.batchedNormalize(b,true)}return Ext.merge({},c,b)},applyMarker:function(c,a){var d=(c&&c.type)||(a&&a.type)||"circle",b=Ext.ClassManager.get(Ext.ClassManager.getNameByAlias("sprite."+d));if(b&&b.def){c=b.def.normalize(Ext.isObject(c)?c:{},true);c.type=d}return Ext.merge(a||{},c)},applyMarkerSubStyle:function(c,a){var d=(c&&c.type)||(a&&a.type)||"circle",b=Ext.ClassManager.get(Ext.ClassManager.getNameByAlias("sprite."+d));if(b&&b.def){c=b.def.batchedNormalize(c,true)}return Ext.merge(a||{},c)},updateHidden:function(b){var a=this;a.getColors();a.getSubStyle();a.setSubStyle({hidden:b});a.processData();a.doUpdateStyles();if(!Ext.isArray(b)){a.updateLegendStore(b)}},updateLegendStore:function(f,b){var e=this,d=e.getChart(),c=d.getLegendStore(),g=e.getId(),a;if(c){if(arguments.length>1){a=c.findBy(function(h){return h.get("series")===g&&h.get("index")===b});if(a!==-1){a=c.getAt(a)}}else{a=c.findRecord("series",g)}if(a&&a.get("disabled")!==f){a.set("disabled",f)}}},setHiddenByIndex:function(a,c){var b=this;if(Ext.isArray(b.getHidden())){b.getHidden()[a]=c;b.updateHidden(b.getHidden());b.updateLegendStore(c,a)}else{b.setHidden(c)}},getStrokeColorsFromFillColors:function(a){var c=this,e=c.getUseDarkerStrokeColor(),b=(Ext.isNumber(e)?e:c.darkerStrokeRatio),d;if(e){d=Ext.Array.map(a,function(f){f=Ext.isString(f)?f:f.stops[0].color;f=Ext.draw.Color.fromString(f);return f.createDarker(b).toString()})}else{d=Ext.Array.clone(a)}return d},updateThemeColors:function(b){var c=this,d=c.getThemeStyle(),a=Ext.Array.clone(b),f=c.getStrokeColorsFromFillColors(b),e={fillStyle:a,strokeStyle:f};d.subStyle=Ext.apply(d.subStyle||{},e);d.markerSubStyle=Ext.apply(d.markerSubStyle||{},e);c.doUpdateStyles()},themeOnlyIfConfigured:{},updateTheme:function(d){var h=this,a=d.getSeries(),n=h.getInitialConfig(),c=h.defaultConfig,f=h.getConfigurator().configs,j=a.defaults,k=a[h.type],g=h.themeOnlyIfConfigured,l,i,o,b,m,e;a=Ext.merge({},j,k);for(l in a){i=a[l];e=f[l];if(i!==null&&i!==undefined&&e){m=n[l];o=Ext.isObject(i);b=m===c[l];if(o){if(b&&g[l]){continue}i=Ext.merge({},i,m)}if(b||o){h[e.names.set](i)}}}},updateChartColors:function(a){var b=this;if(!b.getColors()){b.updateThemeColors(a)}},updateColors:function(a){this.updateThemeColors(a)},updateStyle:function(){this.doUpdateStyles()},updateSubStyle:function(){this.doUpdateStyles()},updateThemeStyle:function(){this.doUpdateStyles()},doUpdateStyles:function(){var g=this,h=g.sprites,d=g.getItemInstancing(),c=0,f=h&&h.length,a=g.getConfig("showMarkers",true),b=g.getMarker(),e;for(;ce.to){j.call(this,e.max,e.getLabel(e.max),e.steps+1,e)}}else{b=this.getAxis();h=b.floatingAxes;d=[];f=(e.to-e.from)/(e.steps+1);if(b.getFloating()){for(a in h){d.push(h[a])}}function l(i){return !d.length||k(d,function(n){return m(n-i)>f})}if(e.mine.to&&l(e.max)){j.call(this,e.max,e.max,e.steps+1,e)}}},renderTicks:function(l,m,s,p){var v=this,k=v.attr,u=k.position,n=k.matrix,e=0.5*k.lineWidth,f=n.getXX(),i=n.getDX(),j=n.getYY(),h=n.getDY(),o=s.majorTicks,d=k.majorTickSize,a=s.minorTicks,r=k.minorTickSize;if(o){switch(u){case"right":function q(w){return function(x,z,y){x=l.roundPixel(x*j+h)+e;m.moveTo(0,x);m.lineTo(w,x)}}v.iterate(o,q(d));a&&v.iterate(a,q(r));break;case"left":function t(w){return function(x,z,y){x=l.roundPixel(x*j+h)+e;m.moveTo(p[2]-w,x);m.lineTo(p[2],x)}}v.iterate(o,t(d));a&&v.iterate(a,t(r));break;case"bottom":function c(w){return function(x,z,y){x=l.roundPixel(x*f+i)-e;m.moveTo(x,0);m.lineTo(x,w)}}v.iterate(o,c(d));a&&v.iterate(a,c(r));break;case"top":function b(w){return function(x,z,y){x=l.roundPixel(x*f+i)-e;m.moveTo(x,p[3]);m.lineTo(x,p[3]-w)}}v.iterate(o,b(d));a&&v.iterate(a,b(r));break;case"angular":v.iterate(o,function(w,y,x){w=w/(k.max+1)*Math.PI*2+k.baseRotation;m.moveTo(k.centerX+(k.length)*Math.cos(w),k.centerY+(k.length)*Math.sin(w));m.lineTo(k.centerX+(k.length+d)*Math.cos(w),k.centerY+(k.length+d)*Math.sin(w))});break;case"gauge":var g=v.getGaugeAngles();v.iterate(o,function(w,y,x){w=(w-k.min)/(k.max-k.min+1)*k.totalAngle-k.totalAngle+g.start;m.moveTo(k.centerX+(k.length)*Math.cos(w),k.centerY+(k.length)*Math.sin(w));m.lineTo(k.centerX+(k.length+d)*Math.cos(w),k.centerY+(k.length+d)*Math.sin(w))});break}}},renderLabels:function(E,q,D,K){var o=this,k=o.attr,i=0.5*k.lineWidth,u=k.position,y=k.matrix,A=k.textPadding,x=y.getXX(),d=y.getDX(),g=y.getYY(),c=y.getDY(),n=0,I=D.majorTicks,G=Math.max(k.majorTickSize,k.minorTickSize)+k.lineWidth,f=Ext.draw.Draw.isBBoxIntersect,F=o.getLabel(),J,s,r=null,w=0,b=0,m=D.segmenter,B=o.getRenderer(),t=o.getAxis(),z=t.getTitle(),a=z&&z.attr.text!==""&&z.getBBox(),l,h=null,p,C,v,e,H;if(I&&F&&!F.attr.hidden){J=F.attr.font;if(q.font!==J){q.font=J}F.setAttributes({translationX:0,translationY:0},true);F.applyTransformations();l=F.attr.inverseMatrix.elements.slice(0);switch(u){case"left":e=a?a.x+a.width:0;switch(F.attr.textAlign){case"start":H=E.roundPixel(e+d)-i;break;case"end":H=E.roundPixel(K[2]-G+d)-i;break;default:H=E.roundPixel(e+(K[2]-e-G)/2+d)-i}F.setAttributes({translationX:H},true);break;case"right":e=a?K[2]-a.x:0;switch(F.attr.textAlign){case"start":H=E.roundPixel(G+d)+i;break;case"end":H=E.roundPixel(K[2]-e+d)+i;break;default:H=E.roundPixel(G+(K[2]-G-e)/2+d)+i}F.setAttributes({translationX:H},true);break;case"top":e=a?a.y+a.height:0;F.setAttributes({translationY:E.roundPixel(e+(K[3]-e-G)/2)-i},true);break;case"bottom":e=a?K[3]-a.y:0;F.setAttributes({translationY:E.roundPixel(G+(K[3]-G-e)/2)+i},true);break;case"radial":F.setAttributes({translationX:k.centerX},true);break;case"angular":F.setAttributes({translationY:k.centerY},true);break;case"gauge":F.setAttributes({translationY:k.centerY},true);break}if(u==="left"||u==="right"){o.iterate(I,function(L,N,M){if(N===undefined){return}if(B){v=Ext.callback(B,null,[t,N,D,r],0,t)}else{v=m.renderer(N,D,r)}r=N;F.setAttributes({text:String(v),translationY:E.roundPixel(L*g+c)},true);F.applyTransformations();n=Math.max(n,F.getBBox().width+G);if(n<=o.thickness){C=Ext.draw.Matrix.fly(F.attr.matrix.elements.slice(0));p=C.prepend.apply(C,l).transformBBox(F.getBBox(true));if(h&&!f(p,h,A)){return}E.renderSprite(F);h=p;w+=p.height;b++}})}else{if(u==="top"||u==="bottom"){o.iterate(I,function(L,N,M){if(N===undefined){return}if(B){v=Ext.callback(B,null,[t,N,D,r],0,t)}else{v=m.renderer(N,D,r)}r=N;F.setAttributes({text:String(v),translationX:E.roundPixel(L*x+d)},true);F.applyTransformations();n=Math.max(n,F.getBBox().height+G);if(n<=o.thickness){C=Ext.draw.Matrix.fly(F.attr.matrix.elements.slice(0));p=C.prepend.apply(C,l).transformBBox(F.getBBox(true));if(h&&!f(p,h,A)){return}E.renderSprite(F);h=p;w+=p.width;b++}})}else{if(u==="radial"){o.iterate(I,function(L,N,M){if(N===undefined){return}if(B){v=Ext.callback(B,null,[t,N,D,r],0,t)}else{v=m.renderer(N,D,r)}r=N;if(typeof v!=="undefined"){F.setAttributes({text:String(v),translationX:k.centerX-E.roundPixel(L)/k.max*k.length*Math.cos(k.baseRotation+Math.PI/2),translationY:k.centerY-E.roundPixel(L)/k.max*k.length*Math.sin(k.baseRotation+Math.PI/2)},true);F.applyTransformations();p=F.attr.matrix.transformBBox(F.getBBox(true));if(h&&!f(p,h)){return}E.renderSprite(F);h=p;w+=p.width;b++}})}else{if(u==="angular"){s=k.majorTickSize+k.lineWidth*0.5+(parseInt(F.attr.fontSize,10)||10)/2;o.iterate(I,function(L,N,M){if(N===undefined){return}if(B){v=Ext.callback(B,null,[t,N,D,r],0,t)}else{v=m.renderer(N,D,r)}r=N;n=Math.max(n,Math.max(k.majorTickSize,k.minorTickSize)+(k.lineCap!=="butt"?k.lineWidth*0.5:0));if(typeof v!=="undefined"){var O=L/(k.max+1)*Math.PI*2+k.baseRotation;F.setAttributes({text:String(v),translationX:k.centerX+(k.length+s)*Math.cos(O),translationY:k.centerY+(k.length+s)*Math.sin(O)},true);F.applyTransformations();p=F.attr.matrix.transformBBox(F.getBBox(true));if(h&&!f(p,h)){return}E.renderSprite(F);h=p;w+=p.width;b++}})}else{if(u==="gauge"){var j=o.getGaugeAngles();o.iterate(I,function(L,N,M){if(N===undefined){return}if(B){v=Ext.callback(B,null,[t,N,D,r],0,t)}else{v=m.renderer(N,D,r)}r=N;if(typeof v!=="undefined"){var O=(L-k.min)/(k.max-k.min+1)*k.totalAngle-k.totalAngle+j.start;F.setAttributes({text:String(v),translationX:k.centerX+(k.length+10)*Math.cos(O),translationY:k.centerY+(k.length+10)*Math.sin(O)},true);F.applyTransformations();p=F.attr.matrix.transformBBox(F.getBBox(true));if(h&&!f(p,h)){return}E.renderSprite(F);h=p;w+=p.width;b++}})}}}}}if(k.enlargeEstStepSizeByText&&b){w/=b;w+=G;w*=2;if(k.estStepSize1){o.thickness=n;k.bbox.plain.dirty=true;k.bbox.transform.dirty=true;o.doThicknessChanged();return false}}},renderAxisLine:function(a,i,e,c){var h=this,g=h.attr,b=g.lineWidth*0.5,j=g.position,d,f;if(g.axisLine&&g.length){switch(j){case"left":d=a.roundPixel(c[2])-b;i.moveTo(d,-g.endGap);i.lineTo(d,g.length+g.startGap+1);break;case"right":i.moveTo(b,-g.endGap);i.lineTo(b,g.length+g.startGap+1);break;case"bottom":i.moveTo(-g.startGap,b);i.lineTo(g.length+g.endGap,b);break;case"top":d=a.roundPixel(c[3])-b;i.moveTo(-g.startGap,d);i.lineTo(g.length+g.endGap,d);break;case"angular":i.moveTo(g.centerX+g.length,g.centerY);i.arc(g.centerX,g.centerY,g.length,0,Math.PI*2,true);break;case"gauge":f=h.getGaugeAngles();i.moveTo(g.centerX+Math.cos(f.start)*g.length,g.centerY+Math.sin(f.start)*g.length);i.arc(g.centerX,g.centerY,g.length,f.start,f.end,true);break}}},getGaugeAngles:function(){var a=this,c=a.attr.totalAngle,b;if(c<=Math.PI){b=(Math.PI-c)*0.5}else{b=-(Math.PI*2-c)*0.5}b=Math.PI*2-b;return{start:b,end:b-c}},renderGridLines:function(m,n,s,r){var t=this,b=t.getAxis(),l=t.attr,p=l.matrix,d=l.startGap,a=l.endGap,c=p.getXX(),k=p.getYY(),h=p.getDX(),g=p.getDY(),u=l.position,f=b.getGridAlignment(),q=s.majorTicks,e,o,i;if(l.grid){if(q){if(u==="left"||u==="right"){i=l.min*k+g+a+d;t.iterate(q,function(j,w,v){e=j*k+g+a;t.putMarker(f+"-"+(v%2?"odd":"even"),{y:e,height:i-e},o=v,true);i=e});o++;e=0;t.putMarker(f+"-"+(o%2?"odd":"even"),{y:e,height:i-e},o,true)}else{if(u==="top"||u==="bottom"){i=l.min*c+h+d;if(d){t.putMarker(f+"-even",{x:0,width:i},-1,true)}t.iterate(q,function(j,w,v){e=j*c+h+d;t.putMarker(f+"-"+(v%2?"odd":"even"),{x:e,width:i-e},o=v,true);i=e});o++;e=l.length+l.startGap+l.endGap;t.putMarker(f+"-"+(o%2?"odd":"even"),{x:e,width:i-e},o,true)}else{if(u==="radial"){t.iterate(q,function(j,w,v){if(!j){return}e=j/l.max*l.length;t.putMarker(f+"-"+(v%2?"odd":"even"),{scalingX:e,scalingY:e},v,true);i=e})}else{if(u==="angular"){t.iterate(q,function(j,w,v){if(!l.length){return}e=j/(l.max+1)*Math.PI*2+l.baseRotation;t.putMarker(f+"-"+(v%2?"odd":"even"),{rotationRads:e,rotationCenterX:0,rotationCenterY:0,scalingX:l.length,scalingY:l.length},v,true);i=e})}}}}}}},renderLimits:function(o){var t=this,a=t.getAxis(),h=a.getChart(),p=h.getInnerPadding(),d=Ext.Array.from(a.getLimits());if(!d.length){return}var r=a.limits.surface.getRect(),m=t.attr,n=m.matrix,u=m.position,k=Ext.Object.chain,v=a.limits.titles,c,j,b,s,l,q,f,g,e;v.instances=[];v.position=0;if(u==="left"||u==="right"){for(q=0,f=d.length;qm.max){continue}l=l/m.max*m.length;s.line.cx=m.centerX;s.line.cy=m.centerY;s.line.scalingX=l;s.line.scalingY=l;s.line.strokeStyle=s.line.strokeStyle||m.strokeStyle;t.putMarker("circular-limit-lines",s.line,q,true);if(s.line.title){v.createInstance(s.line.title);c=v.getBBoxFor(v.position-1);v.setAttributesFor(v.position-1,{x:m.centerX,y:m.centerY-l-c.height/2,fillStyle:s.line.title.fillStyle||s.line.strokeStyle})}}}else{if(u==="angular"){for(q=0,f=d.length;q-0.5*Math.PI&&l<0.5*Math.PI)||(l>1.5*Math.PI&&l<2*Math.PI))?1:-1;v.setAttributesFor(v.position-1,{x:m.centerX+0.5*m.length*Math.cos(l)+b*c.height/2*Math.sin(l),y:m.centerY+0.5*m.length*Math.sin(l)-b*c.height/2*Math.cos(l),rotationRads:b===1?l:l-Math.PI,fillStyle:s.line.title.fillStyle||s.line.strokeStyle})}}}else{if(u==="gauge"){}}}}}},doThicknessChanged:function(){var a=this.getAxis();if(a){a.onThicknessChanged()}},render:function(a,c,d){var e=this,b=e.getLayoutContext();if(b){if(false===e.renderLabels(a,c,b,d)){return false}c.beginPath();e.renderTicks(a,c,b,d);e.renderAxisLine(a,c,b,d);e.renderGridLines(a,c,b,d);e.renderLimits(d);c.stroke()}}});Ext.define("Ext.chart.axis.segmenter.Segmenter",{config:{axis:null},constructor:function(a){this.initConfig(a)},renderer:function(b,a){return String(b)},from:function(a){return a},diff:Ext.emptyFn,align:Ext.emptyFn,add:Ext.emptyFn,preferredStep:Ext.emptyFn});Ext.define("Ext.chart.axis.segmenter.Names",{extend:"Ext.chart.axis.segmenter.Segmenter",alias:"segmenter.names",renderer:function(b,a){return b},diff:function(b,a,c){return Math.floor(a-b)},align:function(c,b,a){return Math.floor(c)},add:function(c,b,a){return c+b},preferredStep:function(c,a,b,d){return{unit:1,step:1}}});Ext.define("Ext.chart.axis.segmenter.Numeric",{extend:"Ext.chart.axis.segmenter.Segmenter",alias:"segmenter.numeric",isNumeric:true,renderer:function(b,a){return b.toFixed(Math.max(0,a.majorTicks.unit.fixes))},diff:function(b,a,c){return Math.floor((a-b)/c.scale)},align:function(c,b,a){return Math.floor(c/(a.scale*b))*a.scale*b},add:function(c,b,a){return c+b*a.scale},preferredStep:function(c,b){var a=Math.floor(Math.log(b)*Math.LOG10E),d=Math.pow(10,a);b/=d;if(b<2){b=2}else{if(b<5){b=5}else{if(b<10){b=10;a++}}}return{unit:{fixes:-a,scale:d},step:b}},exactStep:function(c,b){var a=Math.floor(Math.log(b)*Math.LOG10E),d=Math.pow(10,a);return{unit:{fixes:-a+(b%d===0?0:1),scale:1},step:b}},adjustByMajorUnit:function(e,g,c){var d=c[0],b=c[1],a=e*g,f=d%a;if(f!==0){c[0]=d-f+(d<0?-a:0)}f=b%a;if(f!==0){c[1]=b-f+(b>0?a:0)}}});Ext.define("Ext.chart.axis.segmenter.Time",{extend:"Ext.chart.axis.segmenter.Segmenter",alias:"segmenter.time",config:{step:null},renderer:function(c,b){var a=Ext.Date;switch(b.majorTicks.unit){case"y":return a.format(c,"Y");case"mo":return a.format(c,"Y-m");case"d":return a.format(c,"Y-m-d")}return a.format(c,"Y-m-d\nH:i:s")},from:function(a){return new Date(a)},diff:function(b,a,c){if(isFinite(b)){b=new Date(b)}if(isFinite(a)){a=new Date(a)}return Ext.Date.diff(b,a,c)},align:function(a,c,b){if(b==="d"&&c>=7){a=Ext.Date.align(a,"d",c);a.setDate(a.getDate()-a.getDay()+1);return a}else{return Ext.Date.align(a,b,c)}},add:function(c,b,a){return Ext.Date.add(new Date(c),a,b)},stepUnits:[[Ext.Date.YEAR,1,2,5,10,20,50,100,200,500],[Ext.Date.MONTH,1,3,6],[Ext.Date.DAY,1,7,14],[Ext.Date.HOUR,1,6,12],[Ext.Date.MINUTE,1,5,15,30],[Ext.Date.SECOND,1,5,15,30],[Ext.Date.MILLI,1,2,5,10,20,50,100,200,500]],preferredStep:function(b,e){if(this.getStep()){return this.getStep()}var f=new Date(+b),g=new Date(+b+Math.ceil(e)),d=this.stepUnits,l,k,h,c,a;for(c=0;c0){for(a=1;aa){f.max=f.to}if(f.froma){f.max=f.to}if(f.from0){f.from=f.from+c*f.step*i;while(f.from0?b:a},applyLabel:function(b,a){if(!a){a=new Ext.draw.sprite.Text({})}if(this.limitTitleTpl){this.limitTitleTpl.setAttributes(b)}a.setAttributes(b);return a},applyLayout:function(b,a){b=Ext.factory(b,null,a,"axisLayout");b.setAxis(this);return b},applySegmenter:function(a,b){a=Ext.factory(a,null,b,"segmenter");a.setAxis(this);return a},updateMinimum:function(){this.range=null},updateMaximum:function(){this.range=null},hideLabels:function(){this.getSprites()[0].setDirty(true);this.setLabel({hidden:true})},showLabels:function(){this.getSprites()[0].setDirty(true);this.setLabel({hidden:false})},renderFrame:function(){this.getSurface().renderFrame()},updateChart:function(d,b){var c=this,a;if(b){b.unregister(c);b.un("serieschange",c.onSeriesChange,c);b.un("redraw",c.renderLimits,c);c.linkAxis();c.fireEvent("chartdetached",b,c)}if(d){d.on("serieschange",c.onSeriesChange,c);c.surface=null;a=c.getSurface();c.getLabel().setSurface(a);a.add(c.getSprites());a.add(c.getTitle());d.register(c);c.fireEvent("chartattached",d,c)}},applyBackground:function(a){var b=Ext.ClassManager.getByAlias("sprite.rect");return b.def.normalize(a)},processData:function(){this.getLayout().processData();this.range=null},getDirection:function(){return this.getChart().getDirectionForAxis(this.getPosition())},isSide:function(){var a=this.getPosition();return a==="left"||a==="right"},applyFields:function(a){return Ext.Array.from(a)},applyVisibleRange:function(a,c){this.getChart();if(a[0]>a[1]){var b=a[0];a[0]=a[1];a[0]=b}if(a[1]===a[0]){a[1]+=1/this.getMaxZoom()}if(a[1]>a[0]+1){a[0]=0;a[1]=1}else{if(a[0]<0){a[1]-=a[0];a[0]=0}else{if(a[1]>1){a[0]-=a[1]-1;a[1]=1}}}if(c&&a[0]===c[0]&&a[1]===c[1]){return undefined}return a},updateVisibleRange:function(a){this.fireEvent("visiblerangechange",this,a)},onSeriesChange:function(e){var f=this,b=e.getSeries(),j="get"+f.getDirection()+"Axis",g=[],c,d=b.length,a,h;for(c=0;cn){n=c[1]}}}if(!isFinite(n)){n=m.prevMax}if(!isFinite(d)){d=m.prevMin}if(m.getLabelInSpan()||d===n){n+=m.getIncrement();d-=m.getIncrement()}if(Ext.isNumber(m.getMinimum())){d=m.getMinimum()}else{m.prevMin=d}if(Ext.isNumber(m.getMaximum())){n=m.getMaximum()}else{m.prevMax=n}m.range=[Ext.Number.correctFloat(d),Ext.Number.correctFloat(n)];if(m.getReconcileRange()){m.reconcileRange()}if(m.getAdjustByMajorUnit()&&l.adjustByMajorUnit&&!m.getMajorTickSteps()){j=Ext.Object.chain(m.getSprites()[0].attr);j.min=m.range[0];j.max=m.range[1];j.visibleMin=p[0];j.visibleMax=p[1];a={attr:j,segmenter:l};h.calculateLayout(a);g=a.majorTicks;if(g){l.adjustByMajorUnit(g.step,g.unit.scale,m.range);j.min=m.range[0];j.max=m.range[1];delete a.majorTicks;h.calculateLayout(a);g=a.majorTicks;l.adjustByMajorUnit(g.step,g.unit.scale,m.range)}else{if(!m.hasClearRangePending){m.hasClearRangePending=true;m.getChart().on("layout","clearRange",m)}}}if(!Ext.Array.equals(m.range,m.oldRange||[])){m.fireEvent("rangechange",m,m.range);m.oldRange=m.range}return m.range},clearRange:function(){delete this.hasClearRangePending;this.range=null},reconcileRange:function(){var e=this,g=e.getChart().getAxes(),f=e.getDirection(),b,d,c,a;if(!g){return}for(b=0,d=g.length;be.range[1]){e.range[1]=a[1]}}},applyStyle:function(c,b){var a=Ext.ClassManager.getByAlias("sprite."+this.seriesType);if(a&&a.def){c=a.def.normalize(c)}b=Ext.apply(b||{},c);return b},themeOnlyIfConfigured:{grid:true},updateTheme:function(d){var i=this,k=d.getAxis(),e=i.getPosition(),o=i.getInitialConfig(),c=i.defaultConfig,g=i.getConfigurator().configs,a=k.defaults,n=k[e],h=i.themeOnlyIfConfigured,l,j,p,b,m,f;k=Ext.merge({},a,n);for(l in k){j=k[l];f=g[l];if(j!==null&&j!==undefined&&f){m=o[l];p=Ext.isObject(j);b=m===c[l];if(p){if(b&&h[l]){continue}j=Ext.merge({},j,m)}if(b||p){i[f.names.set](j)}}}},updateCenter:function(b){var e=this.getSprites(),a=e[0],d=b[0],c=b[1];if(a){a.setAttributes({centerX:d,centerY:c})}if(this.gridSpriteEven){this.gridSpriteEven.getTemplate().setAttributes({translationX:d,translationY:c,rotationCenterX:d,rotationCenterY:c})}if(this.gridSpriteOdd){this.gridSpriteOdd.getTemplate().setAttributes({translationX:d,translationY:c,rotationCenterX:d,rotationCenterY:c})}},getSprites:function(){if(!this.getChart()){return}var i=this,e=i.getRange(),f=i.getPosition(),g=i.getChart(),c=g.getAnimation(),d,a,b=i.getLength(),h=i.superclass;if(c===false){c={duration:0}}if(e){a=Ext.applyIf({position:f,axis:i,min:e[0],max:e[1],length:b,grid:i.getGrid(),hidden:i.getHidden(),titleOffset:i.titleOffset,layout:i.getLayout(),segmenter:i.getSegmenter(),totalAngle:i.getTotalAngle(),label:i.getLabel()},i.getStyle());if(!i.sprites.length){while(!h.xtype){h=h.superclass}d=Ext.create("sprite."+h.xtype,a);d.fx.setCustomDurations({baseRotation:0});d.fx.on("animationstart","onAnimationStart",i);d.fx.on("animationend","onAnimationEnd",i);d.setLayout(i.getLayout());d.setSegmenter(i.getSegmenter());d.setLabel(i.getLabel());i.sprites.push(d);i.updateTitleSprite()}else{d=i.sprites[0];d.setAnimation(c);d.setAttributes(a)}if(i.getRenderer()){d.setRenderer(i.getRenderer())}}return i.sprites},updateTitleSprite:function(){var f=this,b=f.getLength();if(!f.sprites[0]||!Ext.isNumber(b)){return}var h=this.sprites[0].thickness,a=f.getSurface(),g=f.getTitle(),e=f.getPosition(),c=f.getMargin(),i=f.getTitleMargin(),d=a.roundPixel(b/2);if(g){switch(e){case"top":g.setAttributes({x:d,y:c+i/2,textBaseline:"top",textAlign:"center"},true);g.applyTransformations();f.titleOffset=g.getBBox().height+i;break;case"bottom":g.setAttributes({x:d,y:h+i/2,textBaseline:"top",textAlign:"center"},true);g.applyTransformations();f.titleOffset=g.getBBox().height+i;break;case"left":g.setAttributes({x:c+i/2,y:d,textBaseline:"top",textAlign:"center",rotationCenterX:c+i/2,rotationCenterY:d,rotationRads:-Math.PI/2},true);g.applyTransformations();f.titleOffset=g.getBBox().width+i;break;case"right":g.setAttributes({x:h-c+i/2,y:d,textBaseline:"bottom",textAlign:"center",rotationCenterX:h+i/2,rotationCenterY:d,rotationRads:Math.PI/2},true);g.applyTransformations();f.titleOffset=g.getBBox().width+i;break}}},onThicknessChanged:function(){this.getChart().onThicknessChanged()},getThickness:function(){if(this.getHidden()){return 0}return(this.sprites[0]&&this.sprites[0].thickness||1)+this.titleOffset+this.getMargin()},onAnimationStart:function(){this.spriteAnimationCount++;if(this.spriteAnimationCount===1){this.fireEvent("animationstart",this)}},onAnimationEnd:function(){this.spriteAnimationCount--;if(this.spriteAnimationCount===0){this.fireEvent("animationend",this)}},getItemId:function(){return this.getId()},getAncestorIds:function(){return[this.getChart().getId()]},isXType:function(a){return a==="axis"},resolveListenerScope:function(e){var d=this,a=Ext._namedScopes[e],c=d.getChart(),b;if(!a){b=c?c.resolveListenerScope(e,false):(e||d)}else{if(a.isThis){b=d}else{if(a.isController){b=c?c.resolveListenerScope(e,false):d}else{if(a.isSelf){b=c?c.resolveListenerScope(e,false):d;if(b===c&&!c.getInheritedConfig("defaultListenerScope")){b=d}}}}}return b},destroy:function(){var a=this;a.setChart(null);a.surface.destroy();a.surface=null;a.callParent()}});Ext.define("Ext.chart.LegendBase",{extend:"Ext.view.View",config:{tpl:['
','','
',"',"{name}","
","
","
"],nodeContainerSelector:"div."+Ext.baseCSSPrefix+"legend-container",itemSelector:"div."+Ext.baseCSSPrefix+"legend-item",docked:"bottom"},setDocked:function(d){var c=this,a=c.ownerCt,b;c.docked=d;switch(d){case"top":case"bottom":c.addCls(Ext.baseCSSPrefix+"horizontal");b="hbox";break;case"left":case"right":c.removeCls(Ext.baseCSSPrefix+"horizontal");b="vbox";break}if(a){a.setDocked(d)}},setStore:function(a){this.bindStore(a)},clearViewEl:function(){this.callParent(arguments);Ext.removeNode(this.getNodeContainer())},onItemClick:function(a,c,b,d){this.callParent(arguments);this.toggleItem(b)}});Ext.define("Ext.chart.Legend",{xtype:"legend",extend:"Ext.chart.LegendBase",config:{baseCls:Ext.baseCSSPrefix+"legend",padding:5,rect:null,disableSelection:true,toggleable:true},toggleItem:function(c){if(!this.getToggleable()){return}var b=this.getStore(),h=0,e,g=true,d,f,a;if(b){f=b.getCount();for(d=0;d1;a=b.getAt(c);if(a){e=a.get("disabled");if(e||g){a.set("disabled",!e)}}}}});Ext.define("Ext.chart.AbstractChart",{extend:"Ext.draw.Container",requires:["Ext.chart.theme.Default","Ext.chart.series.Series","Ext.chart.interactions.Abstract","Ext.chart.axis.Axis","Ext.data.StoreManager","Ext.chart.Legend","Ext.data.Store"],isChart:true,defaultBindProperty:"store",config:{store:"ext-empty-store",theme:"default",style:null,animation:!Ext.isIE8,series:[],axes:[],legend:null,colors:null,insetPadding:{top:10,left:10,right:10,bottom:10},background:null,interactions:[],mainRect:null,resizeHandler:null,highlightItem:null},animationSuspendCount:0,chartLayoutSuspendCount:0,axisThicknessSuspendCount:0,isThicknessChanged:false,surfaceZIndexes:{background:0,main:1,grid:2,series:3,axis:4,chart:5,overlay:6,events:7},constructor:function(a){var b=this;b.itemListeners={};b.surfaceMap={};b.chartComponents={};b.isInitializing=true;b.suspendChartLayout();b.animationSuspendCount++;b.callParent(arguments);delete b.isInitializing;b.getSurface("main");b.getSurface("chart").setFlipRtlText(b.getInherited().rtl);b.getSurface("overlay").waitFor(b.getSurface("series"));b.animationSuspendCount--;b.resumeChartLayout()},applyAnimation:function(a,b){if(!a){a={duration:0}}else{if(a===true){a={easing:"easeInOut",duration:500}}}return b?Ext.apply({},a,b):a},getAnimation:function(){if(this.animationSuspendCount){return{duration:0}}else{return this.callParent()}},applyInsetPadding:function(b,a){if(!Ext.isObject(b)){return Ext.util.Format.parseBox(b)}else{if(!a){return b}else{return Ext.apply(a,b)}}},suspendAnimation:function(){var d=this,c=d.getSeries(),e=c.length,b=-1,a;d.animationSuspendCount++;if(d.animationSuspendCount===1){while(++b0){this.axisThicknessSuspendCount--;if(this.axisThicknessSuspendCount===0&&this.isThicknessChanged){this.onThicknessChanged()}}},onThicknessChanged:function(){if(this.axisThicknessSuspendCount===0){this.isThicknessChanged=false;this.performLayout()}else{this.isThicknessChanged=true}},applySprites:function(b){var a=this.getSurface("chart");b=Ext.Array.from(b);a.removeAll(true);a.add(b);return b},initItems:function(){var a=this.items,b,d,c;if(a&&!a.isMixedCollection){this.items=[];a=Ext.Array.from(a);for(b=0,d=a.length;b=0){c.splice(b,1)}},applyAxes:function(b,k){var l=this,g={left:"right",right:"left"},m=[],c,d,e,a,f,h,j;l.animationSuspendCount++;l.getStore();if(!k){k=[];k.map={}}j=k.map;m.map={};b=Ext.Array.from(b,true);for(f=0,h=b.length;f0){a=b.applyColors(a)}return a||(c&&c.getColors())},applyColors:function(a){a=Ext.Array.map(a,function(b){if(Ext.isString(b)){return b}else{return b.toString()}});return a},updateColors:function(c){var k=this,e=k.getTheme(),a=c||(e&&e.getColors()),l=0,f=k.getSeries(),d=f&&f.length,g,j,b,h;if(a.length){for(g=0;g=0&&h<=e[2]&&g>=0&&g<=e[3])){return null}for(;b>=0;b--){c=a[b];j=c.getItemForPoint(h,g);if(j){return j}}return null},getItemsForPoint:function(h,g){var f=this,a=f.getSeries(),d=a.length,b=f.hasFirstLayout?d-1:-1,e=[],c,j;for(;b>=0;b--){c=a[b];j=c.getItemForPoint(h,g);if(j){e.push(j)}}return e},onAnimationStart:function(){this.fireEvent("animationstart",this)},onAnimationEnd:function(){this.fireEvent("animationend",this)},onDataChanged:function(){var d=this;if(d.isInitializing){return}var c=d.getMainRect(),a=d.getStore(),b=d.getSeries(),e=d.getAxes();if(!a||!e||!b){return}if(!c){d.on({redraw:d.onDataChanged,scope:d,single:true});return}d.processData();d.redraw()},recordCount:0,processData:function(){var g=this,e=g.getStore().getCount(),c=g.getSeries(),f=c.length,d=false,b=0,a;for(;bg.recordCount){g.updateColors(g.getColors());g.recordCount=e}},bindStore:function(a){this.setStore(a)},applyHighlightItem:function(f,a){if(f===a){return}if(Ext.isObject(f)&&Ext.isObject(a)){var e=f,d=a,c=e.sprite&&(e.sprite[0]||e.sprite),b=d.sprite&&(d.sprite[0]||d.sprite);if(c===b&&e.index===d.index){return}}return f},updateHighlightItem:function(b,a){if(a){a.series.setAttributesForItem(a,{highlighted:false})}if(b){b.series.setAttributesForItem(b,{highlighted:true});this.fireEvent("itemhighlight",this,b,a)}this.fireEvent("itemhighlightchange",this,b,a)},destroyChart:function(){var f=this,d=f.getLegend(),g=f.getAxes(),c=f.getSeries(),h=f.getInteractions(),b=[],a,e;f.surfaceMap=null;for(a=0,e=h.length;ad.getDepth()){g=f}else{for(a=0;ag){g=f}}}}d.setDepth(g)},updateDepth:function(d){var b=this,c=b.getSprites(),a={depth:d};if(c&&c.length){c[0].setAttributes(a)}if(b.gridSpriteEven&&b.gridSpriteOdd){b.gridSpriteEven.getTemplate().setAttributes(a);b.gridSpriteOdd.getTemplate().setAttributes(a)}},getGridAlignment:function(){switch(this.getPosition()){case"left":case"right":return"horizontal3d";case"top":case"bottom":return"vertical3d"}}});Ext.define("Ext.chart.axis.Category",{requires:["Ext.chart.axis.layout.CombineDuplicate","Ext.chart.axis.segmenter.Names"],extend:"Ext.chart.axis.Axis",alias:"axis.category",type:"category",config:{layout:"combineDuplicate",segmenter:"names"}});Ext.define("Ext.chart.axis.Category3D",{requires:["Ext.chart.axis.layout.CombineDuplicate","Ext.chart.axis.segmenter.Names"],extend:"Ext.chart.axis.Axis3D",alias:"axis.category3d",type:"category3d",config:{layout:"combineDuplicate",segmenter:"names"}});Ext.define("Ext.chart.axis.Numeric",{extend:"Ext.chart.axis.Axis",type:"numeric",alias:["axis.numeric","axis.radial"],requires:["Ext.chart.axis.layout.Continuous","Ext.chart.axis.segmenter.Numeric"],config:{layout:"continuous",segmenter:"numeric",aggregator:"double"}});Ext.define("Ext.chart.axis.Numeric3D",{extend:"Ext.chart.axis.Axis3D",alias:["axis.numeric3d"],type:"numeric3d",requires:["Ext.chart.axis.layout.Continuous","Ext.chart.axis.segmenter.Numeric"],config:{layout:"continuous",segmenter:"numeric",aggregator:"double"}});Ext.define("Ext.chart.axis.Time",{extend:"Ext.chart.axis.Numeric",alias:"axis.time",type:"time",requires:["Ext.chart.axis.layout.Continuous","Ext.chart.axis.segmenter.Time"],config:{calculateByLabelSize:true,dateFormat:null,fromDate:null,toDate:null,step:[Ext.Date.DAY,1],layout:"continuous",segmenter:"time",aggregator:"time"},updateDateFormat:function(a){this.setRenderer(function(c,b){return Ext.Date.format(new Date(b),a)})},updateFromDate:function(a){this.setMinimum(+a)},updateToDate:function(a){this.setMaximum(+a)},getCoordFor:function(a){if(Ext.isString(a)){a=new Date(a)}return +a}});Ext.define("Ext.chart.axis.Time3D",{extend:"Ext.chart.axis.Numeric3D",alias:"axis.time3d",type:"time3d",requires:["Ext.chart.axis.layout.Continuous","Ext.chart.axis.segmenter.Time"],config:{calculateByLabelSize:true,dateFormat:null,fromDate:null,toDate:null,step:[Ext.Date.DAY,1],layout:"continuous",segmenter:"time",aggregator:"time"},updateDateFormat:function(a){this.setRenderer(function(c,b){return Ext.Date.format(new Date(b),a)})},updateFromDate:function(a){this.setMinimum(+a)},updateToDate:function(a){this.setMaximum(+a)},getCoordFor:function(a){if(Ext.isString(a)){a=new Date(a)}return +a}});Ext.define("Ext.chart.grid.HorizontalGrid3D",{extend:"Ext.chart.grid.HorizontalGrid",alias:"grid.horizontal3d",inheritableStatics:{def:{processors:{depth:"number"},defaults:{depth:0}}},render:function(a,k,d){var f=this.attr,i=a.roundPixel(f.x),h=a.roundPixel(f.y),l=a.matrix.getDX(),c=k.lineWidth*0.5,j=f.height,e=f.depth,b,g;if(h<=d[1]){return}b=d[0]+e-l;g=h+c-e;k.beginPath();k.rect(b,g,d[2],j);k.fill();k.beginPath();k.moveTo(b,g);k.lineTo(b+d[2],g);k.stroke();b=d[0]+i-l;g=h+c;k.beginPath();k.moveTo(b,g);k.lineTo(b+e,g-e);k.lineTo(b+e,g-e+j);k.lineTo(b,g+j);k.closePath();k.fill();k.beginPath();k.moveTo(b,g);k.lineTo(b+e,g-e);k.stroke()}});Ext.define("Ext.chart.grid.VerticalGrid3D",{extend:"Ext.chart.grid.VerticalGrid",alias:"grid.vertical3d",inheritableStatics:{def:{processors:{depth:"number"},defaults:{depth:0}}},render_:function(c,d,f){var b=this.attr,a=c.roundPixel(b.x),e=d.lineWidth*0.5;d.beginPath();d.rect(a-e,f[1]-c.matrix.getDY(),b.width,f[3]);d.fill();d.beginPath();d.moveTo(a-e,f[1]-c.matrix.getDY());d.lineTo(a-e,f[1]+f[3]-c.matrix.getDY());d.stroke()},render:function(b,j,e){var g=this.attr,i=b.roundPixel(g.x),k=b.matrix.getDY(),d=j.lineWidth*0.5,a=g.width,f=g.depth,c,h;if(i>=e[2]){return}c=i-d+f;h=e[1]-f-k;j.beginPath();j.rect(c,h,a,e[3]);j.fill();j.beginPath();j.moveTo(c,h);j.lineTo(c,h+e[3]);j.stroke();c=i-d;h=e[3];j.beginPath();j.moveTo(c,h);j.lineTo(c+f,h-f);j.lineTo(c+f+a,h-f);j.lineTo(c+a,h);j.closePath();j.fill();c=i-d;h=e[3];j.beginPath();j.moveTo(c,h);j.lineTo(c+f,h-f);j.stroke()}});Ext.define("Ext.chart.interactions.CrossZoom",{extend:"Ext.chart.interactions.Abstract",type:"crosszoom",alias:"interaction.crosszoom",isCrossZoom:true,config:{axes:true,gestures:{dragstart:"onGestureStart",drag:"onGesture",dragend:"onGestureEnd",dblclick:"onDoubleTap"},undoButton:{}},stopAnimationBeforeSync:false,zoomAnimationInProgress:false,constructor:function(){this.callParent(arguments);this.zoomHistory=[]},applyAxes:function(b){var a={};if(b===true){return{top:{},right:{},bottom:{},left:{}}}else{if(Ext.isArray(b)){a={};Ext.each(b,function(c){a[c]={}})}else{if(Ext.isObject(b)){Ext.iterate(b,function(c,d){if(d===true){a[c]={}}else{if(d!==false){a[c]=d}}})}}}return a},applyUndoButton:function(b,a){var c=this;if(a){a.destroy()}if(b){return Ext.create("Ext.Button",Ext.apply({cls:[],text:"Undo Zoom",disabled:true,handler:function(){c.undoZoom()}},b))}},getSurface:function(){return this.getChart()&&this.getChart().getSurface("main")},setSeriesOpacity:function(b){var a=this.getChart()&&this.getChart().getSurface("series");if(a){a.element.setStyle("opacity",b)}},onGestureStart:function(h){var j=this,i=j.getChart(),d=j.getSurface(),l=i.getInnerRect(),c=i.getInnerPadding(),g=c.left,b=g+l[2],f=c.top,a=f+l[3],n=i.getEventXY(h),m=n[0],k=n[1];if(j.zoomAnimationInProgress){return}if(m>g&&mf&&kb){m=b}}if(ka){k=a}}j.selectionRect.setAttributes({width:m-j.startX,height:k-j.startY});if(Math.abs(j.startX-m)<11||Math.abs(j.startY-k)<11){j.selectionRect.setAttributes({globalAlpha:0.5})}else{j.selectionRect.setAttributes({globalAlpha:1})}d.renderFrame();return false}},onGestureEnd:function(i){var l=this;if(l.zoomAnimationInProgress){return}if(l.getLocks()[l.gestureEvent]===l){var k=l.getChart(),d=l.getSurface(),n=k.getInnerRect(),c=k.getInnerPadding(),g=c.left,b=g+n[2],f=c.top,a=f+n[3],h=n[2],j=n[3],p=k.getEventXY(i),o=p[0],m=p[1];if(ob){o=b}}if(ma){m=a}}if(Math.abs(l.startX-o)<11||Math.abs(l.startY-m)<11){d.remove(l.selectionRect)}else{l.zoomBy([Math.min(l.startX,o)/h,1-Math.max(l.startY,m)/j,Math.max(l.startX,o)/h,1-Math.min(l.startY,m)/j]);l.selectionRect.setAttributes({x:Math.min(l.startX,o),y:Math.min(l.startY,m),width:Math.abs(l.startX-o),height:Math.abs(l.startY-m)});l.selectionRect.setAnimation(k.getAnimation()||{duration:0});l.selectionRect.setAttributes({globalAlpha:0,x:0,y:0,width:h,height:j});l.zoomAnimationInProgress=true;k.suspendThicknessChanged();l.selectionRect.fx.on("animationend",function(){k.resumeThicknessChanged();d.remove(l.selectionRect);l.selectionRect=null;l.zoomAnimationInProgress=false})}d.renderFrame();l.sync();l.unlockEvents(l.gestureEvent);l.setSeriesOpacity(1);if(!l.zoomAnimationInProgress){d.remove(l.selectionRect);l.selectionRect=null}}},zoomBy:function(o){var n=this,a=n.getAxes(),k=n.getChart(),j=k.getAxes(),l=k.getInherited().rtl,f,d={},c,b;if(l){o=o.slice();c=1-o[0];b=1-o[2];o[0]=Math.min(c,b);o[2]=Math.max(c,b)}for(var h=0;h0&&D0&&CE){k=E}}if(j<0){j=0}else{if(j>f){j=f}}k+=t;j+=q;for(B=0;B"))},onDragStart:function(d){var c=this,a=c.getChart(),b=a.getHighlightItem();if(b){a.fireEvent("beginitemedit",a,c,c.item=b);return false}},onDrag:function(f){var d=this,b=d.getChart(),c=b.getHighlightItem(),a=c&&c.sprite.type;if(c){switch(a){case"barSeries":return d.onDragBar(f);break;case"scatterSeries":return d.onDragScatter(f);break}}},highlight:function(f){var e=this,d=e.getChart(),a=d.getFlipXY(),g=e.getCursors(),c=f&&f.sprite.type,b=d.el.dom.style;e.callParent([f]);if(f){switch(c){case"barSeries":if(a){b.cursor=g.ewResize}else{b.cursor=g.nsResize}break;case"scatterSeries":b.cursor=g.move;break}}else{d.el.dom.style.cursor="default"}},onDragBar:function(i){var m=this,k=m.getChart(),l=k.getInherited().rtl,f=k.isCartesian&&k.getFlipXY(),q=k.getHighlightItem(),g=q.sprite.getMarker("items"),p=g.getMarkerFor(q.sprite.getId(),q.index),b=q.sprite.getSurface(),c=b.getRect(),r=b.getEventXY(i),o=q.sprite.attr.matrix,j=m.getRenderer(),a,n,d,h;if(f){h=l?c[2]-r[0]:r[0]}else{h=c[3]-r[1]}a={x:p.x,y:h,width:p.width,height:p.height+(p.y-h),radius:p.radius,fillStyle:"none",lineDash:[4,4],zIndex:100};Ext.apply(a,m.getStyle());if(Ext.isArray(q.series.getYField())){h=h-p.y-p.height}m.target={index:q.index,yField:q.field,yValue:(h-o.getDY())/o.getYY()};d=[k,{target:m.target,style:a,item:q}];n=Ext.callback(j,null,d,0,k);if(n){Ext.apply(a,n)}q.sprite.putMarker("items",a,"itemedit");m.showTooltip(i,m.target,q);b.renderFrame()},onDragScatter:function(n){var t=this,g=t.getChart(),d=g.getInherited().rtl,l=g.isCartesian&&g.getFlipXY(),o=g.getHighlightItem(),b=o.sprite.getMarker("items"),p=b.getMarkerFor(o.sprite.getId(),o.index),j=o.sprite.getSurface(),h=j.getRect(),a=j.getEventXY(n),k=o.sprite.attr.matrix,c=o.series.getXAxis(),f=c&&c.getLayout().isContinuous,i=t.getRenderer(),m,u,q,s,r;if(l){r=d?h[2]-a[0]:a[0]}else{r=h[3]-a[1]}if(f){if(l){s=h[3]-a[1]}else{s=a[0]}}else{s=p.translationX}m={translationX:s,translationY:r,scalingX:p.scalingX,scalingY:p.scalingY,r:p.r,fillStyle:"none",lineDash:[4,4],zIndex:100};Ext.apply(m,t.getStyle());t.target={index:o.index,yField:o.field,yValue:(r-k.getDY())/k.getYY()};if(f){Ext.apply(t.target,{xField:o.series.getXField(),xValue:(s-k.getDX())/k.getXX()})}q=[g,{target:t.target,style:m,item:o}];u=Ext.callback(i,null,q,0,g);if(u){Ext.apply(m,u)}o.sprite.putMarker("items",m,"itemedit");t.showTooltip(n,t.target,o);j.renderFrame()},showTooltip:function(g,f,c){var d=this.getTooltip(),a,b;if(d&&Ext.toolkit!=="modern"){a=d.config;b=this.getChart();Ext.callback(a.renderer,null,[d,c,f,g],0,b);d.show([g.x+a.offsetX,g.y+a.offsetY])}},hideTooltip:function(){var a=this.getTooltip();if(a&&Ext.toolkit!=="modern"){a.hide()}},onDragEnd:function(g){var d=this,f=d.target,c=d.getChart(),b=c.getStore(),a;if(f){a=b.getAt(f.index);if(f.yField){a.set(f.yField,f.yValue,{convert:false})}if(f.xField){a.set(f.xField,f.xValue,{convert:false})}if(f.yField||f.xField){d.getChart().onDataChanged()}d.target=null}d.hideTooltip();if(d.item){c.fireEvent("enditemedit",c,d,d.item,f)}d.highlight(d.item=null)},destroy:function(){var a=this.getConfig("tooltip",true);Ext.destroy(a);this.callParent()}});Ext.define("Ext.chart.interactions.PanZoom",{extend:"Ext.chart.interactions.Abstract",type:"panzoom",alias:"interaction.panzoom",requires:["Ext.draw.Animator"],config:{axes:{top:{},right:{},bottom:{},left:{}},minZoom:null,maxZoom:null,showOverflowArrows:true,panGesture:"drag",zoomGesture:"pinch",zoomOnPanGesture:false,modeToggleButton:{xtype:"segmentedbutton",width:200,defaults:{ui:"default-toolbar"},cls:Ext.baseCSSPrefix+"panzoom-toggle",items:[{text:"Pan"},{text:"Zoom"}]},hideLabelInGesture:false},stopAnimationBeforeSync:true,applyAxes:function(b,a){return Ext.merge(a||{},b)},applyZoomOnPanGesture:function(a){this.getChart();if(this.isMultiTouch()){return false}return a},updateZoomOnPanGesture:function(b){var a=this.getModeToggleButton();if(!this.isMultiTouch()){a.show();a.setValue(b?1:0)}else{a.hide()}},toggleMode:function(){var a=this;if(!a.isMultiTouch()){a.setZoomOnPanGesture(!a.getZoomOnPanGesture())}},applyModeToggleButton:function(c,b){var d=this,a=Ext.factory(c,"Ext.button.Segmented",b);if(!a&&b){b.destroy()}if(a&&!b){a.addListener("toggle",function(e){d.setZoomOnPanGesture(e.getValue()===1)})}return a},getGestures:function(){var c=this,e={},d=c.getPanGesture(),b=c.getZoomGesture(),a=Ext.supports.Touch;e[b]="onZoomGestureMove";e[b+"start"]="onZoomGestureStart";e[b+"end"]="onZoomGestureEnd";e[d]="onPanGestureMove";e[d+"start"]="onPanGestureStart";e[d+"end"]="onPanGestureEnd";e.doubletap="onDoubleTap";return e},onDoubleTap:function(h){var f=this,c=f.getChart(),g=c.getAxes(),b,a,d;for(a=0,d=g.length;a1){b=1}if(b*j<1){b=1/j}f=o[0];p=o[1];l=l[1]-l[0];if(b===l&&l===1){return}c.setVisibleRange([(o[0]+o[1]-b)*0.5-n/e*b,(o[0]+o[1]+b)*0.5-n/e*b]);return(Math.abs(f-c.getVisibleRange()[0])>1e-10||Math.abs(p-c.getVisibleRange()[1])>1e-10)},destroy:function(){this.setModeToggleButton(null);this.callParent()}});Ext.define("Ext.chart.interactions.Rotate",{extend:"Ext.chart.interactions.Abstract",type:"rotate",alias:"interaction.rotate",config:{gesture:"rotate",gestures:{rotate:"onRotate",rotateend:"onRotate",dragstart:"onGestureStart",drag:"onGesture",dragend:"onGestureEnd"},rotation:0},oldRotations:null,getAngle:function(f){var c=this,b=c.getChart(),d=b.getEventXY(f),a=b.getCenter();return Math.atan2(d[1]-a[1],d[0]-a[0])},getRadius:function(a){return this.getChart().getRadius()},getEventRadius:function(h){var f=this,d=f.getChart(),g=d.getEventXY(h),a=d.getCenter(),c=g[0]-a[0],b=g[1]-a[1];return Math.sqrt(c*c+b*b)},onGestureStart:function(d){var c=this,b=c.getRadius(d),a=c.getEventRadius(d);if(b>=a){c.lockEvents("drag");c.angle=c.getAngle(d);c.oldRotations={};return false}},onGesture:function(b){var a=this,c=a.getAngle(b)-a.angle;if(a.getLocks().drag===a){a.doRotateTo(c,true);return false}},doRotateTo:function(d,a,b){var n=this,l=n.getChart(),k=l.getAxes(),f=l.getSeries(),m=n.oldRotations,c,j,g,e,h;if(!b){l.suspendAnimation()}for(e=0,h=k.length;ea){a=g}}}return a}});Ext.define("Ext.chart.plugin.ItemEvents",{extend:"Ext.plugin.Abstract",alias:"plugin.chartitemevents",moveEvents:false,mouseMoveEvents:{mousemove:true,mouseover:true,mouseout:true},itemMouseMoveEvents:{itemmousemove:true,itemmouseover:true,itemmouseout:true},init:function(b){var a="handleEvent";this.chart=b;b.addElementListener({click:a,dblclick:a,mousedown:a,mousemove:a,mouseup:a,mouseover:a,mouseout:a,priority:1001,scope:this})},hasItemMouseMoveListeners:function(){var b=this.chart.hasListeners,a;for(a in this.itemMouseMoveEvents){if(a in b){return true}}return false},handleEvent:function(g){var d=this,a=d.chart,h=g.type in d.mouseMoveEvents,c=d.lastItem,f,b;if(h&&!d.hasItemMouseMoveListeners()&&!d.moveEvents){return}f=a.getEventXY(g);b=a.getItemForPoint(f[0],f[1]);if(h&&!Ext.Object.equals(b,c)){if(c){a.fireEvent("itemmouseout",a,c,g);c.series.fireEvent("itemmouseout",c.series,c,g)}if(b){a.fireEvent("itemmouseover",a,b,g);b.series.fireEvent("itemmouseover",b.series,b,g)}}if(b){a.fireEvent("item"+g.type,a,b,g);b.series.fireEvent("item"+g.type,b.series,b,g)}d.lastItem=b}});Ext.define("Ext.chart.series.Cartesian",{extend:"Ext.chart.series.Series",config:{xField:null,yField:null,xAxis:null,yAxis:null},directions:["X","Y"],fieldCategoryX:["X"],fieldCategoryY:["Y"],applyXAxis:function(a,b){return this.getChart().getAxis(a)||b},applyYAxis:function(a,b){return this.getChart().getAxis(a)||b},updateXAxis:function(a){a.processData(this)},updateYAxis:function(a){a.processData(this)},coordinateX:function(){return this.coordinate("X",0,2)},coordinateY:function(){return this.coordinate("Y",1,2)},getItemForPoint:function(a,g){if(this.getSprites()){var f=this,d=f.getSprites()[0],b=f.getStore(),e,c;if(f.getHidden()){return null}if(d){c=d.getIndexNearPoint(a,g);if(c!==-1){e={series:f,category:f.getItemInstancing()?"items":"markers",index:c,record:b.getData().items[c],field:f.getYField(),sprite:d};return e}}}},createSprite:function(){var c=this,a=c.callParent(),b=c.getChart(),d=c.getXAxis();a.setAttributes({flipXY:b.getFlipXY(),xAxis:d});if(a.setAggregator&&d&&d.getAggregator){if(d.getAggregator){a.setAggregator({strategy:d.getAggregator()})}else{a.setAggregator({})}}return a},getSprites:function(){var d=this,c=this.getChart(),e=d.getAnimation()||c&&c.getAnimation(),b=d.getItemInstancing(),f=d.sprites,a;if(!c){return[]}if(!f.length){a=d.createSprite()}else{a=f[0]}if(e){if(b){a.itemsMarker.getTemplate().setAnimation(e)}a.setAnimation(e)}return f},provideLegendInfo:function(d){var b=this,a=b.getSubStyleWithTheme(),c=a.fillStyle;if(Ext.isArray(c)){c=c[0]}d.push({name:b.getTitle()||b.getYField()||b.getId(),mark:(Ext.isObject(c)?c.stops&&c.stops[0].color:c)||a.strokeStyle||"black",disabled:b.getHidden(),series:b.getId(),index:0})},getXRange:function(){return[this.dataRange[0],this.dataRange[2]]},getYRange:function(){return[this.dataRange[1],this.dataRange[3]]}});Ext.define("Ext.chart.series.StackedCartesian",{extend:"Ext.chart.series.Cartesian",config:{stacked:true,splitStacks:true,fullStack:false,fullStackTotal:100,hidden:[]},spriteAnimationCount:0,themeColorCount:function(){var b=this,a=b.getYField();return Ext.isArray(a)?a.length:1},updateStacked:function(){this.processData()},updateSplitStacks:function(){this.processData()},coordinateY:function(){return this.coordinateStacked("Y",1,2)},coordinateStacked:function(D,e,m){var F=this,f=F.getStore(),r=f.getData().items,B=r.length,c=F["get"+D+"Axis"](),x=F.getHidden(),a=F.getSplitStacks(),z=F.getFullStack(),l=F.getFullStackTotal(),p={min:0,max:0},n=F["fieldCategory"+D],C=[],o=[],E=[],h,A=F.getStacked(),g=F.getSprites(),q=[],w,v,u,s,H,y,b,d,G,t;if(!g.length){return}for(w=0;w=0||!a){y[v]+=G}else{if(G<0){b[v]+=G}}}}}for(v=0;v=0||!a){if(z&&y[u]){G[u]*=l/y[u]}C[u]=o[u];o[u]+=G[u];h[u]=o[u]}else{if(z&&b[u]){G[u]*=l/b[u]}C[u]=E[u];E[u]+=G[u];h[u]=E[u]}}t["dataStart"+d]=C;t["data"+d]=h;F.getRangeOfData(C,p);F.getRangeOfData(h,p)}else{t["dataStart"+d]=C;t["data"+d]=G;F.getRangeOfData(G,p)}g[v].setAttributes(t)}}F.dataRange[e]=p.min;F.dataRange[e+m]=p.max;t={};t["dataMin"+D]=p.min;t["dataMax"+D]=p.max;for(w=0;w=b[a-1]){return a-1}while(f+1>1,e=b[c];if(e===d){return c}else{if(ea&&ml&&v=b;v--){h=l[v]*e+m;g=C[v]*o+k;s.lineTo(f,g);s.lineTo(f=h,g)}}else{for(v=a;v>=b;v--){h=l[v]*e+m;g=C[v]*o+k;s.lineTo(h,g)}}}else{s.lineTo(l[a]*e+m,g);s.lineTo(l[a]*e+m,k);s.lineTo(z,k);s.lineTo(z,j[v]*o+k)}if(p.transformFillStroke){p.matrix.toContext(s)}s.fill();if(p.transformFillStroke){p.inverseMatrix.toContext(s)}s.beginPath();s.moveTo(z,w);if(p.step){for(v=b;v<=a;v++){h=l[v]*e+m;g=j[v]*o+k;s.lineTo(h,d);s.lineTo(h,d=g);n.translationX=c.x(h,g);n.translationY=c.y(h,g);B.putMarker("markers",n,v,!p.renderer)}}else{for(v=b;v<=a;v++){h=l[v]*e+m;g=j[v]*o+k;s.lineTo(h,g);n.translationX=c.x(h,g);n.translationY=c.y(h,g);B.putMarker("markers",n,v,!p.renderer)}}if(p.transformFillStroke){p.matrix.toContext(s)}s.stroke()}});Ext.define("Ext.chart.series.Area",{extend:"Ext.chart.series.StackedCartesian",alias:"series.area",type:"area",seriesType:"areaSeries",requires:["Ext.chart.series.sprite.Area"],config:{splitStacks:false}});Ext.define("Ext.chart.series.sprite.Bar",{alias:"sprite.barSeries",extend:"Ext.chart.series.sprite.StackedCartesian",inheritableStatics:{def:{processors:{minBarWidth:"number",maxBarWidth:"number",minGapWidth:"number",radius:"number",inGroupGapWidth:"number"},defaults:{minBarWidth:2,maxBarWidth:100,minGapWidth:5,inGroupGapWidth:3,radius:0}}},drawLabel:function(k,i,s,h,o){var q=this,n=q.attr,f=q.getMarker("labels"),d=f.getTemplate(),l=q.labelCfg||(q.labelCfg={}),c=q.surfaceMatrix,j=n.labelOverflowPadding,b=d.attr.display,m=d.attr.orientation,g,e,a,r,t,p;l.x=c.x(i,h);l.y=c.y(i,h);if(!n.flipXY){l.rotationRads=-Math.PI*0.5}else{l.rotationRads=0}l.calloutVertical=!n.flipXY;switch(m){case"horizontal":l.rotationRads=0;l.calloutVertical=false;break;case"vertical":l.rotationRads=-Math.PI*0.5;l.calloutVertical=true;break}l.text=k;if(d.attr.renderer){p=[k,f,l,{store:q.getStore()},o];r=Ext.callback(d.attr.renderer,null,p,0,q.getSeries());if(typeof r==="string"){l.text=r}else{if(typeof r==="object"){if("text" in r){l.text=r.text}t=true}}}a=q.getMarkerBBox("labels",o,true);if(!a){q.putMarker("labels",l,o);a=q.getMarkerBBox("labels",o,true)}e=(a.width/2+j);if(s>h){e=-e}if((m==="horizontal"&&n.flipXY)||(m==="vertical"&&!n.flipXY)||!m){g=(b==="insideStart")?s+e:h-e}else{g=(b==="insideStart")?s+j*2:h-j*2}l.x=c.x(i,g);l.y=c.y(i,g);g=(b==="insideStart")?s-e:h+e;l.calloutPlaceX=c.x(i,g);l.calloutPlaceY=c.y(i,g);g=(b==="insideStart")?s:h;l.calloutStartX=c.x(i,g);l.calloutStartY=c.y(i,g);if(s>h){e=-e}if(Math.abs(h-s)<=e*2||b==="outside"){l.callout=1}else{l.callout=0}if(t){Ext.apply(l,r)}q.putMarker("labels",l,o)},drawBar:function(l,b,d,c,h,k,a,e){var g=this,j={},f=g.attr.renderer,i;j.x=c;j.y=h;j.width=k-c;j.height=a-h;j.radius=g.attr.radius;if(f){i=Ext.callback(f,null,[g,j,{store:g.getStore()},e],0,g.getSeries());Ext.apply(j,i)}g.putMarker("items",j,e,!f)},renderClipped:function(G,u,F,C){if(this.cleanRedraw){return}var q=this,o=q.attr,w=o.dataX,v=o.dataY,H=o.labels,n=o.dataStartY,m=o.groupCount,E=o.groupOffset-(m-1)*0.5,z=o.inGroupGapWidth,t=u.lineWidth,D=o.matrix,B=D.elements[0],j=D.elements[3],e=D.elements[4],d=G.roundPixel(D.elements[5])-1,J=(B<0?-1:1)*B-o.minGapWidth,k=(Math.min(J,o.maxBarWidth)-z*(m-1))/m,A=G.roundPixel(Math.max(o.minBarWidth,k)),c=q.surfaceMatrix,g,I,b,h,K,a,l=0.5*o.lineWidth,L=Math.min(F[0],F[2]),x=Math.max(F[0],F[2]),y=Math.max(0,Math.floor(L)),p=Math.min(w.length-1,Math.ceil(x)),f=H&&q.getMarker("labels"),s,r;for(K=y;K<=p;K++){s=n?n[K]:0;r=v[K];a=w[K]*B+e+E*(A+z);g=G.roundPixel(a-A/2)+l;h=G.roundPixel(r*j+d+t);I=G.roundPixel(a+A/2)-l;b=G.roundPixel(s*j+d+t);q.drawBar(u,G,F,g,h-l,I,b-l,K);if(f&&H[K]!=null){q.drawLabel(H[K],a,b,h,K)}q.putMarker("markers",{translationX:c.x(a,h),translationY:c.y(a,h)},K,true)}},getIndexNearPoint:function(l,k){var m=this,g=m.attr,h=g.dataX,a=m.getSurface(),b=a.getRect()||[0,0,0,0],j=b[3],e,d,c,n,f=-1;if(g.flipXY){e=j-k;if(a.getInherited().rtl){d=b[2]-l}else{d=l}}else{e=l;d=j-k}for(c=0;c0){d.y=g;d.height=a+f}else{d.y=g+f;d.height=a-f}},render:function(l,m){var u=this,k=u.attr,r=k.x,j=k.y,f=j+k.height,i=j=0;b--){if(!c[b]){o=h[b];d=o.getIndexNearPoint(m,k);if(d!==-1){e=j.getYField();p={series:j,index:d,category:a?"items":"markers",record:n.getData().items[d],field:typeof e==="string"?e:e[b],sprite:o};return p}}}return null}}});Ext.define("Ext.draw.LimitedCache",{config:{limit:40,feeder:function(){return 0},scope:null},cache:null,constructor:function(a){this.cache={};this.cache.list=[];this.cache.tail=0;this.initConfig(a)},get:function(e){var c=this.cache,b=this.getLimit(),a=this.getFeeder(),d=this.getScope()||this;if(c[e]){return c[e].value}if(c.list[c.tail]){delete c[c.list[c.tail].cacheId]}c[e]=c.list[c.tail]={value:a.apply(d,Array.prototype.slice.call(arguments,1)),cacheId:e};c.tail++;if(c.tail===b){c.tail=0}return c[e].value},clear:function(){this.cache={};this.cache.list=[];this.cache.tail=0}});Ext.define("Ext.draw.SegmentTree",{config:{strategy:"double"},time:function(m,l,n,c,E,d,e){var f=0,o,A,s=new Date(n[m.startIdx[0]]),x=new Date(n[m.endIdx[l-1]]),D=Ext.Date,u=[[D.MILLI,1,"ms1",null],[D.MILLI,2,"ms2","ms1"],[D.MILLI,5,"ms5","ms1"],[D.MILLI,10,"ms10","ms5"],[D.MILLI,50,"ms50","ms10"],[D.MILLI,100,"ms100","ms50"],[D.MILLI,500,"ms500","ms100"],[D.SECOND,1,"s1","ms500"],[D.SECOND,10,"s10","s1"],[D.SECOND,30,"s30","s10"],[D.MINUTE,1,"mi1","s10"],[D.MINUTE,5,"mi5","mi1"],[D.MINUTE,10,"mi10","mi5"],[D.MINUTE,30,"mi30","mi10"],[D.HOUR,1,"h1","mi30"],[D.HOUR,6,"h6","h1"],[D.HOUR,12,"h12","h6"],[D.DAY,1,"d1","h12"],[D.DAY,7,"d7","d1"],[D.MONTH,1,"mo1","d1"],[D.MONTH,3,"mo3","mo1"],[D.MONTH,6,"mo6","mo3"],[D.YEAR,1,"y1","mo3"],[D.YEAR,5,"y5","y1"],[D.YEAR,10,"y10","y5"],[D.YEAR,100,"y100","y10"]],z,b,k=f,F=l,j=false,r=m.startIdx,h=m.endIdx,w=m.minIdx,C=m.maxIdx,a=m.open,y=m.close,g=m.minX,q=m.minY,p=m.maxX,B=m.maxY,v,t;for(z=0;l>f+1&&zn.length*2*b[1]){continue}if(b[3]&&m.map["time_"+b[3]]){o=m.map["time_"+b[3]][0];A=m.map["time_"+b[3]][1]}else{o=k;A=F}f=l;t=s;j=true;r[l]=r[o];h[l]=h[o];w[l]=w[o];C[l]=C[o];a[l]=a[o];y[l]=y[o];g[l]=g[o];q[l]=q[o];p[l]=p[o];B[l]=B[o];t=Ext.Date.add(t,b[0],b[1]);for(v=o+1;vB[l]){B[l]=B[v];p[l]=p[v];C[l]=C[v]}if(q[v]f){m.map["time_"+b[2]]=[f,l]}}},"double":function(h,u,j,a,t,b,c){var e=0,k,f=1,n,d,v,g,s,l,m,r,q,p,o;while(u>e+1){k=e;e=u;f+=f;for(n=k;n=h.maxY[n+1]){s=h.maxIdx[n];p=h.maxX[n];o=h.maxY[n]}else{s=h.maxIdx[n+1];p=h.maxX[n+1];o=h.maxY[n+1]}}h.startIdx[u]=d;h.endIdx[u]=v;h.minIdx[u]=g;h.maxIdx[u]=s;h.open[u]=l;h.close[u]=m;h.minX[u]=r;h.minY[u]=q;h.maxX[u]=p;h.maxY[u]=o;u++}h.map["double_"+f]=[e,u]}},none:Ext.emptyFn,aggregateData:function(h,a,r,c,d){var b=h.length,e=[],s=[],f=[],q=[],j=[],p=[],n=[],o=[],m=[],k=[],g={startIdx:e,endIdx:s,minIdx:f,maxIdx:q,open:j,minX:p,minY:n,maxX:o,maxY:m,close:k},l;for(l=0;l=b[c.startIdx[a-1]]){return a-1}while(g+1>1,f=b[c.startIdx[d]];if(f===e){return d}else{if(f=b[c.endIdx[a-1]]){return a-1}while(g+1>1,f=b[c.endIdx[d]];if(f===e){return d}else{if(f0){if(e){d.getAggregator().setData(b.dataX,b.dataY,e,a,f)}else{d.getAggregator().setData(b.dataX,b.dataY)}}},getGapWidth:function(){return 1},renderClipped:function(b,c,g,f){var e=this,d=Math.min(g[0],g[2]),a=Math.max(g[0],g[2]),h=e.getAggregator()&&e.getAggregator().getAggregation(d,a,(a-d)/f[2]*e.getGapWidth());if(h){e.dataStart=h.data.startIdx[h.start];e.dataEnd=h.data.endIdx[h.end-1];e.renderAggregates(h.data,h.start,h.end,b,c,g,f)}}});Ext.define("Ext.chart.series.sprite.CandleStick",{alias:"sprite.candlestickSeries",extend:"Ext.chart.series.sprite.Aggregative",inheritableStatics:{def:{processors:{raiseStyle:function(b,a){return Ext.merge({},a||{},b)},dropStyle:function(b,a){return Ext.merge({},a||{},b)},barWidth:"number",padding:"number",ohlcType:"enums(candlestick,ohlc)"},defaults:{raiseStyle:{strokeStyle:"green",fillStyle:"green"},dropStyle:{strokeStyle:"red",fillStyle:"red"},planar:false,barWidth:15,padding:3,lineJoin:"miter",miterLimit:5,ohlcType:"candlestick"},triggers:{raiseStyle:"raiseStyle",dropStyle:"dropStyle"},updaters:{raiseStyle:function(){this.raiseTemplate&&this.raiseTemplate.setAttributes(this.attr.raiseStyle)},dropStyle:function(){this.dropTemplate&&this.dropTemplate.setAttributes(this.attr.dropStyle)}}}},candlestick:function(i,c,a,e,h,f,b){var d=Math.min(c,h),g=Math.max(c,h);i.moveTo(f,e);i.lineTo(f,g);i.moveTo(f+b,g);i.lineTo(f+b,d);i.lineTo(f-b,d);i.lineTo(f-b,g);i.closePath();i.moveTo(f,a);i.lineTo(f,d)},ohlc:function(b,d,e,a,f,c,g){b.moveTo(c,e);b.lineTo(c,a);b.moveTo(c,d);b.lineTo(c-g,d);b.moveTo(c,f);b.lineTo(c+g,f)},constructor:function(){this.callParent(arguments);this.raiseTemplate=new Ext.draw.sprite.Rect({parent:this});this.dropTemplate=new Ext.draw.sprite.Rect({parent:this})},getGapWidth:function(){var a=this.attr,b=a.barWidth,c=a.padding;return b+c},renderAggregates:function(d,c,b,t,u,z){var D=this,s=this.attr,j=s.dataX,v=s.matrix,e=v.getXX(),r=v.getYY(),l=v.getDX(),h=v.getDY(),o=s.barWidth/e,C,k=s.ohlcType,f=Math.round(o*0.5*e),a=d.open,y=d.close,B=d.maxY,p=d.minY,q=d.startIdx,m,g,E,n,A,x,w=s.lineWidth*t.devicePixelRatio/2;w-=Math.floor(w);u.save();C=this.raiseTemplate;C.useAttributes(u,z);u.beginPath();for(x=c;xy[x]){m=Math.round(a[x]*r+h)+w;g=Math.round(B[x]*r+h)+w;E=Math.round(p[x]*r+h)+w;n=Math.round(y[x]*r+h)+w;A=Math.round(j[q[x]]*e+l)+w;D[k](u,m,g,E,n,A,f)}}u.fillStroke(C.attr);u.restore()}});Ext.define("Ext.chart.series.CandleStick",{extend:"Ext.chart.series.Cartesian",requires:["Ext.chart.series.sprite.CandleStick"],alias:"series.candlestick",type:"candlestick",seriesType:"candlestickSeries",config:{openField:null,highField:null,lowField:null,closeField:null},fieldCategoryY:["Open","High","Low","Close"],themeColorCount:function(){return 2}});Ext.define("Ext.chart.series.Polar",{extend:"Ext.chart.series.Series",config:{rotation:0,radius:null,center:[0,0],offsetX:0,offsetY:0,showInLegend:true,xField:null,yField:null,angleField:null,radiusField:null,xAxis:null,yAxis:null},directions:["X","Y"],fieldCategoryX:["X"],fieldCategoryY:["Y"],deprecatedConfigs:{field:"angleField",lengthField:"radiusField"},constructor:function(b){var c=this,a=c.getConfigurator(),e=a.configs,d;if(b){for(d in c.deprecatedConfigs){if(d in b&&!(b in e)){Ext.raise("'"+d+"' config has been deprecated. Please use the '"+c.deprecatedConfigs[d]+"' config instead.")}}}c.callParent([b])},getXField:function(){return this.getAngleField()},updateXField:function(a){this.setAngleField(a)},getYField:function(){return this.getRadiusField()},updateYField:function(a){this.setRadiusField(a)},applyXAxis:function(a,b){return this.getChart().getAxis(a)||b},applyYAxis:function(a,b){return this.getChart().getAxis(a)||b},getXRange:function(){return[this.dataRange[0],this.dataRange[2]]},getYRange:function(){return[this.dataRange[1],this.dataRange[3]]},themeColorCount:function(){var c=this,a=c.getStore(),b=a&&a.getCount()||0;return b},isStoreDependantColorCount:true,getDefaultSpriteConfig:function(){return{type:this.seriesType,renderer:this.getRenderer(),centerX:0,centerY:0,rotationCenterX:0,rotationCenterY:0}},applyRotation:function(a){return Ext.draw.sprite.AttributeParser.angle(a)},updateRotation:function(a){var b=this.getSprites();if(b&&b[0]){b[0].setAttributes({baseRotation:a})}}});Ext.define("Ext.chart.series.Gauge",{alias:"series.gauge",extend:"Ext.chart.series.Polar",type:"gauge",seriesType:"pieslice",requires:["Ext.draw.sprite.Sector"],config:{needle:false,needleLength:90,needleWidth:4,donut:30,showInLegend:false,value:null,colors:null,sectors:null,minimum:0,maximum:100,rotation:0,totalAngle:Math.PI/2,rect:[0,0,1,1],center:[0.5,0.75],radius:0.5,wholeDisk:false},coordinateX:function(){return this.coordinate("X",0,2)},coordinateY:function(){return this.coordinate("Y",1,2)},updateNeedle:function(b){var a=this,d=a.getSprites(),c=a.valueToAngle(a.getValue());if(d&&d.length){d[0].setAttributes({startAngle:(b?c:0),endAngle:c,strokeOpacity:(b?1:0),lineWidth:(b?a.getNeedleWidth():0)});a.doUpdateStyles()}},themeColorCount:function(){var c=this,a=c.getStore(),b=a&&a.getCount()||0;return b+(c.getNeedle()?0:1)},updateColors:function(a,b){var f=this,h=f.getSectors(),j=h&&h.length,e=f.getSprites(),c=Ext.Array.clone(a),g=a&&a.length,d;if(!g||!a[0]){return}for(d=0;d0?f[b-1].end:d.getMinimum()),end:Math.min(e,d.getMaximum())};if(b==(c-1)&&f[b].end0?f[b-1].end:d.getMinimum())}if(typeof e.end==="number"){a=Math.min(e.end,d.getMaximum())}else{a=d.getMaximum()}f[b].start=g;f[b].end=a}}}else{f=[{start:d.getMinimum(),end:d.getMaximum()}]}return f},getSprites:function(){var j=this,m=j.getStore(),l=j.getValue(),c,g;if(!m&&!Ext.isNumber(l)){return[]}var h=j.getChart(),b=j.getAnimation()||h&&h.getAnimation(),f=j.sprites,k=0,o,n,e,d,a=[];if(f&&f.length){f[0].setAnimation(b);return f}d={store:m,field:j.getXField(),angleField:j.getXField(),value:l,series:j};o=j.createSprite();o.setAttributes({zIndex:10},true);o.rendererData=d;o.rendererIndex=k++;a.push(j.getNeedleWidth());j.getLabel().getTemplate().setField(true);n=j.normalizeSectors(j.getSectors());for(c=0,g=n.length;c2&&b.length>2){this.smoothX=Ext.draw.Draw.spline(c);this.smoothY=Ext.draw.Draw.spline(b)}else{delete this.smoothX;delete this.smoothY}}}}},list:null,updatePlainBBox:function(d){var b=this.attr,c=Math.min(0,b.dataMinY),a=Math.max(0,b.dataMaxY);d.x=b.dataMinX;d.y=c;d.width=b.dataMaxX-b.dataMinX;d.height=a-c},drawStrip:function(a,c){a.moveTo(c[0],c[1]);for(var b=2,d=c.length;b0){b++;d+=c>>b}return Math.pow(2,b>0?b-1:b)},drawSmoothStroke:function(u,v,c,b,C,f){var G=this,t=G.attr,d=t.step,z=t.matrix,s=t.renderer,e=z.getXX(),p=z.getYY(),m=z.getDX(),k=z.getDY(),r=G.smoothX,q=G.smoothY,I=G.calculateScale(t.dataX.length,b),o,F,n,E,h,g,B,a,A,w,H,D,l={type:"line",smooth:true,step:d};v.beginPath();v.moveTo(r[c*3]*e+m,q[c*3]*p+k);for(A=0,w=c*3+1;Ap){q.push(p*C+c,n*f+b,U[R]);q.push(O*C+c,M*f+b,U[R])}else{q.push(p*C+c,n*f+b,U[R])}}}if(q.length){for(R=0;Ra){K=a}else{if(K<-a){K=-a}}q[R+1]=K}else{S=false;continue}G=q[R+2];if(t){m.drawMarker(L,K,G)}if(d&&h[G]){m.drawLabel(h[G],L,K,G,D)}}m.isContinuousLine=S;if(g&&!S){Ext.raise("Line smoothing in only supported for gapless data, where all data points are finite numbers.")}if(v){T=v.getAlignment()==="vertical";if(Ext.isNumber(v.floatingAtCoord)){Q=(T?D[2]:D[3])-v.floatingAtCoord}else{Q=T?D[0]:D[1]}}else{Q=k.flipXY?D[0]:D[1]}if(k.preciseStroke){if(k.fillArea){o.fill()}if(k.transformFillStroke){k.inverseMatrix.toContext(o)}m.drawStroke(N,o,w,l,q,Q);if(k.transformFillStroke){k.matrix.toContext(o)}o.stroke()}else{m.drawStroke(N,o,w,l,q,Q);if(S&&g&&k.fillArea&&!k.renderer){var A=s[s.length-1]*C+c+u,z=r[r.length-1]*f+b,J=s[0]*C+c-u,H=r[0]*f+b;o.lineTo(A,z);o.lineTo(A,Q-k.lineWidth);o.lineTo(J,Q-k.lineWidth);o.lineTo(J,H)}if(k.transformFillStroke){k.matrix.toContext(o)}if(k.fillArea){o.fillStroke(k,true)}else{o.stroke(true)}}}}});Ext.define("Ext.chart.series.Line",{extend:"Ext.chart.series.Cartesian",alias:"series.line",type:"line",seriesType:"lineSeries",requires:["Ext.chart.series.sprite.Line"],config:{selectionTolerance:20,smooth:false,step:false,fill:undefined,aggregator:{strategy:"double"}},defaultSmoothness:3,overflowBuffer:1,themeMarkerCount:function(){return 1},getDefaultSpriteConfig:function(){var d=this,e=d.callParent(arguments),c=Ext.apply({},d.getStyle()),b,a=false;if(typeof d.config.fill!="undefined"){if(d.config.fill){a=true;if(typeof c.fillStyle=="undefined"){if(typeof c.strokeStyle=="undefined"){b=d.getStyleWithTheme();c.fillStyle=b.fillStyle;c.strokeStyle=b.strokeStyle}else{c.fillStyle=c.strokeStyle}}}}else{if(c.fillStyle){a=true}}if(!a){delete c.fillStyle}c=Ext.apply(e||{},c);return Ext.apply(c,{fillArea:a,step:d.config.step,smooth:d.config.smooth,selectionTolerance:d.config.selectionTolerance})},updateStep:function(b){var a=this.getSprites()[0];if(a&&a.attr.step!==b){a.setAttributes({step:b})}},updateFill:function(b){var a=this.getSprites()[0];if(a&&a.attr.fillArea!==b){a.setAttributes({fillArea:b})}},updateSmooth:function(a){var b=this.getSprites()[0];if(b&&b.attr.smooth!==a){b.setAttributes({smooth:a})}}});Ext.define("Ext.chart.series.sprite.PieSlice",{extend:"Ext.draw.sprite.Sector",mixins:{markerHolder:"Ext.chart.MarkerHolder"},alias:"sprite.pieslice",inheritableStatics:{def:{processors:{doCallout:"bool",label:"string",rotateLabels:"bool",labelOverflowPadding:"number",renderer:"default"},defaults:{doCallout:true,rotateLabels:true,label:"",labelOverflowPadding:10,renderer:null}}},config:{rendererData:null,rendererIndex:0,series:null},setGradientBBox:function(q,k){var j=this,i=j.attr,g=(i.fillStyle&&i.fillStyle.isGradient)||(i.strokeStyle&&i.strokeStyle.isGradient);if(g&&!i.constrainGradients){var b=j.getMidAngle(),d=i.margin,e=i.centerX,c=i.centerY,a=i.endRho,l=i.matrix,o=l.getScaleX(),n=l.getScaleY(),m=o*a,f=n*a,p={width:m+m,height:f+f};if(d){e+=d*Math.cos(b);c+=d*Math.sin(b)}p.x=l.x(e,c)-m;p.y=l.y(e,c)-f;q.setGradientBBox(p)}else{j.callParent([q,k])}},render:function(b,c,g,f){var e=this,a=e.attr,h={},d;if(a.renderer){h={type:"sector",text:a.text,centerX:a.centerX,centerY:a.centerY,margin:a.margin,startAngle:Math.min(a.startAngle,a.endAngle),endAngle:Math.max(a.startAngle,a.endAngle),startRho:Math.min(a.startRho,a.endRho),endRho:Math.max(a.startRho,a.endRho)};d=Ext.callback(a.renderer,null,[e,h,e.rendererData,e.rendererIndex],0,e.getSeries());e.setAttributes(d);e.useAttributes(c,g)}e.callParent([b,c,g,f]);if(a.label&&e.getMarker("labels")){e.placeLabel()}},placeLabel:function(){var z=this,s=z.attr,r=s.attributeId,t=Math.min(s.startAngle,s.endAngle),p=Math.max(s.startAngle,s.endAngle),k=(t+p)*0.5,n=s.margin,h=s.centerX,g=s.centerY,f=Math.sin(k),c=Math.cos(k),v=Math.min(s.startRho,s.endRho)+n,m=Math.max(s.startRho,s.endRho)+n,l=(v+m)*0.5,b=z.surfaceMatrix,o=z.labelCfg||(z.labelCfg={}),e=z.getMarker("labels"),d=e.getTemplate(),a=d.getCalloutLine(),q=a&&a.length||40,u,j,i,A,w;b.appendMatrix(s.matrix);o.text=s.label;j=h+c*l;i=g+f*l;o.x=b.x(j,i);o.y=b.y(j,i);j=h+c*m;i=g+f*m;o.calloutStartX=b.x(j,i);o.calloutStartY=b.y(j,i);j=h+c*(m+q);i=g+f*(m+q);o.calloutPlaceX=b.x(j,i);o.calloutPlaceY=b.y(j,i);if(!s.rotateLabels){o.rotationRads=0}else{switch(d.attr.orientation){case"horizontal":o.rotationRads=k+Math.atan2(b.y(1,0)-b.y(0,0),b.x(1,0)-b.x(0,0))+Math.PI/2;break;case"vertical":o.rotationRads=k+Math.atan2(b.y(1,0)-b.y(0,0),b.x(1,0)-b.x(0,0));break}}o.calloutColor=(a&&a.color)||z.attr.fillStyle;if(a){if(a.width){o.calloutWidth=a.width}}else{o.calloutHasLine=false}o.globalAlpha=s.globalAlpha*s.fillOpacity;o.hidden=(s.startAngle==s.endAngle);if(d.attr.renderer){w=[z.attr.label,e,o,z.rendererData,z.rendererIndex];A=Ext.callback(d.attr.renderer,null,w,0,z.getSeries());if(typeof A==="string"){o.text=A}else{Ext.apply(o,A)}}z.putMarker("labels",o,r);u=z.getMarkerBBox("labels",r,true);if(u){if(s.doCallout){if(d.attr.display==="outside"){z.putMarker("labels",{callout:1},r)}else{if(d.attr.display==="inside"){z.putMarker("labels",{callout:0},r)}else{z.putMarker("labels",{callout:1-z.sliceContainsLabel(s,u)},r)}}}else{z.putMarker("labels",{globalAlpha:z.sliceContainsLabel(s,u)},r)}}},sliceContainsLabel:function(d,f){var e=d.labelOverflowPadding,h=(d.endRho+d.startRho)/2,g=h+(f.width+e)/2,i=h-(f.width+e)/2,j,c,b,a;if(e<0){return 1}if(f.width+e*2>(d.endRho-d.startRho)){return 0}c=Math.sqrt(d.endRho*d.endRho-g*g);b=Math.sqrt(d.endRho*d.endRho-i*i);j=Math.abs(d.endAngle-d.startAngle);a=(j>Math.PI/2?i:Math.abs(Math.tan(j/2))*i);if(f.height+e*2>Math.min(c,b,a)*2){return 0}return 1}});Ext.define("Ext.chart.series.Pie",{extend:"Ext.chart.series.Polar",requires:["Ext.chart.series.sprite.PieSlice"],type:"pie",alias:"series.pie",seriesType:"pieslice",config:{donut:0,rotation:0,clockwise:true,totalAngle:2*Math.PI,hidden:[],radiusFactor:100,highlightCfg:{margin:20},style:{}},directions:["X"],applyLabel:function(a,b){if(Ext.isObject(a)&&!Ext.isString(a.orientation)){Ext.apply(a=Ext.Object.chain(a),{orientation:"vertical"})}return this.callParent([a,b])},updateLabelData:function(){var h=this,j=h.getStore(),g=j.getData().items,e=h.getSprites(),a=h.getLabel().getTemplate().getField(),d=h.getHidden(),b,f,c,k;if(e.length&&a){c=[];for(b=0,f=g.length;bs){s=k}}d[p]=a;if(p>=o.length){o[p]=false}}o.length=c;t.maxY=s;if(a!==0){m=h/a}for(p=0;p=a){return{series:h,sprite:f[b],index:b,record:g[b],field:h.getXField()}}}}}return null},getItemForPoint:function(f,e){var t=this,c=t.getSprites();if(c){var s=t.getCenter(),q=t.getOffsetX(),p=t.getOffsetY(),j=f-s[0]+q,h=e-s[1]+p,b=t.getStore(),g=t.getDonut(),o=b.getData().items,r=Math.atan2(h,j)-t.getRotation(),a=Math.sqrt(j*j+h*h),l=t.getRadius()*g*0.01,m=t.getHidden(),n,d,k;for(n=0,d=o.length;n=l+k.margin&&a<=k.endRho+k.margin){if(t.betweenAngle(r,k.startAngle,k.endAngle)){return{series:t,sprite:c[n],index:n,record:o[n],field:t.getXField()}}}}}return null}},provideLegendInfo:function(f){var h=this,j=h.getStore();if(j){var g=j.getData().items,b=h.getLabel().getTemplate().getField(),c=h.getXField(),e=h.getHidden(),d,a,k;for(d=0;d=0&&b<0){f=Math.sin(d)}else{if(d<=0&&b>0){f=Math.sin(b)}else{if(d>=0&&b>0){if(d>b){f=0}else{f=Math.max(Math.sin(d),Math.sin(b))}}else{f=1}}}a.zIndex=4+f;break;case"outerBack":a.zIndex=1;break;case"start":a.zIndex=4+Math.sin(c(d+e));break;case"end":a.zIndex=4+Math.sin(c(b+e));break;case"innerFront":a.zIndex=2;break;case"innerBack":a.zIndex=4+Math.sin(c((d+b)/2+e));break;case"bottom":a.zIndex=0;break}a.dirtyZIndex=true},updatePlainBBox:function(k){var f=this.attr,a=f.part,b=f.baseRotation,e=f.centerX,d=f.centerY,j,c,i,h,g,l;if(a==="start"){c=f.startAngle+b}else{if(a==="end"){c=f.endAngle+b}}if(Ext.isNumber(c)){g=Math.sin(c);l=Math.cos(c);i=Math.min(e+l*f.startRho,e+l*f.endRho);h=d+g*f.startRho*f.distortion;k.x=i;k.y=h;k.width=l*(f.endRho-f.startRho);k.height=f.thickness+g*(f.endRho-f.startRho)*2;return}if(a==="innerFront"||a==="innerBack"){j=f.startRho}else{j=f.endRho}k.width=j*2;k.height=j*f.distortion*2+f.thickness;k.x=f.centerX-j;k.y=f.centerY-j*f.distortion},updateTransformedBBox:function(a){if(this.attr.part==="start"||this.attr.part==="end"){return this.callParent(arguments)}return this.updatePlainBBox(a)},updatePath:function(a){if(!this.attr.globalAlpha){return}if(this.attr.endAngle0||c,i;if(n){i=(p+m)/2;g+=Math.cos(i)*k;f+=Math.sin(i)*k*e;l.moveTo(g+d*q,f+b*q*e);l.lineTo(g+d*j,f+b*j*e);l.lineTo(g+d*j,f+b*j*e+a);l.lineTo(g+d*q,f+b*q*e+a);l.closePath()}},startRenderer:function(a){this.sideRenderer(a,"start")},endRenderer:function(a){this.sideRenderer(a,"end")},rimRenderer:function(q,e,o,j){var w=this,s=w.attr,p=s.margin,h=s.centerX,g=s.centerY,d=s.distortion,i=s.baseRotation,t=Ext.draw.sprite.AttributeParser.angle,u=s.startAngle+i,r=s.endAngle+i,k=t((u+r)/2),a=s.thickness,b=s.globalAlpha<1,c,n,v;w.bevelParams=[];u=t(u);r=t(r);h+=Math.cos(k)*p;g+=Math.sin(k)*p*d;c=u>=0&&r>=0;n=u<=0&&r<=0;function l(){q.ellipse(h,g+a,e,e*d,0,Math.PI,u,true);q.lineTo(h+Math.cos(u)*e,g+Math.sin(u)*e*d);v=[h,g,e,e*d,0,u,Math.PI,false];if(!o){w.bevelParams.push(v)}q.ellipse.apply(q,v);q.closePath()}function f(){q.ellipse(h,g+a,e,e*d,0,0,r,false);q.lineTo(h+Math.cos(r)*e,g+Math.sin(r)*e*d);v=[h,g,e,e*d,0,r,0,true];if(!o){w.bevelParams.push(v)}q.ellipse.apply(q,v);q.closePath()}function x(){q.ellipse(h,g+a,e,e*d,0,Math.PI,r,false);q.lineTo(h+Math.cos(r)*e,g+Math.sin(r)*e*d);v=[h,g,e,e*d,0,r,Math.PI,true];if(o){w.bevelParams.push(v)}q.ellipse.apply(q,v);q.closePath()}function m(){q.ellipse(h,g+a,e,e*d,0,u,0,false);q.lineTo(h+e,g);v=[h,g,e,e*d,0,0,u,true];if(o){w.bevelParams.push(v)}q.ellipse.apply(q,v);q.closePath()}if(j){if(!o||b){if(u>=0&&r<0){l()}else{if(u<=0&&r>0){f()}else{if(u<=0&&r<0){if(u>r){q.ellipse(h,g+a,e,e*d,0,0,Math.PI,false);q.lineTo(h-e,g);v=[h,g,e,e*d,0,Math.PI,0,true];if(!o){w.bevelParams.push(v)}q.ellipse.apply(q,v);q.closePath()}}else{if(u>r){l();f()}else{v=[h,g,e,e*d,0,u,r,false];if(c&&!o||n&&o){w.bevelParams.push(v)}q.ellipse.apply(q,v);q.lineTo(h+Math.cos(r)*e,g+Math.sin(r)*e*d+a);q.ellipse(h,g+a,e,e*d,0,r,u,true);q.closePath()}}}}}}else{if(o||b){if(u>=0&&r<0){x()}else{if(u<=0&&r>0){m()}else{if(u<=0&&r<0){if(u>r){x();m()}else{q.ellipse(h,g+a,e,e*d,0,u,r,false);q.lineTo(h+Math.cos(r)*e,g+Math.sin(r)*e*d);v=[h,g,e,e*d,0,r,u,true];if(o){w.bevelParams.push(v)}q.ellipse.apply(q,v);q.closePath()}}else{if(u>r){q.ellipse(h,g+a,e,e*d,0,-Math.PI,0,false);q.lineTo(h+e,g);v=[h,g,e,e*d,0,0,-Math.PI,true];if(o){w.bevelParams.push(v)}q.ellipse.apply(q,v);q.closePath()}}}}}}},innerFrontRenderer:function(a){this.rimRenderer(a,this.attr.startRho,true,true)},innerBackRenderer:function(a){this.rimRenderer(a,this.attr.startRho,true,false)},outerFrontRenderer:function(a){this.rimRenderer(a,this.attr.endRho,false,true)},outerBackRenderer:function(a){this.rimRenderer(a,this.attr.endRho,false,false)}});Ext.define("Ext.draw.PathUtil",function(){var a=Math.abs,c=Math.pow,e=Math.cos,b=Math.acos,d=Math.sqrt,f=Math.PI;return{singleton:true,requires:["Ext.draw.overrides.Path","Ext.draw.overrides.sprite.Path","Ext.draw.overrides.sprite.Instancing","Ext.draw.overrides.Surface"],cubicRoots:function(m){var z=m[0],x=m[1],w=m[2],v=m[3];if(z===0){return this.quadraticRoots(x,w,v)}var s=x/z,r=w/z,q=v/z,k=(3*r-c(s,2))/9,j=(9*s*r-27*q-2*c(s,3))/54,p=c(k,3)+c(j,2),n=[],h,g,o,l,u,y=Ext.Number.sign;if(p>=0){h=y(j+d(p))*c(a(j+d(p)),1/3);g=y(j-d(p))*c(a(j-d(p)),1/3);n[0]=-s/3+(h+g);n[1]=-s/3-(h+g)/2;n[2]=n[1];o=a(d(3)*(h-g)/2);if(o!==0){n[1]=-1;n[2]=-1}}else{l=b(j/d(-c(k,3)));n[0]=2*d(-k)*e(l/3)-s/3;n[1]=2*d(-k)*e((l+2*f)/3)-s/3;n[2]=2*d(-k)*e((l+4*f)/3)-s/3}for(u=0;u<3;u++){if(n[u]<0||n[u]>1){n[u]=-1}}return n},quadraticRoots:function(h,g,n){var m,l,k,j;if(h===0){return this.linearRoot(g,n)}m=g*g-4*h*n;if(m===0){k=[-g/(2*h)]}else{if(m>0){l=d(m);k=[(-g-l)/(2*h),(-g+l)/(2*h)]}else{return[]}}for(j=0;j1){k[j]=-1}}return k},linearRoot:function(h,g){var i=-g/h;if(h===0||i<0||i>1){return[]}return[i]},bezierCoeffs:function(h,g,k,j){var i=[];i[0]=-h+3*g-3*k+j;i[1]=3*h-6*g+3*k;i[2]=-3*h+3*g;i[3]=h;return i},cubicLineIntersections:function(I,G,F,E,l,k,j,h,M,p,K,n){var u=[],N=[],D=p-n,z=K-M,y=M*(n-p)-p*(K-M),L=this.bezierCoeffs(I,G,F,E),J=this.bezierCoeffs(l,k,j,h),H,x,w,v,g,q,o,m;u[0]=D*L[0]+z*J[0];u[1]=D*L[1]+z*J[1];u[2]=D*L[2]+z*J[2];u[3]=D*L[3]+z*J[3]+y;x=this.cubicRoots(u);for(H=0;H1){continue}g=v*v;q=g*v;o=L[0]*q+L[1]*g+L[2]*v+L[3];m=J[0]*q+J[1]*g+J[2]*v+J[3];if((K-M)!==0){w=(o-M)/(K-M)}else{w=(m-p)/(n-p)}if(!(w<0||w>1)){N.push([o,m])}}return N},splitCubic:function(g,q,p,o,m){var j=m*m,n=m*j,i=m-1,h=i*i,k=i*h,l=n*o-3*j*i*p+3*m*h*q-k*g;return[[g,m*q-i*g,j*p-2*m*i*q+h*g,l],[l,j*o-2*m*i*p+h*q,m*o-i*p,o]]},cubicDimension:function(p,o,l,k){var j=3*(-p+3*(o-l)+k),i=6*(p-2*o+l),h=-3*(p-o),q,n,g=Math.min(p,k),m=Math.max(p,k),r;if(j===0){if(i===0){return[g,m]}else{q=-h/i;if(0=0){r=d(r);q=(r-i)/2/j;if(00){q-=r/j;if(0n[1]||x[1]s[1]||B[1]=0&&i<=1&&g>=0&&g<=1){return[k+i*(j-k),p+i*(o-p)]}return null},pointOnLine:function(j,m,h,l,g,n){var k,i;if(a(h-j)1){return false}return a(m+k*(l-m)-n)<4},pointOnCubic:function(w,u,s,r,l,k,h,g,p,o){var C=this,B=C.bezierCoeffs(w,u,s,r),A=C.bezierCoeffs(l,k,h,g),z,v,n,m,q;B[3]-=p;A[3]-=o;n=C.cubicRoots(B);m=C.cubicRoots(A);for(z=0;z=0&&q<=1&&a(q-m[v])<0.05){return true}}}return false}}});Ext.define("Ext.chart.series.Pie3D",{extend:"Ext.chart.series.Polar",requires:["Ext.chart.series.sprite.Pie3DPart","Ext.draw.PathUtil"],type:"pie3d",seriesType:"pie3d",alias:"series.pie3d",isPie3D:true,config:{rect:[0,0,0,0],thickness:35,distortion:0.5,donut:false,hidden:[],highlightCfg:{margin:20},shadow:false},rotationOffset:-Math.PI/2,setField:function(a){return this.setXField(a)},getField:function(){return this.getXField()},updateRotation:function(a){this.setStyle({baseRotation:a+this.rotationOffset});this.doUpdateStyles()},updateDistortion:function(){this.setRadius()},updateThickness:function(){this.setRadius()},updateColors:function(a){this.setSubStyle({baseColor:a})},applyShadow:function(a){if(a===true){a={shadowColor:"rgba(0,0,0,0.8)",shadowBlur:30}}else{if(!Ext.isObject(a)){a={shadowColor:Ext.draw.Color.RGBA_NONE}}}return a},updateShadow:function(g){var e=this,f=e.getSprites(),d=e.spritesPerSlice,c=f&&f.length,b,a;for(b=1;b=s.length){s[r]=false}}s.length=d;if(c===0){return}h=2*Math.PI/c;for(r=0;ra/2){return a/(f.getDistortion()*2)}else{return g}},getSprites:function(){var y=this,e=y.getStore();if(!e){return[]}var n=y.getChart(),p=y.getSurface(),t=e.getData().items,l=y.spritesPerSlice,a=t.length,v=y.getAnimation()||n&&n.getAnimation(),x=y.getCenter(),w=y.getOffsetX(),u=y.getOffsetY(),b=y.getRadius(),q=y.getRotation(),d=y.getHighlight(),c={centerX:x[0]+w,centerY:x[1]+u-y.getThickness()/2,endRho:b,startRho:b*y.getDonut()/100,thickness:y.getThickness(),distortion:y.getDistortion()},k=y.sprites,h=y.getLabel(),f=h.getTemplate(),m,g,o,s,r;for(s=0;s=0;c--){a=b.get(c);d=a.hitTestEvent(f);if(d){return d}}return null},handleEvent:function(f){var d=this,b=d.drawContainer,g=f.type in d.mouseMoveEvents,a=d.lastSprite,c;if(g&&!d.hasSpriteMouseMoveListeners()){return}c=d.hitTestEvent(f);if(g&&!Ext.Object.equals(c,a)){if(a){b.fireEvent("spritemouseout",a,f)}if(c){b.fireEvent("spritemouseover",c,f)}}if(c){b.fireEvent("sprite"+f.type,c,f)}d.lastSprite=c}});Ext.define("Ext.chart.TipSurface",{extend:"Ext.draw.Container",spriteArray:false,renderFirst:true,constructor:function(a){this.callParent([a]);if(a.sprites){this.spriteArray=[].concat(a.sprites);delete a.sprites}},onRender:function(){var c=this,b=0,a=0,d,e;this.callParent(arguments);e=c.spriteArray;if(c.renderFirst&&e){c.renderFirst=false;for(a=e.length;b Proxmox.Utils.bond_mode_gettext_map[value] || value || '', + + bond_mode_array: function(modes) { + return modes.map(mode => [mode, Proxmox.Utils.render_bond_mode(mode)]); + }, + + getNoSubKeyHtml: function(url) { + // url http://www.proxmox.com/products/proxmox-ve/subscription-service-plans + return Ext.String.format('You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.', url || 'https://www.proxmox.com'); + }, + + format_boolean_with_default: function(value) { + if (Ext.isDefined(value) && value !== '__default__') { + return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText; + } + return Proxmox.Utils.defaultText; + }, + + format_boolean: function(value) { + return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText; + }, + + format_neg_boolean: function(value) { + return !value ? Proxmox.Utils.yesText : Proxmox.Utils.noText; + }, + + format_enabled_toggle: function(value) { + return value ? Proxmox.Utils.enabledText : Proxmox.Utils.disabledText; + }, + + format_expire: function(date) { + if (!date) { + return Proxmox.Utils.neverText; + } + return Ext.Date.format(date, "Y-m-d"); + }, + + format_duration_long: function(ut) { + + var days = Math.floor(ut / 86400); + ut -= days*86400; + var hours = Math.floor(ut / 3600); + ut -= hours*3600; + var mins = Math.floor(ut / 60); + ut -= mins*60; + + var hours_str = '00' + hours.toString(); + hours_str = hours_str.substr(hours_str.length - 2); + var mins_str = "00" + mins.toString(); + mins_str = mins_str.substr(mins_str.length - 2); + var ut_str = "00" + ut.toString(); + ut_str = ut_str.substr(ut_str.length - 2); + + if (days) { + var ds = days > 1 ? Proxmox.Utils.daysText : Proxmox.Utils.dayText; + return days.toString() + ' ' + ds + ' ' + + hours_str + ':' + mins_str + ':' + ut_str; + } else { + return hours_str + ':' + mins_str + ':' + ut_str; + } + }, + + format_subscription_level: function(level) { + if (level === 'c') { + return 'Community'; + } else if (level === 'b') { + return 'Basic'; + } else if (level === 's') { + return 'Standard'; + } else if (level === 'p') { + return 'Premium'; + } else { + return Proxmox.Utils.noneText; + } + }, + + compute_min_label_width: function(text, width) { + + if (width === undefined) { width = 100; } + + var tm = new Ext.util.TextMetrics(); + var min = tm.getWidth(text + ':'); + + return min < width ? width : min; + }, + + setAuthData: function(data) { + Proxmox.CSRFPreventionToken = data.CSRFPreventionToken; + Proxmox.UserName = data.username; + Proxmox.LoggedOut = data.LoggedOut; + // creates a session cookie (expire = null) + // that way the cookie gets deleted after the browser window is closed + Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true); + }, + + authOK: function() { + if (Proxmox.LoggedOut) { + return undefined; + } + return (Proxmox.UserName !== '') && Ext.util.Cookies.get(Proxmox.Setup.auth_cookie_name); + }, + + authClear: function() { + if (Proxmox.LoggedOut) { + return undefined; + } + Ext.util.Cookies.clear(Proxmox.Setup.auth_cookie_name); + }, + + // comp.setLoading() is buggy in ExtJS 4.0.7, so we + // use el.mask() instead + setErrorMask: function(comp, msg) { + var el = comp.el; + if (!el) { + return; + } + if (!msg) { + el.unmask(); + } else { + if (msg === true) { + el.mask(gettext("Loading...")); + } else { + el.mask(msg); + } + } + }, + + monStoreErrors: function(me, store, clearMaskBeforeLoad) { + if (clearMaskBeforeLoad) { + me.mon(store, 'beforeload', function(s, operation, eOpts) { + Proxmox.Utils.setErrorMask(me, false); + }); + } else { + me.mon(store, 'beforeload', function(s, operation, eOpts) { + if (!me.loadCount) { + me.loadCount = 0; // make sure it is numeric + Proxmox.Utils.setErrorMask(me, true); + } + }); + } + + // only works with 'proxmox' proxy + me.mon(store.proxy, 'afterload', function(proxy, request, success) { + me.loadCount++; + + if (success) { + Proxmox.Utils.setErrorMask(me, false); + return; + } + + var msg; + /*jslint nomen: true */ + var operation = request._operation; + var error = operation.getError(); + if (error.statusText) { + msg = error.statusText + ' (' + error.status + ')'; + } else { + msg = gettext('Connection error'); + } + Proxmox.Utils.setErrorMask(me, msg); + }); + }, + + extractRequestError: function(result, verbose) { + var msg = gettext('Successful'); + + if (!result.success) { + msg = gettext("Unknown error"); + if (result.message) { + msg = result.message; + if (result.status) { + msg += ' (' + result.status + ')'; + } + } + if (verbose && Ext.isObject(result.errors)) { + msg += "
"; + Ext.Object.each(result.errors, function(prop, desc) { + msg += "
" + Ext.htmlEncode(prop) + ": " + + Ext.htmlEncode(desc); + }); + } + } + + return msg; + }, + + // Ext.Ajax.request + API2Request: function(reqOpts) { + + var newopts = Ext.apply({ + waitMsg: gettext('Please wait...') + }, reqOpts); + + if (!newopts.url.match(/^\/api2/)) { + newopts.url = '/api2/extjs' + newopts.url; + } + delete newopts.callback; + + var createWrapper = function(successFn, callbackFn, failureFn) { + Ext.apply(newopts, { + success: function(response, options) { + if (options.waitMsgTarget) { + if (Proxmox.Utils.toolkit === 'touch') { + options.waitMsgTarget.setMasked(false); + } else { + options.waitMsgTarget.setLoading(false); + } + } + var result = Ext.decode(response.responseText); + response.result = result; + if (!result.success) { + response.htmlStatus = Proxmox.Utils.extractRequestError(result, true); + Ext.callback(callbackFn, options.scope, [options, false, response]); + Ext.callback(failureFn, options.scope, [response, options]); + return; + } + Ext.callback(callbackFn, options.scope, [options, true, response]); + Ext.callback(successFn, options.scope, [response, options]); + }, + failure: function(response, options) { + if (options.waitMsgTarget) { + if (Proxmox.Utils.toolkit === 'touch') { + options.waitMsgTarget.setMasked(false); + } else { + options.waitMsgTarget.setLoading(false); + } + } + response.result = {}; + try { + response.result = Ext.decode(response.responseText); + } catch(e) {} + var msg = gettext('Connection error') + ' - server offline?'; + if (response.aborted) { + msg = gettext('Connection error') + ' - aborted.'; + } else if (response.timedout) { + msg = gettext('Connection error') + ' - Timeout.'; + } else if (response.status && response.statusText) { + msg = gettext('Connection error') + ' ' + response.status + ': ' + response.statusText; + } + response.htmlStatus = msg; + Ext.callback(callbackFn, options.scope, [options, false, response]); + Ext.callback(failureFn, options.scope, [response, options]); + } + }); + }; + + createWrapper(reqOpts.success, reqOpts.callback, reqOpts.failure); + + var target = newopts.waitMsgTarget; + if (target) { + if (Proxmox.Utils.toolkit === 'touch') { + target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg} ); + } else { + // Note: ExtJS bug - this does not work when component is not rendered + target.setLoading(newopts.waitMsg); + } + } + Ext.Ajax.request(newopts); + }, + + checked_command: function(orig_cmd) { + Proxmox.Utils.API2Request({ + url: '/nodes/localhost/subscription', + method: 'GET', + //waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + var data = response.result.data; + + if (data.status !== 'Active') { + Ext.Msg.show({ + title: gettext('No valid subscription'), + icon: Ext.Msg.WARNING, + msg: Proxmox.Utils.getNoSubKeyHtml(data.url), + buttons: Ext.Msg.OK, + callback: function(btn) { + if (btn !== 'ok') { + return; + } + orig_cmd(); + } + }); + } else { + orig_cmd(); + } + } + }); + }, + + assemble_field_data: function(values, data) { + if (Ext.isObject(data)) { + Ext.Object.each(data, function(name, val) { + if (values.hasOwnProperty(name)) { + var bucket = values[name]; + if (!Ext.isArray(bucket)) { + bucket = values[name] = [bucket]; + } + if (Ext.isArray(val)) { + values[name] = bucket.concat(val); + } else { + bucket.push(val); + } + } else { + values[name] = val; + } + }); + } + }, + + dialog_title: function(subject, create, isAdd) { + if (create) { + if (isAdd) { + return gettext('Add') + ': ' + subject; + } else { + return gettext('Create') + ': ' + subject; + } + } else { + return gettext('Edit') + ': ' + subject; + } + }, + + network_iface_types: { + eth: gettext("Network Device"), + bridge: 'Linux Bridge', + bond: 'Linux Bond', + vlan: 'Linux VLAN', + OVSBridge: 'OVS Bridge', + OVSBond: 'OVS Bond', + OVSPort: 'OVS Port', + OVSIntPort: 'OVS IntPort' + }, + + render_network_iface_type: function(value) { + return Proxmox.Utils.network_iface_types[value] || + Proxmox.Utils.unknownText; + }, + + task_desc_table: { + acmenewcert: [ 'SRV', gettext('Order Certificate') ], + acmeregister: [ 'ACME Account', gettext('Register') ], + acmedeactivate: [ 'ACME Account', gettext('Deactivate') ], + acmeupdate: [ 'ACME Account', gettext('Update') ], + acmerefresh: [ 'ACME Account', gettext('Refresh') ], + acmerenew: [ 'SRV', gettext('Renew Certificate') ], + acmerevoke: [ 'SRV', gettext('Revoke Certificate') ], + 'move_volume': [ 'CT', gettext('Move Volume') ], + clustercreate: [ '', gettext('Create Cluster') ], + clusterjoin: [ '', gettext('Join Cluster') ], + diskinit: [ 'Disk', gettext('Initialize Disk with GPT') ], + vncproxy: [ 'VM/CT', gettext('Console') ], + spiceproxy: [ 'VM/CT', gettext('Console') + ' (Spice)' ], + vncshell: [ '', gettext('Shell') ], + spiceshell: [ '', gettext('Shell') + ' (Spice)' ], + qmsnapshot: [ 'VM', gettext('Snapshot') ], + qmrollback: [ 'VM', gettext('Rollback') ], + qmdelsnapshot: [ 'VM', gettext('Delete Snapshot') ], + qmcreate: [ 'VM', gettext('Create') ], + qmrestore: [ 'VM', gettext('Restore') ], + qmdestroy: [ 'VM', gettext('Destroy') ], + qmigrate: [ 'VM', gettext('Migrate') ], + qmclone: [ 'VM', gettext('Clone') ], + qmmove: [ 'VM', gettext('Move disk') ], + qmtemplate: [ 'VM', gettext('Convert to template') ], + qmstart: [ 'VM', gettext('Start') ], + qmstop: [ 'VM', gettext('Stop') ], + qmreset: [ 'VM', gettext('Reset') ], + qmshutdown: [ 'VM', gettext('Shutdown') ], + qmsuspend: [ 'VM', gettext('Hibernate') ], + qmpause: [ 'VM', gettext('Pause') ], + qmresume: [ 'VM', gettext('Resume') ], + qmconfig: [ 'VM', gettext('Configure') ], + vzsnapshot: [ 'CT', gettext('Snapshot') ], + vzrollback: [ 'CT', gettext('Rollback') ], + vzdelsnapshot: [ 'CT', gettext('Delete Snapshot') ], + vzcreate: ['CT', gettext('Create') ], + vzrestore: ['CT', gettext('Restore') ], + vzdestroy: ['CT', gettext('Destroy') ], + vzmigrate: [ 'CT', gettext('Migrate') ], + vzclone: [ 'CT', gettext('Clone') ], + vztemplate: [ 'CT', gettext('Convert to template') ], + vzstart: ['CT', gettext('Start') ], + vzstop: ['CT', gettext('Stop') ], + vzmount: ['CT', gettext('Mount') ], + vzumount: ['CT', gettext('Unmount') ], + vzshutdown: ['CT', gettext('Shutdown') ], + vzsuspend: [ 'CT', gettext('Suspend') ], + vzresume: [ 'CT', gettext('Resume') ], + hamigrate: [ 'HA', gettext('Migrate') ], + hastart: [ 'HA', gettext('Start') ], + hastop: [ 'HA', gettext('Stop') ], + srvstart: ['SRV', gettext('Start') ], + srvstop: ['SRV', gettext('Stop') ], + srvrestart: ['SRV', gettext('Restart') ], + srvreload: ['SRV', gettext('Reload') ], + cephcreatemgr: ['Ceph Manager', gettext('Create') ], + cephdestroymgr: ['Ceph Manager', gettext('Destroy') ], + cephcreatemon: ['Ceph Monitor', gettext('Create') ], + cephdestroymon: ['Ceph Monitor', gettext('Destroy') ], + cephcreateosd: ['Ceph OSD', gettext('Create') ], + cephdestroyosd: ['Ceph OSD', gettext('Destroy') ], + cephcreatepool: ['Ceph Pool', gettext('Create') ], + cephdestroypool: ['Ceph Pool', gettext('Destroy') ], + cephfscreate: ['CephFS', gettext('Create') ], + cephcreatemds: ['Ceph Metadata Server', gettext('Create') ], + cephdestroymds: ['Ceph Metadata Server', gettext('Destroy') ], + imgcopy: ['', gettext('Copy data') ], + imgdel: ['', gettext('Erase data') ], + unknownimgdel: ['', gettext('Destroy image from unknown guest') ], + download: ['', gettext('Download') ], + vzdump: ['VM/CT', gettext('Backup') ], + aptupdate: ['', gettext('Update package database') ], + startall: [ '', gettext('Start all VMs and Containers') ], + stopall: [ '', gettext('Stop all VMs and Containers') ], + migrateall: [ '', gettext('Migrate all VMs and Containers') ], + dircreate: [ gettext('Directory Storage'), gettext('Create') ], + lvmcreate: [ gettext('LVM Storage'), gettext('Create') ], + lvmthincreate: [ gettext('LVM-Thin Storage'), gettext('Create') ], + zfscreate: [ gettext('ZFS Storage'), gettext('Create') ] + }, + + format_task_description: function(type, id) { + var farray = Proxmox.Utils.task_desc_table[type]; + var text; + if (!farray) { + text = type; + if (id) { + type += ' ' + id; + } + return text; + } + var prefix = farray[0]; + text = farray[1]; + if (prefix) { + return prefix + ' ' + id + ' - ' + text; + } + return text; + }, + + format_size: function(size) { + /*jslint confusion: true */ + + var units = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']; + var num = 0; + + while (size >= 1024 && ((num++)+1) < units.length) { + size = size / 1024; + } + + return size.toFixed((num > 0)?2:0) + " " + units[num] + "B"; + }, + + render_upid: function(value, metaData, record) { + var type = record.data.type; + var id = record.data.id; + + return Proxmox.Utils.format_task_description(type, id); + }, + + render_uptime: function(value) { + + var uptime = value; + + if (uptime === undefined) { + return ''; + } + + if (uptime <= 0) { + return '-'; + } + + return Proxmox.Utils.format_duration_long(uptime); + }, + + parse_task_upid: function(upid) { + var task = {}; + + var res = upid.match(/^UPID:(\S+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/); + if (!res) { + throw "unable to parse upid '" + upid + "'"; + } + task.node = res[1]; + task.pid = parseInt(res[2], 16); + task.pstart = parseInt(res[3], 16); + task.starttime = parseInt(res[4], 16); + task.type = res[5]; + task.id = res[6]; + task.user = res[7]; + + task.desc = Proxmox.Utils.format_task_description(task.type, task.id); + + return task; + }, + + render_timestamp: function(value, metaData, record, rowIndex, colIndex, store) { + var servertime = new Date(value * 1000); + return Ext.Date.format(servertime, 'Y-m-d H:i:s'); + }, + + get_help_info: function(section) { + var helpMap; + if (typeof proxmoxOnlineHelpInfo !== 'undefined') { + helpMap = proxmoxOnlineHelpInfo; + } else if (typeof pveOnlineHelpInfo !== 'undefined') { + // be backward compatible with older pve-doc-generators + helpMap = pveOnlineHelpInfo; + } else { + throw "no global OnlineHelpInfo map declared"; + } + + return helpMap[section]; + }, + + get_help_link: function(section) { + var info = Proxmox.Utils.get_help_info(section); + if (!info) { + return; + } + + return window.location.origin + info.link; + }, + + openXtermJsViewer: function(vmtype, vmid, nodename, vmname, cmd) { + var url = Ext.Object.toQueryString({ + console: vmtype, // kvm, lxc, upgrade or shell + xtermjs: 1, + vmid: vmid, + vmname: vmname, + node: nodename, + cmd: cmd, + + }); + var nw = window.open("?" + url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,resizable=yes,width=800,height=420'); + if (nw) { + nw.focus(); + } + } + +}, + + singleton: true, + constructor: function() { + var me = this; + Ext.apply(me, me.utilities); + + var IPV4_OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])"; + var IPV4_REGEXP = "(?:(?:" + IPV4_OCTET + "\\.){3}" + IPV4_OCTET + ")"; + var IPV6_H16 = "(?:[0-9a-fA-F]{1,4})"; + var IPV6_LS32 = "(?:(?:" + IPV6_H16 + ":" + IPV6_H16 + ")|" + IPV4_REGEXP + ")"; + var IPV4_CIDR_MASK = "([0-9]{1,2})"; + var IPV6_CIDR_MASK = "([0-9]{1,3})"; + + + me.IP4_match = new RegExp("^(?:" + IPV4_REGEXP + ")$"); + me.IP4_cidr_match = new RegExp("^(?:" + IPV4_REGEXP + ")\/" + IPV4_CIDR_MASK + "$"); + + var IPV6_REGEXP = "(?:" + + "(?:(?:" + "(?:" + IPV6_H16 + ":){6})" + IPV6_LS32 + ")|" + + "(?:(?:" + "::" + "(?:" + IPV6_H16 + ":){5})" + IPV6_LS32 + ")|" + + "(?:(?:(?:" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){4})" + IPV6_LS32 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,1}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){3})" + IPV6_LS32 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,2}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){2})" + IPV6_LS32 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,3}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){1})" + IPV6_LS32 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,4}" + IPV6_H16 + ")?::" + ")" + IPV6_LS32 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,5}" + IPV6_H16 + ")?::" + ")" + IPV6_H16 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,7}" + IPV6_H16 + ")?::" + ")" + ")" + + ")"; + + me.IP6_match = new RegExp("^(?:" + IPV6_REGEXP + ")$"); + me.IP6_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + ")\/" + IPV6_CIDR_MASK + "$"); + me.IP6_bracket_match = new RegExp("^\\[(" + IPV6_REGEXP + ")\\]"); + + me.IP64_match = new RegExp("^(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + ")$"); + me.IP64_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + "\/" + IPV6_CIDR_MASK + ")|(?:" + IPV4_REGEXP + "\/" + IPV4_CIDR_MASK + ")$"); + + var DnsName_REGEXP = "(?:(([a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)\\.)*([A-Za-z0-9]([A-Za-z0-9\\-]*[A-Za-z0-9])?))"; + me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$"); + + me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(:\\d+)?$"); + me.HostPortBrackets_match = new RegExp("^\\[(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](:\\d+)?$"); + me.IP6_dotnotation_match = new RegExp("^" + IPV6_REGEXP + "(\\.\\d+)?$"); + } +}); +// ExtJS related things + + // do not send '_dc' parameter +Ext.Ajax.disableCaching = false; + +// custom Vtypes +Ext.apply(Ext.form.field.VTypes, { + IPAddress: function(v) { + return Proxmox.Utils.IP4_match.test(v); + }, + IPAddressText: gettext('Example') + ': 192.168.1.1', + IPAddressMask: /[\d\.]/i, + + IPCIDRAddress: function(v) { + var result = Proxmox.Utils.IP4_cidr_match.exec(v); + // limits according to JSON Schema see + // pve-common/src/PVE/JSONSchema.pm + return (result !== null && result[1] >= 8 && result[1] <= 32); + }, + IPCIDRAddressText: gettext('Example') + ': 192.168.1.1/24' + "
" + gettext('Valid CIDR Range') + ': 8-32', + IPCIDRAddressMask: /[\d\.\/]/i, + + IP6Address: function(v) { + return Proxmox.Utils.IP6_match.test(v); + }, + IP6AddressText: gettext('Example') + ': 2001:DB8::42', + IP6AddressMask: /[A-Fa-f0-9:]/, + + IP6CIDRAddress: function(v) { + var result = Proxmox.Utils.IP6_cidr_match.exec(v); + // limits according to JSON Schema see + // pve-common/src/PVE/JSONSchema.pm + return (result !== null && result[1] >= 8 && result[1] <= 128); + }, + IP6CIDRAddressText: gettext('Example') + ': 2001:DB8::42/64' + "
" + gettext('Valid CIDR Range') + ': 8-128', + IP6CIDRAddressMask: /[A-Fa-f0-9:\/]/, + + IP6PrefixLength: function(v) { + return v >= 0 && v <= 128; + }, + IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128', + IP6PrefixLengthMask: /[0-9]/, + + IP64Address: function(v) { + return Proxmox.Utils.IP64_match.test(v); + }, + IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42', + IP64AddressMask: /[A-Fa-f0-9\.:]/, + + IP64CIDRAddress: function(v) { + var result = Proxmox.Utils.IP64_cidr_match.exec(v); + if (result === null) { + return false; + } + if (result[1] !== undefined) { + return result[1] >= 8 && result[1] <= 128; + } else if (result[2] !== undefined) { + return result[2] >= 8 && result[2] <= 32; + } else { + return false; + } + }, + IP64CIDRAddressText: gettext('Example') + ': 192.168.1.1/24 2001:DB8::42/64', + IP64CIDRAddressMask: /[A-Fa-f0-9\.:\/]/, + + MacAddress: function(v) { + return (/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/).test(v); + }, + MacAddressMask: /[a-fA-F0-9:]/, + MacAddressText: gettext('Example') + ': 01:23:45:67:89:ab', + + MacPrefix: function(v) { + return (/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i).test(v); + }, + MacPrefixMask: /[a-fA-F0-9:]/, + MacPrefixText: gettext('Example') + ': 02:8f - ' + gettext('only unicast addresses are allowed'), + + BridgeName: function(v) { + return (/^vmbr\d{1,4}$/).test(v); + }, + BridgeNameText: gettext('Format') + ': vmbrN, where 0 <= N <= 9999', + + BondName: function(v) { + return (/^bond\d{1,4}$/).test(v); + }, + BondNameText: gettext('Format') + ': bondN, where 0 <= N <= 9999', + + InterfaceName: function(v) { + return (/^[a-z][a-z0-9_]{1,20}$/).test(v); + }, + InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'" + "
" + + gettext("Minimum characters") + ": 2" + "
" + + gettext("Maximum characters") + ": 21" + "
" + + gettext("Must start with") + ": 'a-z'", + + StorageId: function(v) { + return (/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i).test(v); + }, + StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'" + "
" + + gettext("Minimum characters") + ": 2" + "
" + + gettext("Must start with") + ": 'A-Z', 'a-z'
" + + gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'
", + + ConfigId: function(v) { + return (/^[a-z][a-z0-9\_]+$/i).test(v); + }, + ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'" + "
" + + gettext("Minimum characters") + ": 2" + "
" + + gettext("Must start with") + ": " + gettext("letter"), + + HttpProxy: function(v) { + return (/^http:\/\/.*$/).test(v); + }, + HttpProxyText: gettext('Example') + ": http://username:password@host:port/", + + DnsName: function(v) { + return Proxmox.Utils.DnsName_match.test(v); + }, + DnsNameText: gettext('This is not a valid DNS name'), + + // workaround for https://www.sencha.com/forum/showthread.php?302150 + proxmoxMail: function(v) { + return (/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/).test(v); + }, + proxmoxMailText: gettext('Example') + ": user@example.com", + + DnsOrIp: function(v) { + if (!Proxmox.Utils.DnsName_match.test(v) && + !Proxmox.Utils.IP64_match.test(v)) { + return false; + } + + return true; + }, + DnsOrIpText: gettext('Not a valid DNS name or IP address.'), + + HostList: function(v) { + var list = v.split(/[\ \,\;]+/); + var i; + for (i = 0; i < list.length; i++) { + if (list[i] == "") { + continue; + } + + if (!Proxmox.Utils.HostPort_match.test(list[i]) && + !Proxmox.Utils.HostPortBrackets_match.test(list[i]) && + !Proxmox.Utils.IP6_dotnotation_match.test(list[i])) { + return false; + } + } + + return true; + }, + HostListText: gettext('Not a valid list of hosts'), + + password: function(val, field) { + if (field.initialPassField) { + var pwd = field.up('form').down( + '[name=' + field.initialPassField + ']'); + return (val == pwd.getValue()); + } + return true; + }, + + passwordText: gettext('Passwords do not match') +}); + +// Firefox 52+ Touchscreen bug +// see https://www.sencha.com/forum/showthread.php?336762-Examples-don-t-work-in-Firefox-52-touchscreen/page2 +// and https://bugzilla.proxmox.com/show_bug.cgi?id=1223 +Ext.define('EXTJS_23846.Element', { + override: 'Ext.dom.Element' +}, function(Element) { + var supports = Ext.supports, + proto = Element.prototype, + eventMap = proto.eventMap, + additiveEvents = proto.additiveEvents; + + if (Ext.os.is.Desktop && supports.TouchEvents && !supports.PointerEvents) { + eventMap.touchstart = 'mousedown'; + eventMap.touchmove = 'mousemove'; + eventMap.touchend = 'mouseup'; + eventMap.touchcancel = 'mouseup'; + + additiveEvents.mousedown = 'mousedown'; + additiveEvents.mousemove = 'mousemove'; + additiveEvents.mouseup = 'mouseup'; + additiveEvents.touchstart = 'touchstart'; + additiveEvents.touchmove = 'touchmove'; + additiveEvents.touchend = 'touchend'; + additiveEvents.touchcancel = 'touchcancel'; + + additiveEvents.pointerdown = 'mousedown'; + additiveEvents.pointermove = 'mousemove'; + additiveEvents.pointerup = 'mouseup'; + additiveEvents.pointercancel = 'mouseup'; + } +}); + +Ext.define('EXTJS_23846.Gesture', { + override: 'Ext.event.publisher.Gesture' +}, function(Gesture) { + var me = Gesture.instance; + + if (Ext.supports.TouchEvents && !Ext.isWebKit && Ext.os.is.Desktop) { + me.handledDomEvents.push('mousedown', 'mousemove', 'mouseup'); + me.registerEvents(); + } +}); + +Ext.define('EXTJS_18900.Pie', { + override: 'Ext.chart.series.Pie', + + // from 6.0.2 + betweenAngle: function (x, a, b) { + var pp = Math.PI * 2, + offset = this.rotationOffset; + + if (a === b) { + return false; + } + + if (!this.getClockwise()) { + x *= -1; + a *= -1; + b *= -1; + a -= offset; + b -= offset; + } else { + a += offset; + b += offset; + } + + x -= a; + b -= a; + + // Normalize, so that both x and b are in the [0,360) interval. + x %= pp; + b %= pp; + x += pp; + b += pp; + x %= pp; + b %= pp; + + // Because 360 * n angles will be normalized to 0, + // we need to treat b === 0 as a special case. + return x < b || b === 0; + }, +}); + +// we always want the number in x.y format and never in, e.g., x,y +Ext.define('PVE.form.field.Number', { + override: 'Ext.form.field.Number', + submitLocaleSeparator: false +}); + +// ExtJs 5-6 has an issue with caching +// see https://www.sencha.com/forum/showthread.php?308989 +Ext.define('Proxmox.UnderlayPool', { + override: 'Ext.dom.UnderlayPool', + + checkOut: function () { + var cache = this.cache, + len = cache.length, + el; + + // do cleanup because some of the objects might have been destroyed + while (len--) { + if (cache[len].destroyed) { + cache.splice(len, 1); + } + } + // end do cleanup + + el = cache.shift(); + + if (!el) { + el = Ext.Element.create(this.elementConfig); + el.setVisibilityMode(2); + // + // tell the spec runner to ignore this element when checking if the dom is clean + el.dom.setAttribute('data-sticky', true); + // + } + + return el; + } +}); + +// 'Enter' in Textareas and aria multiline fields should not activate the +// defaultbutton, fixed in extjs 6.0.2 +Ext.define('PVE.panel.Panel', { + override: 'Ext.panel.Panel', + + fireDefaultButton: function(e) { + if (e.target.getAttribute('aria-multiline') === 'true' || + e.target.tagName === "TEXTAREA") { + return true; + } + return this.callParent(arguments); + } +}); + +// if the order of the values are not the same in originalValue and value +// extjs will not overwrite value, but marks the field dirty and thus +// the reset button will be enabled (but clicking it changes nothing) +// so if the arrays are not the same after resetting, we +// clear and set it +Ext.define('Proxmox.form.ComboBox', { + override: 'Ext.form.field.ComboBox', + + reset: function() { + // copied from combobox + var me = this; + me.callParent(); + + // clear and set when not the same + var value = me.getValue(); + if (Ext.isArray(me.originalValue) && Ext.isArray(value) && !Ext.Array.equals(value, me.originalValue)) { + me.clearValue(); + me.setValue(me.originalValue); + } + } +}); + +// when refreshing a grid/tree view, restoring the focus moves the view back to +// the previously focused item. Save scroll position before refocusing. +Ext.define(null, { + override: 'Ext.view.Table', + + jumpToFocus: false, + + saveFocusState: function() { + var me = this, + store = me.dataSource, + actionableMode = me.actionableMode, + navModel = me.getNavigationModel(), + focusPosition = actionableMode ? me.actionPosition : navModel.getPosition(true), + refocusRow, refocusCol; + + if (focusPosition) { + // Separate this from the instance that the nav model is using. + focusPosition = focusPosition.clone(); + + // Exit actionable mode. + // We must inform any Actionables that they must relinquish control. + // Tabbability must be reset. + if (actionableMode) { + me.ownerGrid.setActionableMode(false); + } + + // Blur the focused descendant, but do not trigger focusLeave. + me.el.dom.focus(); + + // Exiting actionable mode navigates to the owning cell, so in either focus mode we must + // clear the navigation position + navModel.setPosition(); + + // The following function will attempt to refocus back in the same mode to the same cell + // as it was at before based upon the previous record (if it's still inthe store), or the row index. + return function() { + // If we still have data, attempt to refocus in the same mode. + if (store.getCount()) { + + // Adjust expectations of where we are able to refocus according to what kind of destruction + // might have been wrought on this view's DOM during focus save. + refocusRow = Math.min(focusPosition.rowIdx, me.all.getCount() - 1); + refocusCol = Math.min(focusPosition.colIdx, me.getVisibleColumnManager().getColumns().length - 1); + focusPosition = new Ext.grid.CellContext(me).setPosition( + store.contains(focusPosition.record) ? focusPosition.record : refocusRow, refocusCol); + + if (actionableMode) { + me.ownerGrid.setActionableMode(true, focusPosition); + } else { + me.cellFocused = true; + + // we sometimes want to scroll back to where we were + var x = me.getScrollX(); + var y = me.getScrollY(); + + // Pass "preventNavigation" as true so that that does not cause selection. + navModel.setPosition(focusPosition, null, null, null, true); + + if (!me.jumpToFocus) { + me.scrollTo(x,y); + } + } + } + // No rows - focus associated column header + else { + focusPosition.column.focus(); + } + }; + } + return Ext.emptyFn; + } +}); + +// should be fixed with ExtJS 6.0.2, see: +// https://www.sencha.com/forum/showthread.php?307244-Bug-with-datefield-in-window-with-scroll +Ext.define('Proxmox.Datepicker', { + override: 'Ext.picker.Date', + hideMode: 'visibility' +}); + +// ExtJS 6.0.1 has no setSubmitValue() (although you find it in the docs). +// Note: this.submitValue is a boolean flag, whereas getSubmitValue() returns +// data to be submitted. +Ext.define('Proxmox.form.field.Text', { + override: 'Ext.form.field.Text', + + setSubmitValue: function(v) { + this.submitValue = v; + }, +}); + +// this should be fixed with ExtJS 6.0.2 +// make mousescrolling work in firefox in the containers overflowhandler +Ext.define(null, { + override: 'Ext.layout.container.boxOverflow.Scroller', + + createWheelListener: function() { + var me = this; + if (Ext.isFirefox) { + me.wheelListener = me.layout.innerCt.on('wheel', me.onMouseWheelFirefox, me, {destroyable: true}); + } else { + me.wheelListener = me.layout.innerCt.on('mousewheel', me.onMouseWheel, me, {destroyable: true}); + } + }, + + // special wheel handler for firefox. differs from the default onMouseWheel + // handler by using deltaY instead of wheelDeltaY and no normalizing, + // because it is already + onMouseWheelFirefox: function(e) { + e.stopEvent(); + var delta = e.browserEvent.deltaY || 0; + this.scrollBy(delta * this.wheelIncrement, false); + } + +}); + +// add '@' to the valid id +Ext.define('Proxmox.validIdReOverride', { + override: 'Ext.Component', + validIdRe: /^[a-z_][a-z0-9\-_\@]*$/i, +}); + +// force alert boxes to be rendered with an Error Icon +// since Ext.Msg is an object and not a prototype, we need to override it +// after the framework has been initiated +Ext.onReady(function() { +/*jslint confusion: true */ + Ext.override(Ext.Msg, { + alert: function(title, message, fn, scope) { + if (Ext.isString(title)) { + var config = { + title: title, + message: message, + icon: this.ERROR, + buttons: this.OK, + fn: fn, + scope : scope, + minWidth: this.minWidth + }; + return this.show(config); + } + } + }); +/*jslint confusion: false */ +}); +Ext.define('Ext.ux.IFrame', { + extend: 'Ext.Component', + + alias: 'widget.uxiframe', + + loadMask: 'Loading...', + + src: 'about:blank', + + renderTpl: [ + '' + ], + childEls: ['iframeEl'], + + initComponent: function () { + this.callParent(); + + this.frameName = this.frameName || this.id + '-frame'; + }, + + initEvents : function() { + var me = this; + me.callParent(); + me.iframeEl.on('load', me.onLoad, me); + }, + + initRenderData: function() { + return Ext.apply(this.callParent(), { + src: this.src, + frameName: this.frameName + }); + }, + + getBody: function() { + var doc = this.getDoc(); + return doc.body || doc.documentElement; + }, + + getDoc: function() { + try { + return this.getWin().document; + } catch (ex) { + return null; + } + }, + + getWin: function() { + var me = this, + name = me.frameName, + win = Ext.isIE + ? me.iframeEl.dom.contentWindow + : window.frames[name]; + return win; + }, + + getFrame: function() { + var me = this; + return me.iframeEl.dom; + }, + + beforeDestroy: function () { + this.cleanupListeners(true); + this.callParent(); + }, + + cleanupListeners: function(destroying){ + var doc, prop; + + if (this.rendered) { + try { + doc = this.getDoc(); + if (doc) { + /*jslint nomen: true*/ + Ext.get(doc).un(this._docListeners); + /*jslint nomen: false*/ + if (destroying && doc.hasOwnProperty) { + for (prop in doc) { + if (doc.hasOwnProperty(prop)) { + delete doc[prop]; + } + } + } + } + } catch(e) { } + } + }, + + onLoad: function() { + var me = this, + doc = me.getDoc(), + fn = me.onRelayedEvent; + + if (doc) { + try { + // These events need to be relayed from the inner document (where they stop + // bubbling) up to the outer document. This has to be done at the DOM level so + // the event reaches listeners on elements like the document body. The effected + // mechanisms that depend on this bubbling behavior are listed to the right + // of the event. + /*jslint nomen: true*/ + Ext.get(doc).on( + me._docListeners = { + mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront) + mousemove: fn, // window resize drag detection + mouseup: fn, // window resize termination + click: fn, // not sure, but just to be safe + dblclick: fn, // not sure again + scope: me + } + ); + /*jslint nomen: false*/ + } catch(e) { + // cannot do this xss + } + + // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK! + Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me); + + this.el.unmask(); + this.fireEvent('load', this); + + } else if (me.src) { + + this.el.unmask(); + this.fireEvent('error', this); + } + + + }, + + onRelayedEvent: function (event) { + // relay event from the iframe's document to the document that owns the iframe... + + var iframeEl = this.iframeEl, + + // Get the left-based iframe position + iframeXY = iframeEl.getTrueXY(), + originalEventXY = event.getXY(), + + // Get the left-based XY position. + // This is because the consumer of the injected event will + // perform its own RTL normalization. + eventXY = event.getTrueXY(); + + // the event from the inner document has XY relative to that document's origin, + // so adjust it to use the origin of the iframe in the outer document: + event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]]; + + event.injectEvent(iframeEl); // blame the iframe for the event... + + event.xy = originalEventXY; // restore the original XY (just for safety) + }, + + load: function (src) { + var me = this, + text = me.loadMask, + frame = me.getFrame(); + + if (me.fireEvent('beforeload', me, src) !== false) { + if (text && me.el) { + me.el.mask(text); + } + + frame.src = me.src = (src || me.src); + } + } +}); +Ext.define('Proxmox.Mixin.CBind', { + extend: 'Ext.Mixin', + + mixinConfig: { + before: { + initComponent: 'cloneTemplates' + } + }, + + cloneTemplates: function() { + var me = this; + + if (typeof(me.cbindData) == "function") { + me.cbindData = me.cbindData(me.initialConfig) || {}; + } + + var getConfigValue = function(cname) { + + if (cname in me.initialConfig) { + return me.initialConfig[cname]; + } + if (cname in me.cbindData) { + return me.cbindData[cname]; + } + if (cname in me) { + return me[cname]; + } + throw "unable to get cbind data for '" + cname + "'"; + }; + + var applyCBind = function(obj) { + var cbind = obj.cbind, prop, cdata, cvalue, match, found; + if (!cbind) return; + + for (prop in cbind) { + cdata = cbind[prop]; + + found = false; + if (match = /^\{(!)?([a-z_][a-z0-9_]*)\}$/i.exec(cdata)) { + var cvalue = getConfigValue(match[2]); + if (match[1]) cvalue = !cvalue; + obj[prop] = cvalue; + found = true; + } else if (match = /^\{(!)?([a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)+)\}$/i.exec(cdata)) { + var keys = match[2].split('.'); + var cvalue = getConfigValue(keys.shift()); + keys.forEach(function(k) { + if (k in cvalue) { + cvalue = cvalue[k]; + } else { + throw "unable to get cbind data for '" + match[2] + "'"; + } + }); + if (match[1]) cvalue = !cvalue; + obj[prop] = cvalue; + found = true; + } else { + obj[prop] = cdata.replace(/{([a-z_][a-z0-9_]*)\}/ig, function(match, cname) { + var cvalue = getConfigValue(cname); + found = true; + return cvalue; + }); + } + if (!found) { + throw "unable to parse cbind template '" + cdata + "'"; + } + + } + }; + + if (me.cbind) { + applyCBind(me); + } + + var cloneTemplateArray = function(org) { + var copy, i, found, el, elcopy, arrayLength; + + arrayLength = org.length; + found = false; + for (i = 0; i < arrayLength; i++) { + el = org[i]; + if (el.constructor == Object && el.xtype) { + found = true; + break; + } + } + + if (!found) return org; // no need to copy + + copy = []; + for (i = 0; i < arrayLength; i++) { + el = org[i]; + if (el.constructor == Object && el.xtype) { + elcopy = cloneTemplateObject(el); + if (elcopy.cbind) { + applyCBind(elcopy); + } + copy.push(elcopy); + } else if (el.constructor == Array) { + elcopy = cloneTemplateArray(el); + copy.push(elcopy); + } else { + copy.push(el); + } + } + return copy; + }; + + var cloneTemplateObject = function(org) { + var res = {}, prop, el, copy; + for (prop in org) { + el = org[prop]; + if (el.constructor == Object && el.xtype) { + copy = cloneTemplateObject(el); + if (copy.cbind) { + applyCBind(copy); + } + res[prop] = copy; + } else if (el.constructor == Array) { + copy = cloneTemplateArray(el); + res[prop] = copy; + } else { + res[prop] = el; + } + } + return res; + }; + + var condCloneProperties = function() { + var prop, el, i, tmp; + + for (prop in me) { + el = me[prop]; + if (el === undefined || el === null) continue; + if (typeof(el) === 'object' && el.constructor == Object) { + if (el.xtype && prop != 'config') { + me[prop] = cloneTemplateObject(el); + } + } else if (el.constructor == Array) { + tmp = cloneTemplateArray(el); + me[prop] = tmp; + } + } + }; + + condCloneProperties(); + } +}); +/* A reader to store a single JSON Object (hash) into a storage. + * Also accepts an array containing a single hash. + * + * So it can read: + * + * example1: {data1: "xyz", data2: "abc"} + * returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}] + * + * example2: [ {data1: "xyz", data2: "abc"} ] + * returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}] + * + * If you set 'readArray', the reader expexts the object as array: + * + * example3: [ { key: "data1", value: "xyz", p2: "cde" }, { key: "data2", value: "abc", p2: "efg" }] + * returns [{key: "data1", value: "xyz", p2: "cde}, {key: "data2", value: "abc", p2: "efg"}] + * + * Note: The records can contain additional properties (like 'p2' above) when you use 'readArray' + * + * Additional feature: specify allowed properties with default values with 'rows' object + * + * var rows = { + * memory: { + * required: true, + * defaultValue: 512 + * } + * } + * + */ + +Ext.define('Proxmox.data.reader.JsonObject', { + extend: 'Ext.data.reader.Json', + alias : 'reader.jsonobject', + + readArray: false, + + rows: undefined, + + constructor: function(config) { + var me = this; + + Ext.apply(me, config || {}); + + me.callParent([config]); + }, + + getResponseData: function(response) { + var me = this; + + var data = []; + try { + var result = Ext.decode(response.responseText); + // get our data items inside the server response + var root = result[me.getRootProperty()]; + + if (me.readArray) { + + var rec_hash = {}; + Ext.Array.each(root, function(rec) { + if (Ext.isDefined(rec.key)) { + rec_hash[rec.key] = rec; + } + }); + + if (me.rows) { + Ext.Object.each(me.rows, function(key, rowdef) { + var rec = rec_hash[key]; + if (Ext.isDefined(rec)) { + if (!Ext.isDefined(rec.value)) { + rec.value = rowdef.defaultValue; + } + data.push(rec); + } else if (Ext.isDefined(rowdef.defaultValue)) { + data.push({key: key, value: rowdef.defaultValue} ); + } else if (rowdef.required) { + data.push({key: key, value: undefined }); + } + }); + } else { + Ext.Array.each(root, function(rec) { + if (Ext.isDefined(rec.key)) { + data.push(rec); + } + }); + } + + } else { + + var org_root = root; + + if (Ext.isArray(org_root)) { + if (root.length == 1) { + root = org_root[0]; + } else { + root = {}; + } + } + + if (me.rows) { + Ext.Object.each(me.rows, function(key, rowdef) { + if (Ext.isDefined(root[key])) { + data.push({key: key, value: root[key]}); + } else if (Ext.isDefined(rowdef.defaultValue)) { + data.push({key: key, value: rowdef.defaultValue}); + } else if (rowdef.required) { + data.push({key: key, value: undefined}); + } + }); + } else { + Ext.Object.each(root, function(key, value) { + data.push({key: key, value: value }); + }); + } + } + } + catch (ex) { + Ext.Error.raise({ + response: response, + json: response.responseText, + parseError: ex, + msg: 'Unable to parse the JSON returned by the server: ' + ex.toString() + }); + } + + return data; + } +}); + +Ext.define('Proxmox.RestProxy', { + extend: 'Ext.data.RestProxy', + alias : 'proxy.proxmox', + + pageParam : null, + startParam: null, + limitParam: null, + groupParam: null, + sortParam: null, + filterParam: null, + noCache : false, + + afterRequest: function(request, success) { + this.fireEvent('afterload', this, request, success); + return; + }, + + constructor: function(config) { + + Ext.applyIf(config, { + reader: { + type: 'json', + rootProperty: config.root || 'data' + } + }); + + this.callParent([config]); + } +}, function() { + + Ext.define('KeyValue', { + extend: "Ext.data.Model", + fields: [ 'key', 'value' ], + idProperty: 'key' + }); + + Ext.define('KeyValuePendingDelete', { + extend: "Ext.data.Model", + fields: [ 'key', 'value', 'pending', 'delete' ], + idProperty: 'key' + }); + + Ext.define('proxmox-tasks', { + extend: 'Ext.data.Model', + fields: [ + { name: 'starttime', type : 'date', dateFormat: 'timestamp' }, + { name: 'endtime', type : 'date', dateFormat: 'timestamp' }, + { name: 'pid', type: 'int' }, + 'node', 'upid', 'user', 'status', 'type', 'id' + ], + idProperty: 'upid' + }); + + Ext.define('proxmox-cluster-log', { + extend: 'Ext.data.Model', + fields: [ + { name: 'uid' , type: 'int' }, + { name: 'time', type : 'date', dateFormat: 'timestamp' }, + { name: 'pri', type: 'int' }, + { name: 'pid', type: 'int' }, + 'node', 'user', 'tag', 'msg', + { + name: 'id', + convert: function(value, record) { + var info = record.data; + var text; + + if (value) { + return value; + } + // compute unique ID + return info.uid + ':' + info.node; + } + } + ], + idProperty: 'id' + }); + +}); +/* Extends the Ext.data.Store type + * with startUpdate() and stopUpdate() methods + * to refresh the store data in the background + * Components using this store directly will flicker + * due to the redisplay of the element ater 'config.interval' ms + * + * Note that you have to call yourself startUpdate() for the background load + * to begin + */ +Ext.define('Proxmox.data.UpdateStore', { + extend: 'Ext.data.Store', + alias: 'store.update', + + isStopped: true, + + autoStart: false, + + destroy: function() { + var me = this; + me.stopUpdate(); + me.callParent(); + }, + + constructor: function(config) { + var me = this; + + config = config || {}; + + if (!config.interval) { + config.interval = 3000; + } + + if (!config.storeid) { + throw "no storeid specified"; + } + + var load_task = new Ext.util.DelayedTask(); + + var run_load_task = function() { + if (me.isStopped) { + return; + } + + if (Proxmox.Utils.authOK()) { + var start = new Date(); + me.load(function() { + var runtime = (new Date()) - start; + var interval = config.interval + runtime*2; + load_task.delay(interval, run_load_task); + }); + } else { + load_task.delay(200, run_load_task); + } + }; + + Ext.apply(config, { + startUpdate: function() { + me.isStopped = false; + // run_load_task(); this makes problems with chrome + load_task.delay(1, run_load_task); + }, + stopUpdate: function() { + me.isStopped = true; + load_task.cancel(); + } + }); + + me.callParent([config]); + + me.load_task = load_task; + + if (me.autoStart) { + me.startUpdate(); + } + } +}); +/* + * The DiffStore is a in-memory store acting as proxy between a real store + * instance and a component. + * Its purpose is to redisplay the component *only* if the data has been changed + * inside the real store, to avoid the annoying visual flickering of using + * the real store directly. + * + * Implementation: + * The DiffStore monitors via mon() the 'load' events sent by the real store. + * On each 'load' event, the DiffStore compares its own content with the target + * store (call to cond_add_item()) and then fires a 'refresh' event. + * The 'refresh' event will automatically trigger a view refresh on the component + * who binds to this store. + */ + +/* Config properties: + * rstore: the realstore which will autorefresh its content from the API + * Only works if rstore has a model and use 'idProperty' + * sortAfterUpdate: sort the diffstore before rendering the view + */ +Ext.define('Proxmox.data.DiffStore', { + extend: 'Ext.data.Store', + alias: 'store.diff', + + sortAfterUpdate: false, + + constructor: function(config) { + var me = this; + + config = config || {}; + + if (!config.rstore) { + throw "no rstore specified"; + } + + if (!config.rstore.model) { + throw "no rstore model specified"; + } + + var rstore = config.rstore; + + Ext.apply(config, { + model: rstore.model, + proxy: { type: 'memory' } + }); + + me.callParent([config]); + + var first_load = true; + + var cond_add_item = function(data, id) { + var olditem = me.getById(id); + if (olditem) { + olditem.beginEdit(); + Ext.Array.each(me.model.prototype.fields, function(field) { + if (olditem.data[field.name] !== data[field.name]) { + olditem.set(field.name, data[field.name]); + } + }); + olditem.endEdit(true); + olditem.commit(); + } else { + var newrec = Ext.create(me.model, data); + var pos = (me.appendAtStart && !first_load) ? 0 : me.data.length; + me.insert(pos, newrec); + } + }; + + var loadFn = function(s, records, success) { + + if (!success) { + return; + } + + me.suspendEvents(); + + // getSource returns null if data is not filtered + // if it is filtered it returns all records + var allItems = me.getData().getSource() || me.getData(); + + // remove vanished items + allItems.each(function(olditem) { + var item = rstore.getById(olditem.getId()); + if (!item) { + me.remove(olditem); + } + }); + + rstore.each(function(item) { + cond_add_item(item.data, item.getId()); + }); + + me.filter(); + + if (me.sortAfterUpdate) { + me.sort(); + } + + first_load = false; + + me.resumeEvents(); + me.fireEvent('refresh', me); + me.fireEvent('datachanged', me); + }; + + if (rstore.isLoaded()) { + // if store is already loaded, + // insert items instantly + loadFn(rstore, [], true); + } + + me.mon(rstore, 'load', loadFn); + } +}); +/* This store encapsulates data items which are organized as an Array of key-values Objects + * ie data[0] contains something like {key: "keyboard", value: "da"} +* +* Designed to work with the KeyValue model and the JsonObject data reader +*/ +Ext.define('Proxmox.data.ObjectStore', { + extend: 'Proxmox.data.UpdateStore', + + getRecord: function() { + var me = this; + var record = Ext.create('Ext.data.Model'); + me.getData().each(function(item) { + record.set(item.data.key, item.data.value); + }); + record.commit(true); + return record; + }, + + constructor: function(config) { + var me = this; + + config = config || {}; + + if (!config.storeid) { + config.storeid = 'proxmox-store-' + (++Ext.idSeed); + } + + Ext.applyIf(config, { + model: 'KeyValue', + proxy: { + type: 'proxmox', + url: config.url, + extraParams: config.extraParams, + reader: { + type: 'jsonobject', + rows: config.rows, + readArray: config.readArray, + rootProperty: config.root || 'data' + } + } + }); + + me.callParent([config]); + } +}); +/* Extends the Proxmox.data.UpdateStore type + * + * + */ +Ext.define('Proxmox.data.RRDStore', { + extend: 'Proxmox.data.UpdateStore', + alias: 'store.proxmoxRRDStore', + + setRRDUrl: function(timeframe, cf) { + var me = this; + if (!timeframe) { + timeframe = me.timeframe; + } + + if (!cf) { + cf = me.cf; + } + + me.proxy.url = me.rrdurl + "?timeframe=" + timeframe + "&cf=" + cf; + }, + + proxy: { + type: 'proxmox' + }, + + timeframe: 'hour', + + cf: 'AVERAGE', + + constructor: function(config) { + var me = this; + + config = config || {}; + + // set default interval to 30seconds + if (!config.interval) { + config.interval = 30000; + } + + // set a new storeid + if (!config.storeid) { + config.storeid = 'rrdstore-' + (++Ext.idSeed); + } + + // rrdurl is required + if (!config.rrdurl) { + throw "no rrdurl specified"; + } + + var stateid = 'proxmoxRRDTypeSelection'; + var sp = Ext.state.Manager.getProvider(); + var stateinit = sp.get(stateid); + + if (stateinit) { + if(stateinit.timeframe !== me.timeframe || stateinit.cf !== me.rrdcffn){ + me.timeframe = stateinit.timeframe; + me.rrdcffn = stateinit.cf; + } + } + + me.callParent([config]); + + me.setRRDUrl(); + me.mon(sp, 'statechange', function(prov, key, state){ + if (key === stateid) { + if (state && state.id) { + if (state.timeframe !== me.timeframe || state.cf !== me.cf) { + me.timeframe = state.timeframe; + me.cf = state.cf; + me.setRRDUrl(); + me.reload(); + } + } + } + }); + } +}); +Ext.define('Timezone', { + extend: 'Ext.data.Model', + fields: ['zone'] +}); + +Ext.define('Proxmox.data.TimezoneStore', { + extend: 'Ext.data.Store', + model: 'Timezone', + data: [ + ['Africa/Abidjan'], + ['Africa/Accra'], + ['Africa/Addis_Ababa'], + ['Africa/Algiers'], + ['Africa/Asmara'], + ['Africa/Bamako'], + ['Africa/Bangui'], + ['Africa/Banjul'], + ['Africa/Bissau'], + ['Africa/Blantyre'], + ['Africa/Brazzaville'], + ['Africa/Bujumbura'], + ['Africa/Cairo'], + ['Africa/Casablanca'], + ['Africa/Ceuta'], + ['Africa/Conakry'], + ['Africa/Dakar'], + ['Africa/Dar_es_Salaam'], + ['Africa/Djibouti'], + ['Africa/Douala'], + ['Africa/El_Aaiun'], + ['Africa/Freetown'], + ['Africa/Gaborone'], + ['Africa/Harare'], + ['Africa/Johannesburg'], + ['Africa/Kampala'], + ['Africa/Khartoum'], + ['Africa/Kigali'], + ['Africa/Kinshasa'], + ['Africa/Lagos'], + ['Africa/Libreville'], + ['Africa/Lome'], + ['Africa/Luanda'], + ['Africa/Lubumbashi'], + ['Africa/Lusaka'], + ['Africa/Malabo'], + ['Africa/Maputo'], + ['Africa/Maseru'], + ['Africa/Mbabane'], + ['Africa/Mogadishu'], + ['Africa/Monrovia'], + ['Africa/Nairobi'], + ['Africa/Ndjamena'], + ['Africa/Niamey'], + ['Africa/Nouakchott'], + ['Africa/Ouagadougou'], + ['Africa/Porto-Novo'], + ['Africa/Sao_Tome'], + ['Africa/Tripoli'], + ['Africa/Tunis'], + ['Africa/Windhoek'], + ['America/Adak'], + ['America/Anchorage'], + ['America/Anguilla'], + ['America/Antigua'], + ['America/Araguaina'], + ['America/Argentina/Buenos_Aires'], + ['America/Argentina/Catamarca'], + ['America/Argentina/Cordoba'], + ['America/Argentina/Jujuy'], + ['America/Argentina/La_Rioja'], + ['America/Argentina/Mendoza'], + ['America/Argentina/Rio_Gallegos'], + ['America/Argentina/Salta'], + ['America/Argentina/San_Juan'], + ['America/Argentina/San_Luis'], + ['America/Argentina/Tucuman'], + ['America/Argentina/Ushuaia'], + ['America/Aruba'], + ['America/Asuncion'], + ['America/Atikokan'], + ['America/Bahia'], + ['America/Bahia_Banderas'], + ['America/Barbados'], + ['America/Belem'], + ['America/Belize'], + ['America/Blanc-Sablon'], + ['America/Boa_Vista'], + ['America/Bogota'], + ['America/Boise'], + ['America/Cambridge_Bay'], + ['America/Campo_Grande'], + ['America/Cancun'], + ['America/Caracas'], + ['America/Cayenne'], + ['America/Cayman'], + ['America/Chicago'], + ['America/Chihuahua'], + ['America/Costa_Rica'], + ['America/Cuiaba'], + ['America/Curacao'], + ['America/Danmarkshavn'], + ['America/Dawson'], + ['America/Dawson_Creek'], + ['America/Denver'], + ['America/Detroit'], + ['America/Dominica'], + ['America/Edmonton'], + ['America/Eirunepe'], + ['America/El_Salvador'], + ['America/Fortaleza'], + ['America/Glace_Bay'], + ['America/Godthab'], + ['America/Goose_Bay'], + ['America/Grand_Turk'], + ['America/Grenada'], + ['America/Guadeloupe'], + ['America/Guatemala'], + ['America/Guayaquil'], + ['America/Guyana'], + ['America/Halifax'], + ['America/Havana'], + ['America/Hermosillo'], + ['America/Indiana/Indianapolis'], + ['America/Indiana/Knox'], + ['America/Indiana/Marengo'], + ['America/Indiana/Petersburg'], + ['America/Indiana/Tell_City'], + ['America/Indiana/Vevay'], + ['America/Indiana/Vincennes'], + ['America/Indiana/Winamac'], + ['America/Inuvik'], + ['America/Iqaluit'], + ['America/Jamaica'], + ['America/Juneau'], + ['America/Kentucky/Louisville'], + ['America/Kentucky/Monticello'], + ['America/La_Paz'], + ['America/Lima'], + ['America/Los_Angeles'], + ['America/Maceio'], + ['America/Managua'], + ['America/Manaus'], + ['America/Marigot'], + ['America/Martinique'], + ['America/Matamoros'], + ['America/Mazatlan'], + ['America/Menominee'], + ['America/Merida'], + ['America/Mexico_City'], + ['America/Miquelon'], + ['America/Moncton'], + ['America/Monterrey'], + ['America/Montevideo'], + ['America/Montreal'], + ['America/Montserrat'], + ['America/Nassau'], + ['America/New_York'], + ['America/Nipigon'], + ['America/Nome'], + ['America/Noronha'], + ['America/North_Dakota/Center'], + ['America/North_Dakota/New_Salem'], + ['America/Ojinaga'], + ['America/Panama'], + ['America/Pangnirtung'], + ['America/Paramaribo'], + ['America/Phoenix'], + ['America/Port-au-Prince'], + ['America/Port_of_Spain'], + ['America/Porto_Velho'], + ['America/Puerto_Rico'], + ['America/Rainy_River'], + ['America/Rankin_Inlet'], + ['America/Recife'], + ['America/Regina'], + ['America/Resolute'], + ['America/Rio_Branco'], + ['America/Santa_Isabel'], + ['America/Santarem'], + ['America/Santiago'], + ['America/Santo_Domingo'], + ['America/Sao_Paulo'], + ['America/Scoresbysund'], + ['America/Shiprock'], + ['America/St_Barthelemy'], + ['America/St_Johns'], + ['America/St_Kitts'], + ['America/St_Lucia'], + ['America/St_Thomas'], + ['America/St_Vincent'], + ['America/Swift_Current'], + ['America/Tegucigalpa'], + ['America/Thule'], + ['America/Thunder_Bay'], + ['America/Tijuana'], + ['America/Toronto'], + ['America/Tortola'], + ['America/Vancouver'], + ['America/Whitehorse'], + ['America/Winnipeg'], + ['America/Yakutat'], + ['America/Yellowknife'], + ['Antarctica/Casey'], + ['Antarctica/Davis'], + ['Antarctica/DumontDUrville'], + ['Antarctica/Macquarie'], + ['Antarctica/Mawson'], + ['Antarctica/McMurdo'], + ['Antarctica/Palmer'], + ['Antarctica/Rothera'], + ['Antarctica/South_Pole'], + ['Antarctica/Syowa'], + ['Antarctica/Vostok'], + ['Arctic/Longyearbyen'], + ['Asia/Aden'], + ['Asia/Almaty'], + ['Asia/Amman'], + ['Asia/Anadyr'], + ['Asia/Aqtau'], + ['Asia/Aqtobe'], + ['Asia/Ashgabat'], + ['Asia/Baghdad'], + ['Asia/Bahrain'], + ['Asia/Baku'], + ['Asia/Bangkok'], + ['Asia/Beirut'], + ['Asia/Bishkek'], + ['Asia/Brunei'], + ['Asia/Choibalsan'], + ['Asia/Chongqing'], + ['Asia/Colombo'], + ['Asia/Damascus'], + ['Asia/Dhaka'], + ['Asia/Dili'], + ['Asia/Dubai'], + ['Asia/Dushanbe'], + ['Asia/Gaza'], + ['Asia/Harbin'], + ['Asia/Ho_Chi_Minh'], + ['Asia/Hong_Kong'], + ['Asia/Hovd'], + ['Asia/Irkutsk'], + ['Asia/Jakarta'], + ['Asia/Jayapura'], + ['Asia/Jerusalem'], + ['Asia/Kabul'], + ['Asia/Kamchatka'], + ['Asia/Karachi'], + ['Asia/Kashgar'], + ['Asia/Kathmandu'], + ['Asia/Kolkata'], + ['Asia/Krasnoyarsk'], + ['Asia/Kuala_Lumpur'], + ['Asia/Kuching'], + ['Asia/Kuwait'], + ['Asia/Macau'], + ['Asia/Magadan'], + ['Asia/Makassar'], + ['Asia/Manila'], + ['Asia/Muscat'], + ['Asia/Nicosia'], + ['Asia/Novokuznetsk'], + ['Asia/Novosibirsk'], + ['Asia/Omsk'], + ['Asia/Oral'], + ['Asia/Phnom_Penh'], + ['Asia/Pontianak'], + ['Asia/Pyongyang'], + ['Asia/Qatar'], + ['Asia/Qyzylorda'], + ['Asia/Rangoon'], + ['Asia/Riyadh'], + ['Asia/Sakhalin'], + ['Asia/Samarkand'], + ['Asia/Seoul'], + ['Asia/Shanghai'], + ['Asia/Singapore'], + ['Asia/Taipei'], + ['Asia/Tashkent'], + ['Asia/Tbilisi'], + ['Asia/Tehran'], + ['Asia/Thimphu'], + ['Asia/Tokyo'], + ['Asia/Ulaanbaatar'], + ['Asia/Urumqi'], + ['Asia/Vientiane'], + ['Asia/Vladivostok'], + ['Asia/Yakutsk'], + ['Asia/Yekaterinburg'], + ['Asia/Yerevan'], + ['Atlantic/Azores'], + ['Atlantic/Bermuda'], + ['Atlantic/Canary'], + ['Atlantic/Cape_Verde'], + ['Atlantic/Faroe'], + ['Atlantic/Madeira'], + ['Atlantic/Reykjavik'], + ['Atlantic/South_Georgia'], + ['Atlantic/St_Helena'], + ['Atlantic/Stanley'], + ['Australia/Adelaide'], + ['Australia/Brisbane'], + ['Australia/Broken_Hill'], + ['Australia/Currie'], + ['Australia/Darwin'], + ['Australia/Eucla'], + ['Australia/Hobart'], + ['Australia/Lindeman'], + ['Australia/Lord_Howe'], + ['Australia/Melbourne'], + ['Australia/Perth'], + ['Australia/Sydney'], + ['Europe/Amsterdam'], + ['Europe/Andorra'], + ['Europe/Athens'], + ['Europe/Belgrade'], + ['Europe/Berlin'], + ['Europe/Bratislava'], + ['Europe/Brussels'], + ['Europe/Bucharest'], + ['Europe/Budapest'], + ['Europe/Chisinau'], + ['Europe/Copenhagen'], + ['Europe/Dublin'], + ['Europe/Gibraltar'], + ['Europe/Guernsey'], + ['Europe/Helsinki'], + ['Europe/Isle_of_Man'], + ['Europe/Istanbul'], + ['Europe/Jersey'], + ['Europe/Kaliningrad'], + ['Europe/Kiev'], + ['Europe/Lisbon'], + ['Europe/Ljubljana'], + ['Europe/London'], + ['Europe/Luxembourg'], + ['Europe/Madrid'], + ['Europe/Malta'], + ['Europe/Mariehamn'], + ['Europe/Minsk'], + ['Europe/Monaco'], + ['Europe/Moscow'], + ['Europe/Oslo'], + ['Europe/Paris'], + ['Europe/Podgorica'], + ['Europe/Prague'], + ['Europe/Riga'], + ['Europe/Rome'], + ['Europe/Samara'], + ['Europe/San_Marino'], + ['Europe/Sarajevo'], + ['Europe/Simferopol'], + ['Europe/Skopje'], + ['Europe/Sofia'], + ['Europe/Stockholm'], + ['Europe/Tallinn'], + ['Europe/Tirane'], + ['Europe/Uzhgorod'], + ['Europe/Vaduz'], + ['Europe/Vatican'], + ['Europe/Vienna'], + ['Europe/Vilnius'], + ['Europe/Volgograd'], + ['Europe/Warsaw'], + ['Europe/Zagreb'], + ['Europe/Zaporozhye'], + ['Europe/Zurich'], + ['Indian/Antananarivo'], + ['Indian/Chagos'], + ['Indian/Christmas'], + ['Indian/Cocos'], + ['Indian/Comoro'], + ['Indian/Kerguelen'], + ['Indian/Mahe'], + ['Indian/Maldives'], + ['Indian/Mauritius'], + ['Indian/Mayotte'], + ['Indian/Reunion'], + ['Pacific/Apia'], + ['Pacific/Auckland'], + ['Pacific/Chatham'], + ['Pacific/Chuuk'], + ['Pacific/Easter'], + ['Pacific/Efate'], + ['Pacific/Enderbury'], + ['Pacific/Fakaofo'], + ['Pacific/Fiji'], + ['Pacific/Funafuti'], + ['Pacific/Galapagos'], + ['Pacific/Gambier'], + ['Pacific/Guadalcanal'], + ['Pacific/Guam'], + ['Pacific/Honolulu'], + ['Pacific/Johnston'], + ['Pacific/Kiritimati'], + ['Pacific/Kosrae'], + ['Pacific/Kwajalein'], + ['Pacific/Majuro'], + ['Pacific/Marquesas'], + ['Pacific/Midway'], + ['Pacific/Nauru'], + ['Pacific/Niue'], + ['Pacific/Norfolk'], + ['Pacific/Noumea'], + ['Pacific/Pago_Pago'], + ['Pacific/Palau'], + ['Pacific/Pitcairn'], + ['Pacific/Pohnpei'], + ['Pacific/Port_Moresby'], + ['Pacific/Rarotonga'], + ['Pacific/Saipan'], + ['Pacific/Tahiti'], + ['Pacific/Tarawa'], + ['Pacific/Tongatapu'], + ['Pacific/Wake'], + ['Pacific/Wallis'], + ['UTC'] + ] +}); +Ext.define('Proxmox.form.field.Integer',{ + extend: 'Ext.form.field.Number', + alias: 'widget.proxmoxintegerfield', + + config: { + deleteEmpty: false + }, + + allowDecimals: false, + allowExponential: false, + step: 1, + + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue && !me.isFileUpload()) { + val = me.getSubmitValue(); + if (val !== undefined && val !== null && val !== '') { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + } + +}); +Ext.define('Proxmox.form.field.Textfield', { + extend: 'Ext.form.field.Text', + alias: ['widget.proxmoxtextfield'], + + config: { + skipEmptyText: true, + + deleteEmpty: false, + }, + + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue && !me.isFileUpload()) { + val = me.getSubmitValue(); + if (val !== null) { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + }, + + getSubmitValue: function() { + var me = this; + + var value = this.processRawValue(this.getRawValue()); + if (value !== '') { + return value; + } + + return me.getSkipEmptyText() ? null: value; + }, + + setAllowBlank: function(allowBlank) { + this.allowBlank = allowBlank; + this.validate(); + } +}); +Ext.define('Proxmox.DateTimeField', { + extend: 'Ext.form.FieldContainer', + xtype: 'promxoxDateTimeField', + + layout: 'hbox', + + referenceHolder: true, + + submitFormat: 'U', + + getValue: function() { + var me = this; + var d = me.lookupReference('dateentry').getValue(); + + if (d === undefined || d === null) { return null; } + + var t = me.lookupReference('timeentry').getValue(); + + if (t === undefined || t === null) { return null; } + + var offset = (t.getHours()*3600+t.getMinutes()*60)*1000; + + return new Date(d.getTime() + offset); + }, + + getSubmitValue: function() { + var me = this; + var format = me.submitFormat; + var value = me.getValue(); + + return value ? Ext.Date.format(value, format) : null; + }, + + items: [ + { + xtype: 'datefield', + editable: false, + reference: 'dateentry', + flex: 1, + format: 'Y-m-d' + }, + { + xtype: 'timefield', + reference: 'timeentry', + format: 'H:i', + width: 80, + value: '00:00', + increment: 60 + } + ], + + initComponent: function() { + var me = this; + + me.callParent(); + + var value = me.value || new Date(); + + me.lookupReference('dateentry').setValue(value); + me.lookupReference('timeentry').setValue(value); + + me.relayEvents(me.lookupReference('dateentry'), ['change']); + me.relayEvents(me.lookupReference('timeentry'), ['change']); + } +}); +Ext.define('Proxmox.form.Checkbox', { + extend: 'Ext.form.field.Checkbox', + alias: ['widget.proxmoxcheckbox'], + + config: { + defaultValue: undefined, + deleteDefaultValue: false, + deleteEmpty: false + }, + + inputValue: '1', + + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue) { + val = me.getSubmitValue(); + if (val !== null) { + data = {}; + if ((val == me.getDefaultValue()) && me.getDeleteDefaultValue()) { + data['delete'] = me.getName(); + } else { + data[me.getName()] = val; + } + } else if (me.getDeleteEmpty()) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + }, + + // also accept integer 1 as true + setRawValue: function(value) { + var me = this; + + if (value === 1) { + me.callParent([true]); + } else { + me.callParent([value]); + } + } + +}); +/* Key-Value ComboBox + * + * config properties: + * comboItems: an array of Key - Value pairs + * deleteEmpty: if set to true (default), an empty value received from the + * comboBox will reset the property to its default value + */ +Ext.define('Proxmox.form.KVComboBox', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.proxmoxKVComboBox', + + config: { + deleteEmpty: true + }, + + comboItems: undefined, + displayField: 'value', + valueField: 'key', + queryMode: 'local', + + // overide framework function to implement deleteEmpty behaviour + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue) { + val = me.getSubmitValue(); + if (val !== null && val !== '' && val !== '__default__') { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + }, + + validator: function(val) { + var me = this; + + if (me.editable || val === null || val === '') { + return true; + } + + if (me.store.getCount() > 0) { + var values = me.multiSelect ? val.split(me.delimiter) : [val]; + var items = me.store.getData().collect('value', 'data'); + if (Ext.Array.every(values, function(value) { + return Ext.Array.contains(items, value); + })) { + return true; + } + } + + // returns a boolean or string + /*jslint confusion: true */ + return "value '" + val + "' not allowed!"; + }, + + initComponent: function() { + var me = this; + + me.store = Ext.create('Ext.data.ArrayStore', { + model: 'KeyValue', + data : me.comboItems + }); + + if (me.initialConfig.editable === undefined) { + me.editable = false; + } + + me.callParent(); + }, + + setComboItems: function(items) { + var me = this; + + me.getStore().setData(items); + } + +}); +Ext.define('Proxmox.form.LanguageSelector', { + extend: 'Proxmox.form.KVComboBox', + xtype: 'proxmoxLanguageSelector', + + comboItems: Proxmox.Utils.language_array() +}); +/* + * ComboGrid component: a ComboBox where the dropdown menu (the + * "Picker") is a Grid with Rows and Columns expects a listConfig + * object with a columns property roughly based on the GridPicker from + * https://www.sencha.com/forum/showthread.php?299909 + * +*/ + +Ext.define('Proxmox.form.ComboGrid', { + extend: 'Ext.form.field.ComboBox', + alias: ['widget.proxmoxComboGrid'], + + // this value is used as default value after load() + preferredValue: undefined, + + // hack: allow to select empty value + // seems extjs does not allow that when 'editable == false' + onKeyUp: function(e, t) { + var me = this; + var key = e.getKey(); + + if (!me.editable && me.allowBlank && !me.multiSelect && + (key == e.BACKSPACE || key == e.DELETE)) { + me.setValue(''); + } + + me.callParent(arguments); + }, + + config: { + skipEmptyText: false, + deleteEmpty: false, + }, + + // needed to trigger onKeyUp etc. + enableKeyEvents: true, + + editable: false, + + // override ExtJS method + // if the field has multiSelect enabled, the store is not loaded, and + // the displayfield == valuefield, it saves the rawvalue as an array + // but the getRawValue method is only defined in the textfield class + // (which has not to deal with arrays) an returns the string in the + // field (not an array) + // + // so if we have multiselect enabled, return the rawValue (which + // should be an array) and else we do callParent so + // it should not impact any other use of the class + getRawValue: function() { + var me = this; + if (me.multiSelect) { + return me.rawValue; + } else { + return me.callParent(); + } + }, + + getSubmitData: function() { + var me = this; + + let data = null; + if (!me.disabled && me.submitValue) { + let val = me.getSubmitValue(); + if (val !== null) { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + }, + + getSubmitValue: function() { + var me = this; + + var value = me.callParent(); + if (value !== '') { + return value; + } + + return me.getSkipEmptyText() ? null: value; + }, + + setAllowBlank: function(allowBlank) { + this.allowBlank = allowBlank; + this.validate(); + }, + +// override ExtJS protected method + onBindStore: function(store, initial) { + var me = this, + picker = me.picker, + extraKeySpec, + valueCollectionConfig; + + // We're being bound, not unbound... + if (store) { + // If store was created from a 2 dimensional array with generated field names 'field1' and 'field2' + if (store.autoCreated) { + me.queryMode = 'local'; + me.valueField = me.displayField = 'field1'; + if (!store.expanded) { + me.displayField = 'field2'; + } + + // displayTpl config will need regenerating with the autogenerated displayField name 'field1' + me.setDisplayTpl(null); + } + if (!Ext.isDefined(me.valueField)) { + me.valueField = me.displayField; + } + + // Add a byValue index to the store so that we can efficiently look up records by the value field + // when setValue passes string value(s). + // The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys + // are found, they are all returned by the get call. + // This is so that findByText and findByValue are able to return the *FIRST* matching value. By default, + // if unique is true, CollectionKey keeps the *last* matching value. + extraKeySpec = { + byValue: { + rootProperty: 'data', + unique: false + } + }; + extraKeySpec.byValue.property = me.valueField; + store.setExtraKeys(extraKeySpec); + + if (me.displayField === me.valueField) { + store.byText = store.byValue; + } else { + extraKeySpec.byText = { + rootProperty: 'data', + unique: false + }; + extraKeySpec.byText.property = me.displayField; + store.setExtraKeys(extraKeySpec); + } + + // We hold a collection of the values which have been selected, keyed by this field's valueField. + // This collection also functions as the selected items collection for the BoundList's selection model + valueCollectionConfig = { + rootProperty: 'data', + extraKeys: { + byInternalId: { + property: 'internalId' + }, + byValue: { + property: me.valueField, + rootProperty: 'data' + } + }, + // Whenever this collection is changed by anyone, whether by this field adding to it, + // or the BoundList operating, we must refresh our value. + listeners: { + beginupdate: me.onValueCollectionBeginUpdate, + endupdate: me.onValueCollectionEndUpdate, + scope: me + } + }; + + // This becomes our collection of selected records for the Field. + me.valueCollection = new Ext.util.Collection(valueCollectionConfig); + + // We use the selected Collection as our value collection and the basis + // for rendering the tag list. + + //proxmox override: since the picker is represented by a grid panel, + // we changed here the selection to RowModel + me.pickerSelectionModel = new Ext.selection.RowModel({ + mode: me.multiSelect ? 'SIMPLE' : 'SINGLE', + // There are situations when a row is selected on mousedown but then the mouse is dragged to another row + // and released. In these situations, the event target for the click event won't be the row where the mouse + // was released but the boundview. The view will then determine that it should fire a container click, and + // the DataViewModel will then deselect all prior selections. Setting `deselectOnContainerClick` here will + // prevent the model from deselecting. + deselectOnContainerClick: false, + enableInitialSelection: false, + pruneRemoved: false, + selected: me.valueCollection, + store: store, + listeners: { + scope: me, + lastselectedchanged: me.updateBindSelection + } + }); + + if (!initial) { + me.resetToDefault(); + } + + if (picker) { + picker.setSelectionModel(me.pickerSelectionModel); + if (picker.getStore() !== store) { + picker.bindStore(store); + } + } + } + }, + + // copied from ComboBox + createPicker: function() { + var me = this; + var picker; + + var pickerCfg = Ext.apply({ + // proxmox overrides: display a grid for selection + xtype: 'gridpanel', + id: me.pickerId, + pickerField: me, + floating: true, + hidden: true, + store: me.store, + displayField: me.displayField, + preserveScrollOnRefresh: true, + pageSize: me.pageSize, + tpl: me.tpl, + selModel: me.pickerSelectionModel, + focusOnToFront: false + }, me.listConfig, me.defaultListConfig); + + picker = me.picker || Ext.widget(pickerCfg); + + if (picker.getStore() !== me.store) { + picker.bindStore(me.store); + } + + if (me.pageSize) { + picker.pagingToolbar.on('beforechange', me.onPageChange, me); + } + + // proxmox overrides: pass missing method in gridPanel to its view + picker.refresh = function() { + picker.getSelectionModel().select(me.valueCollection.getRange()); + picker.getView().refresh(); + }; + picker.getNodeByRecord = function() { + picker.getView().getNodeByRecord(arguments); + }; + + // We limit the height of the picker to fit in the space above + // or below this field unless the picker has its own ideas about that. + if (!picker.initialConfig.maxHeight) { + picker.on({ + beforeshow: me.onBeforePickerShow, + scope: me + }); + } + picker.getSelectionModel().on({ + beforeselect: me.onBeforeSelect, + beforedeselect: me.onBeforeDeselect, + focuschange: me.onFocusChange, + selectionChange: function (sm, selectedRecords) { + var me = this; + if (selectedRecords.length) { + me.setValue(selectedRecords); + me.fireEvent('select', me, selectedRecords); + } + }, + scope: me + }); + + // hack for extjs6 + // when the clicked item is the same as the previously selected, + // it does not select the item + // instead we hide the picker + if (!me.multiSelect) { + picker.on('itemclick', function (sm,record) { + if (picker.getSelection()[0] === record) { + picker.hide(); + } + }); + } + + // when our store is not yet loaded, we increase + // the height of the gridpanel, so that we can see + // the loading mask + // + // we save the minheight to reset it after the load + picker.on('show', function() { + if (me.enableLoadMask) { + me.savedMinHeight = picker.getMinHeight(); + picker.setMinHeight(100); + } + }); + + picker.getNavigationModel().navigateOnSpace = false; + + return picker; + }, + + initComponent: function() { + var me = this; + + Ext.apply(me, { + queryMode: 'local', + matchFieldWidth: false + }); + + Ext.applyIf(me, { value: ''}); // hack: avoid ExtJS validate() bug + + Ext.applyIf(me.listConfig, { width: 400 }); + + me.callParent(); + + // Create the picker at an early stage, so it is available to store the previous selection + if (!me.picker) { + me.createPicker(); + } + + if (me.editable) { + // The trigger.picker causes first a focus event on the field then + // toggles the selection picker. Thus skip expanding in this case, + // else our focus listner expands and the picker.trigger then + // collapses it directly afterwards. + Ext.override(me.triggers.picker, { + onMouseDown : function (e) { + // copied "should we focus" check from Ext.form.trigger.Trigger + if (e.pointerType !== 'touch' && !this.field.owns(Ext.Element.getActiveElement())) { + me.skip_expand_on_focus = true; + } + this.callParent(arguments); + } + }); + + me.on("focus", function(me) { + if (!me.isExpanded && !me.skip_expand_on_focus) { + me.expand(); + } + me.skip_expand_on_focus = false; + }); + } + + me.mon(me.store, 'beforeload', function() { + if (!me.isDisabled()) { + me.enableLoadMask = true; + } + }); + + // hack: autoSelect does not work + me.mon(me.store, 'load', function(store, r, success, o) { + if (success) { + me.clearInvalid(); + + if (me.enableLoadMask) { + delete me.enableLoadMask; + + // if the picker exists, + // we reset its minheight to the saved var/0 + // we have to update the layout, otherwise the height + // gets not recalculated + if (me.picker) { + me.picker.setMinHeight(me.savedMinHeight || 0); + delete me.savedMinHeight; + me.picker.updateLayout(); + } + } + + var def = me.getValue() || me.preferredValue; + if (def) { + me.setValue(def, true); // sync with grid + } + var found = false; + if (def) { + if (Ext.isArray(def)) { + Ext.Array.each(def, function(v) { + if (store.findRecord(me.valueField, v)) { + found = true; + return false; // break + } + }); + } else { + found = store.findRecord(me.valueField, def); + } + } + + if (!found) { + var rec = me.store.first(); + if (me.autoSelect && rec && rec.data) { + def = rec.data[me.valueField]; + me.setValue(def, true); + } else { + me.setValue(me.editable ? def : '', true); + } + } + } + }); + } +}); +Ext.define('Proxmox.form.RRDTypeSelector', { + extend: 'Ext.form.field.ComboBox', + alias: ['widget.proxmoxRRDTypeSelector'], + + displayField: 'text', + valueField: 'id', + editable: false, + queryMode: 'local', + value: 'hour', + stateEvents: [ 'select' ], + stateful: true, + stateId: 'proxmoxRRDTypeSelection', + store: { + type: 'array', + fields: [ 'id', 'timeframe', 'cf', 'text' ], + data : [ + [ 'hour', 'hour', 'AVERAGE', + gettext('Hour') + ' (' + gettext('average') +')' ], + [ 'hourmax', 'hour', 'MAX', + gettext('Hour') + ' (' + gettext('maximum') + ')' ], + [ 'day', 'day', 'AVERAGE', + gettext('Day') + ' (' + gettext('average') + ')' ], + [ 'daymax', 'day', 'MAX', + gettext('Day') + ' (' + gettext('maximum') + ')' ], + [ 'week', 'week', 'AVERAGE', + gettext('Week') + ' (' + gettext('average') + ')' ], + [ 'weekmax', 'week', 'MAX', + gettext('Week') + ' (' + gettext('maximum') + ')' ], + [ 'month', 'month', 'AVERAGE', + gettext('Month') + ' (' + gettext('average') + ')' ], + [ 'monthmax', 'month', 'MAX', + gettext('Month') + ' (' + gettext('maximum') + ')' ], + [ 'year', 'year', 'AVERAGE', + gettext('Year') + ' (' + gettext('average') + ')' ], + [ 'yearmax', 'year', 'MAX', + gettext('Year') + ' (' + gettext('maximum') + ')' ] + ] + }, + // save current selection in the state Provider so RRDView can read it + getState: function() { + var ind = this.getStore().findExact('id', this.getValue()); + var rec = this.getStore().getAt(ind); + if (!rec) { + return; + } + return { + id: rec.data.id, + timeframe: rec.data.timeframe, + cf: rec.data.cf + }; + }, + // set selection based on last saved state + applyState : function(state) { + if (state && state.id) { + this.setValue(state.id); + } + } +}); +Ext.define('Proxmox.form.BondModeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.bondModeSelector'], + + openvswitch: false, + + initComponent: function() { + var me = this; + + if (me.openvswitch) { + me.comboItems = Proxmox.Utils.bond_mode_array([ + 'active-backup', + 'balance-slb', + 'lacp-balance-slb', + 'lacp-balance-tcp', + ]); + } else { + me.comboItems = Proxmox.Utils.bond_mode_array([ + 'balance-rr', + 'active-backup', + 'balance-xor', + 'broadcast', + '802.3ad', + 'balance-tlb', + 'balance-alb', + ]); + } + + me.callParent(); + } +}); + +Ext.define('Proxmox.form.BondPolicySelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.bondPolicySelector'], + comboItems: [ + ['layer2', 'layer2'], + ['layer2+3', 'layer2+3'], + ['layer3+4', 'layer3+4'] + ] +}); + +Ext.define('Proxmox.form.NetworkSelectorController', { + extend: 'Ext.app.ViewController', + alias: 'controller.proxmoxNetworkSelectorController', + + init: function(view) { + var me = this; + + if (!view.nodename) { + throw "missing custom view config: nodename"; + } + view.getStore().getProxy().setUrl('/api2/json/nodes/'+ view.nodename + '/network'); + } +}); + +Ext.define('Proxmox.data.NetworkSelector', { + extend: 'Ext.data.Model', + fields: [ + {name: 'active'}, + {name: 'cidr'}, + {name: 'cidr6'}, + {name: 'address'}, + {name: 'address6'}, + {name: 'comments'}, + {name: 'iface'}, + {name: 'slaves'}, + {name: 'type'} + ] +}); + +Ext.define('Proxmox.form.NetworkSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.proxmoxNetworkSelector', + + controller: 'proxmoxNetworkSelectorController', + + nodename: 'localhost', + setNodename: function(nodename) { + this.nodename = nodename; + var networkSelectorStore = this.getStore(); + networkSelectorStore.removeAll(); + // because of manual local copy of data for ip4/6 + this.getPicker().refresh(); + if (networkSelectorStore && typeof networkSelectorStore.getProxy === 'function') { + networkSelectorStore.getProxy().setUrl('/api2/json/nodes/'+ nodename + '/network'); + networkSelectorStore.load(); + } + }, + // set default value to empty array, else it inits it with + // null and after the store load it is an empty array, + // triggering dirtychange + value: [], + valueField: 'cidr', + displayField: 'cidr', + store: { + autoLoad: true, + model: 'Proxmox.data.NetworkSelector', + proxy: { + type: 'proxmox' + }, + sorters: [ + { + property : 'iface', + direction: 'ASC' + } + ], + filters: [ + function(item) { + return item.data.cidr; + } + ], + listeners: { + load: function(store, records, successfull) { + + if (successfull) { + records.forEach(function(record) { + if (record.data.cidr6) { + let dest = (record.data.cidr) ? record.copy(null) : record; + dest.data.cidr = record.data.cidr6; + dest.data.address = record.data.address6; + delete record.data.cidr6; + dest.data.comments = record.data.comments6; + delete record.data.comments6; + store.add(dest); + } + }); + } + } + } + }, + listConfig: { + width: 600, + columns: [ + { + + header: gettext('CIDR'), + dataIndex: 'cidr', + hideable: false, + flex: 1 + }, + { + + header: gettext('IP'), + dataIndex: 'address', + hidden: true, + }, + { + header: gettext('Interface'), + width: 90, + dataIndex: 'iface' + }, + { + header: gettext('Active'), + renderer: Proxmox.Utils.format_boolean, + width: 60, + dataIndex: 'active' + }, + { + header: gettext('Type'), + width: 80, + hidden: true, + dataIndex: 'type' + }, + { + header: gettext('Comment'), + flex: 2, + dataIndex: 'comments' + } + ] + } +}); +/* Button features: + * - observe selection changes to enable/disable the button using enableFn() + * - pop up confirmation dialog using confirmMsg() + */ +Ext.define('Proxmox.button.Button', { + extend: 'Ext.button.Button', + alias: 'widget.proxmoxButton', + + // the selection model to observe + selModel: undefined, + + // if 'false' handler will not be called (button disabled) + enableFn: function(record) { }, + + // function(record) or text + confirmMsg: false, + + // take special care in confirm box (select no as default). + dangerous: false, + + initComponent: function() { + /*jslint confusion: true */ + + var me = this; + + if (me.handler) { + + // Note: me.realHandler may be a string (see named scopes) + var realHandler = me.handler; + + me.handler = function(button, event) { + var rec, msg; + if (me.selModel) { + rec = me.selModel.getSelection()[0]; + if (!rec || (me.enableFn(rec) === false)) { + return; + } + } + + if (me.confirmMsg) { + msg = me.confirmMsg; + if (Ext.isFunction(me.confirmMsg)) { + msg = me.confirmMsg(rec); + } + Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: msg, + buttons: Ext.Msg.YESNO, + defaultFocus: me.dangerous ? 'no' : 'yes', + callback: function(btn) { + if (btn !== 'yes') { + return; + } + Ext.callback(realHandler, me.scope, [button, event, rec], 0, me); + } + }); + } else { + Ext.callback(realHandler, me.scope, [button, event, rec], 0, me); + } + }; + } + + me.callParent(); + + var grid; + if (!me.selModel && me.selModel !== null) { + grid = me.up('grid'); + if (grid && grid.selModel) { + me.selModel = grid.selModel; + } + } + + if (me.waitMsgTarget === true) { + grid = me.up('grid'); + if (grid) { + me.waitMsgTarget = grid; + } else { + throw "unable to find waitMsgTarget"; + } + } + + if (me.selModel) { + + me.mon(me.selModel, "selectionchange", function() { + var rec = me.selModel.getSelection()[0]; + if (!rec || (me.enableFn(rec) === false)) { + me.setDisabled(true); + } else { + me.setDisabled(false); + } + }); + } + } +}); + + +Ext.define('Proxmox.button.StdRemoveButton', { + extend: 'Proxmox.button.Button', + alias: 'widget.proxmoxStdRemoveButton', + + text: gettext('Remove'), + + disabled: true, + + config: { + baseurl: undefined + }, + + getUrl: function(rec) { + var me = this; + + return me.baseurl + '/' + rec.getId(); + }, + + // also works with names scopes + callback: function(options, success, response) {}, + + getRecordName: function(rec) { return rec.getId() }, + + confirmMsg: function (rec) { + var me = this; + + var name = me.getRecordName(rec); + return Ext.String.format( + gettext('Are you sure you want to remove entry {0}'), + "'" + name + "'"); + }, + + handler: function(btn, event, rec) { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.getUrl(rec), + method: 'DELETE', + waitMsgTarget: me.waitMsgTarget, + callback: function(options, success, response) { + Ext.callback(me.callback, me.scope, [options, success, response], 0, me); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } +}); +/* help button pointing to an online documentation + for components contained in a modal window +*/ +/*global + proxmoxOnlineHelpInfo +*/ +Ext.define('Proxmox.button.Help', { + extend: 'Ext.button.Button', + xtype: 'proxmoxHelpButton', + + text: gettext('Help'), + + // make help button less flashy by styling it like toolbar buttons + iconCls: ' x-btn-icon-el-default-toolbar-small fa fa-question-circle', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + + hidden: true, + + listenToGlobalEvent: true, + + controller: { + xclass: 'Ext.app.ViewController', + listen: { + global: { + proxmoxShowHelp: 'onProxmoxShowHelp', + proxmoxHideHelp: 'onProxmoxHideHelp' + } + }, + onProxmoxShowHelp: function(helpLink) { + var me = this.getView(); + if (me.listenToGlobalEvent === true) { + me.setOnlineHelp(helpLink); + me.show(); + } + }, + onProxmoxHideHelp: function() { + var me = this.getView(); + if (me.listenToGlobalEvent === true) { + me.hide(); + } + } + }, + + // this sets the link and the tooltip text + setOnlineHelp:function(blockid) { + var me = this; + + var info = Proxmox.Utils.get_help_info(blockid); + if (info) { + me.onlineHelp = blockid; + var title = info.title; + if (info.subtitle) { + title += ' - ' + info.subtitle; + } + me.setTooltip(title); + } + }, + + // helper to set the onlineHelp via a config object + setHelpConfig: function(config) { + var me = this; + me.setOnlineHelp(config.onlineHelp); + }, + + handler: function() { + var me = this; + var docsURI; + + if (me.onlineHelp) { + docsURI = Proxmox.Utils.get_help_link(me.onlineHelp); + } + + if (docsURI) { + window.open(docsURI); + } else { + Ext.Msg.alert(gettext('Help'), gettext('No Help available')); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + me.callParent(); + + if (me.onlineHelp) { + me.setOnlineHelp(me.onlineHelp); // set tooltip + } + } +}); +/* Renders a list of key values objets + +mandatory config parameters: +rows: an object container where each propery is a key-value object we want to render + var rows = { + keyboard: { + header: gettext('Keyboard Layout'), + editor: 'Your.KeyboardEdit', + required: true + }, + +optional: +disabled: setting this parameter to true will disable selection and focus on the +proxmoxObjectGrid as well as greying out input elements. +Useful for a readonly tabular display + +*/ + +Ext.define('Proxmox.grid.ObjectGrid', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.proxmoxObjectGrid'], + disabled: false, + hideHeaders: true, + + monStoreErrors: false, + + add_combobox_row: function(name, text, opts) { + var me = this; + + opts = opts || {}; + me.rows = me.rows || {}; + + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue, + header: text, + renderer: opts.renderer, + editor: { + xtype: 'proxmoxWindowEdit', + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + items: { + xtype: 'proxmoxKVComboBox', + name: name, + comboItems: opts.comboItems, + value: opts.defaultValue, + deleteEmpty: opts.deleteEmpty ? true : false, + emptyText: opts.defaultValue, + labelWidth: Proxmox.Utils.compute_min_label_width( + text, opts.labelWidth), + fieldLabel: text + } + } + }; + }, + + add_text_row: function(name, text, opts) { + var me = this; + + opts = opts || {}; + me.rows = me.rows || {}; + + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue, + header: text, + renderer: opts.renderer, + editor: { + xtype: 'proxmoxWindowEdit', + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + items: { + xtype: 'proxmoxtextfield', + name: name, + deleteEmpty: opts.deleteEmpty ? true : false, + emptyText: opts.defaultValue, + labelWidth: Proxmox.Utils.compute_min_label_width( + text, opts.labelWidth), + vtype: opts.vtype, + fieldLabel: text + } + } + }; + }, + + add_boolean_row: function(name, text, opts) { + var me = this; + + opts = opts || {}; + me.rows = me.rows || {}; + + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue || 0, + header: text, + renderer: opts.renderer || Proxmox.Utils.format_boolean, + editor: { + xtype: 'proxmoxWindowEdit', + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + items: { + xtype: 'proxmoxcheckbox', + name: name, + uncheckedValue: 0, + defaultValue: opts.defaultValue || 0, + checked: opts.defaultValue ? true : false, + deleteDefaultValue: opts.deleteDefaultValue ? true : false, + labelWidth: Proxmox.Utils.compute_min_label_width( + text, opts.labelWidth), + fieldLabel: text + } + } + }; + }, + + add_integer_row: function(name, text, opts) { + var me = this; + + opts = opts || {} + me.rows = me.rows || {}; + + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue, + header: text, + renderer: opts.renderer, + editor: { + xtype: 'proxmoxWindowEdit', + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + items: { + xtype: 'proxmoxintegerfield', + name: name, + minValue: opts.minValue, + maxValue: opts.maxValue, + emptyText: gettext('Default'), + deleteEmpty: opts.deleteEmpty ? true : false, + value: opts.defaultValue, + labelWidth: Proxmox.Utils.compute_min_label_width( + text, opts.labelWidth), + fieldLabel: text + } + } + }; + }, + + editorConfig: {}, // default config passed to editor + + run_editor: function() { + var me = this; + + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rows = me.rows; + var rowdef = rows[rec.data.key]; + if (!rowdef.editor) { + return; + } + + var win; + var config; + if (Ext.isString(rowdef.editor)) { + config = Ext.apply({ + confid: rec.data.key, + }, me.editorConfig); + win = Ext.create(rowdef.editor, config); + } else { + config = Ext.apply({ + confid: rec.data.key, + }, me.editorConfig); + Ext.apply(config, rowdef.editor); + win = Ext.createWidget(rowdef.editor.xtype, config); + win.load(); + } + + win.show(); + win.on('destroy', me.reload, me); + }, + + reload: function() { + var me = this; + me.rstore.load(); + }, + + getObjectValue: function(key, defaultValue) { + var me = this; + var rec = me.store.getById(key); + if (rec) { + return rec.data.value; + } + return defaultValue; + }, + + renderKey: function(key, metaData, record, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = (rows && rows[key]) ? rows[key] : {}; + return rowdef.header || key; + }, + + renderValue: function(value, metaData, record, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var key = record.data.key; + var rowdef = (rows && rows[key]) ? rows[key] : {}; + + var renderer = rowdef.renderer; + if (renderer) { + return renderer(value, metaData, record, rowIndex, colIndex, store); + } + + return value; + }, + + listeners: { + itemkeydown: function(view, record, item, index, e) { + if (e.getKey() === e.ENTER) { + this.pressedIndex = index; + } + }, + itemkeyup: function(view, record, item, index, e) { + if (e.getKey() === e.ENTER && index == this.pressedIndex) { + this.run_editor(); + } + + this.pressedIndex = undefined; + } + }, + + initComponent : function() { + var me = this; + + var rows = me.rows; + + if (!me.rstore) { + if (!me.url) { + throw "no url specified"; + } + + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + url: me.url, + interval: me.interval, + extraParams: me.extraParams, + rows: me.rows + }); + } + + var rstore = me.rstore; + + var store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore, + sorters: [], + filters: [] + }); + + if (rows) { + Ext.Object.each(rows, function(key, rowdef) { + if (Ext.isDefined(rowdef.defaultValue)) { + store.add({ key: key, value: rowdef.defaultValue }); + } else if (rowdef.required) { + store.add({ key: key, value: undefined }); + } + }); + } + + if (me.sorterFn) { + store.sorters.add(Ext.create('Ext.util.Sorter', { + sorterFn: me.sorterFn + })); + } + + store.filters.add(Ext.create('Ext.util.Filter', { + filterFn: function(item) { + if (rows) { + var rowdef = rows[item.data.key]; + if (!rowdef || (rowdef.visible === false)) { + return false; + } + } + return true; + } + })); + + Proxmox.Utils.monStoreErrors(me, rstore); + + Ext.applyIf(me, { + store: store, + stateful: false, + columns: [ + { + header: gettext('Name'), + width: me.cwidth1 || 200, + dataIndex: 'key', + renderer: me.renderKey + }, + { + flex: 1, + header: gettext('Value'), + dataIndex: 'value', + renderer: me.renderValue + } + ] + }); + + me.callParent(); + + if (me.monStoreErrors) { + Proxmox.Utils.monStoreErrors(me, me.store); + } + } +}); +Ext.define('Proxmox.grid.PendingObjectGrid', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.proxmoxPendingObjectGrid'], + + getObjectValue: function(key, defaultValue, pending) { + var me = this; + var rec = me.store.getById(key); + if (rec) { + var value = rec.data.value; + if (pending) { + if (Ext.isDefined(rec.data.pending) && rec.data.pending !== '') { + value = rec.data.pending; + } else if (rec.data['delete'] === 1) { + value = defaultValue; + } + } + + if (Ext.isDefined(value) && (value !== '')) { + return value; + } else { + return defaultValue; + } + } + return defaultValue; + }, + + hasPendingChanges: function(key) { + var me = this; + var rows = me.rows; + var rowdef = (rows && rows[key]) ? rows[key] : {}; + var keys = rowdef.multiKey || [ key ]; + var pending = false; + + Ext.Array.each(keys, function(k) { + var rec = me.store.getById(k); + if (rec && rec.data && ( + (Ext.isDefined(rec.data.pending) && rec.data.pending !== '') || + rec.data['delete'] === 1 + )) { + pending = true; + return false; // break + } + }); + + return pending; + }, + + renderValue: function(value, metaData, record, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var key = record.data.key; + var rowdef = (rows && rows[key]) ? rows[key] : {}; + var renderer = rowdef.renderer; + var current = ''; + var pendingdelete = ''; + var pending = ''; + + if (renderer) { + current = renderer(value, metaData, record, rowIndex, colIndex, store, false); + if (me.hasPendingChanges(key)) { + pending = renderer(record.data.pending, metaData, record, rowIndex, colIndex, store, true); + } + if (pending == current) { + pending = undefined; + } + } else { + current = value || ''; + pending = record.data.pending; + } + + if (record.data['delete']) { + var delete_all = true; + if (rowdef.multiKey) { + Ext.Array.each(rowdef.multiKey, function(k) { + var rec = me.store.getById(k); + if (rec && rec.data && rec.data['delete'] !== 1) { + delete_all = false; + return false; // break + } + }); + } + if (delete_all) { + pending = '
'+ current +'
'; + } + } + + if (pending) { + return current + '
' + pending + '
'; + } else { + return current; + } + }, + + initComponent : function() { + var me = this; + + var rows = me.rows; + + if (!me.rstore) { + if (!me.url) { + throw "no url specified"; + } + + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + model: 'KeyValuePendingDelete', + readArray: true, + url: me.url, + interval: me.interval, + extraParams: me.extraParams, + rows: me.rows + }); + } + + me.callParent(); + } +}); +Ext.define('Proxmox.panel.InputPanel', { + extend: 'Ext.panel.Panel', + alias: ['widget.inputpanel'], + listeners: { + activate: function() { + // notify owning container that it should display a help button + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp); + } + }, + deactivate: function() { + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp); + } + } + }, + border: false, + + // override this with an URL to a relevant chapter of the pve manual + // setting this will display a help button in our parent panel + onlineHelp: undefined, + + // will be set if the inputpanel has advanced items + hasAdvanced: false, + + // if the panel has advanced items, + // this will determine if they are shown by default + showAdvanced: false, + + // overwrite this to modify submit data + onGetValues: function(values) { + return values; + }, + + getValues: function(dirtyOnly) { + var me = this; + + if (Ext.isFunction(me.onGetValues)) { + dirtyOnly = false; + } + + var values = {}; + + Ext.Array.each(me.query('[isFormField]'), function(field) { + if (!dirtyOnly || field.isDirty()) { + Proxmox.Utils.assemble_field_data(values, field.getSubmitData()); + } + }); + + return me.onGetValues(values); + }, + + setAdvancedVisible: function(visible) { + var me = this; + var advItems = me.getComponent('advancedContainer'); + if (advItems) { + advItems.setVisible(visible); + } + }, + + setValues: function(values) { + var me = this; + + var form = me.up('form'); + + Ext.iterate(values, function(fieldId, val) { + var field = me.query('[isFormField][name=' + fieldId + ']')[0]; + if (field) { + field.setValue(val); + if (form.trackResetOnLoad) { + field.resetOriginalValue(); + } + } + }); + }, + + initComponent: function() { + var me = this; + + var items; + + if (me.items) { + me.columns = 1; + items = [ + { + columnWidth: 1, + layout: 'anchor', + items: me.items + } + ]; + me.items = undefined; + } else if (me.column4) { + me.columns = 4; + items = [ + { + columnWidth: 0.25, + padding: '0 10 0 0', + layout: 'anchor', + items: me.column1 + }, + { + columnWidth: 0.25, + padding: '0 10 0 0', + layout: 'anchor', + items: me.column2 + }, + { + columnWidth: 0.25, + padding: '0 10 0 0', + layout: 'anchor', + items: me.column3 + }, + { + columnWidth: 0.25, + padding: '0 0 0 10', + layout: 'anchor', + items: me.column4 + } + ]; + if (me.columnB) { + items.push({ + columnWidth: 1, + padding: '10 0 0 0', + layout: 'anchor', + items: me.columnB + }); + } + } else if (me.column1) { + me.columns = 2; + items = [ + { + columnWidth: 0.5, + padding: '0 10 0 0', + layout: 'anchor', + items: me.column1 + }, + { + columnWidth: 0.5, + padding: '0 0 0 10', + layout: 'anchor', + items: me.column2 || [] // allow empty column + } + ]; + if (me.columnB) { + items.push({ + columnWidth: 1, + padding: '10 0 0 0', + layout: 'anchor', + items: me.columnB + }); + } + } else { + throw "unsupported config"; + } + + var advItems; + if (me.advancedItems) { + advItems = [ + { + columnWidth: 1, + layout: 'anchor', + items: me.advancedItems + } + ]; + me.advancedItems = undefined; + } else if (me.advancedColumn1) { + advItems = [ + { + columnWidth: 0.5, + padding: '0 10 0 0', + layout: 'anchor', + items: me.advancedColumn1 + }, + { + columnWidth: 0.5, + padding: '0 0 0 10', + layout: 'anchor', + items: me.advancedColumn2 || [] // allow empty column + } + ]; + + me.advancedColumn1 = undefined; + me.advancedColumn2 = undefined; + + if (me.advancedColumnB) { + advItems.push({ + columnWidth: 1, + padding: '10 0 0 0', + layout: 'anchor', + items: me.advancedColumnB + }); + me.advancedColumnB = undefined; + } + } + + if (advItems) { + me.hasAdvanced = true; + advItems.unshift({ + columnWidth: 1, + xtype: 'box', + hidden: false, + border: true, + autoEl: { + tag: 'hr' + } + }); + items.push({ + columnWidth: 1, + xtype: 'container', + itemId: 'advancedContainer', + hidden: !me.showAdvanced, + layout: 'column', + defaults: { + border: false + }, + items: advItems + }); + } + + if (me.useFieldContainer) { + Ext.apply(me, { + layout: 'fit', + items: Ext.apply(me.useFieldContainer, { + layout: 'column', + defaultType: 'container', + items: items + }) + }); + } else { + Ext.apply(me, { + layout: 'column', + defaultType: 'container', + items: items + }); + } + + me.callParent(); + } +}); +/* + * Display log entries in a panel with scrollbar + * The log entries are automatically refreshed via a background task, + * with newest entries comming at the bottom + */ +Ext.define('Proxmox.panel.LogView', { + extend: 'Ext.panel.Panel', + xtype: 'proxmoxLogView', + + pageSize: 500, + viewBuffer: 50, + lineHeight: 16, + + scrollToEnd: true, + + // callback for load failure, used for ceph + failCallback: undefined, + + controller: { + xclass: 'Ext.app.ViewController', + + updateParams: function() { + var me = this; + var viewModel = me.getViewModel(); + var since = viewModel.get('since'); + var until = viewModel.get('until'); + if (viewModel.get('hide_timespan')) { + return; + } + + if (since > until) { + Ext.Msg.alert('Error', 'Since date must be less equal than Until date.'); + return; + } + + viewModel.set('params.since', Ext.Date.format(since, 'Y-m-d')); + viewModel.set('params.until', Ext.Date.format(until, 'Y-m-d') + ' 23:59:59'); + me.getView().loadTask.delay(200); + }, + + scrollPosBottom: function() { + var view = this.getView(); + var pos = view.getScrollY(); + var maxPos = view.getScrollable().getMaxPosition().y; + return maxPos - pos; + }, + + updateView: function(text, first, total) { + var me = this; + var view = me.getView(); + var viewModel = me.getViewModel(); + var content = me.lookup('content'); + var data = viewModel.get('data'); + + if (first === data.first && total === data.total && text.length === data.textlen) { + return; // same content, skip setting and scrolling + } + viewModel.set('data', { + first: first, + total: total, + textlen: text.length + }); + + var scrollPos = me.scrollPosBottom(); + + content.update(text); + + if (view.scrollToEnd && scrollPos <= 0) { + // we use setTimeout to work around scroll handling on touchscreens + setTimeout(function() { view.scrollTo(0, Infinity); }, 10); + } + }, + + doLoad: function() { + var me = this; + if (me.running) { + me.requested = true; + return; + } + me.running = true; + var view = me.getView(); + var viewModel = me.getViewModel(); + Proxmox.Utils.API2Request({ + url: me.getView().url, + params: viewModel.get('params'), + method: 'GET', + success: function(response) { + Proxmox.Utils.setErrorMask(me, false); + var total = response.result.total; + var lines = new Array(); + var first = Infinity; + + Ext.Array.each(response.result.data, function(line) { + if (first > line.n) { + first = line.n; + } + lines[line.n - 1] = Ext.htmlEncode(line.t); + }); + + lines.length = total; + me.updateView(lines.join('
'), first - 1, total); + me.running = false; + if (me.requested) { + me.requested = false; + view.loadTask.delay(200); + } + }, + failure: function(response) { + if (view.failCallback) { + view.failCallback(response); + } else { + var msg = response.htmlStatus; + Proxmox.Utils.setErrorMask(me, msg); + } + me.running = false; + if (me.requested) { + me.requested = false; + view.loadTask.delay(200); + } + } + }); + }, + + onScroll: function(x, y) { + var me = this; + var view = me.getView(); + var viewModel = me.getViewModel(); + + var lineHeight = view.lineHeight; + var line = view.getScrollY()/lineHeight; + var start = viewModel.get('params.start'); + var limit = viewModel.get('params.limit'); + var viewLines = view.getHeight()/lineHeight; + + var viewStart = Math.max(parseInt(line - 1 - view.viewBuffer, 10), 0); + var viewEnd = parseInt(line + viewLines + 1 + view.viewBuffer, 10); + + if (viewStart < start || viewEnd > (start+limit)) { + viewModel.set('params.start', + Math.max(parseInt(line - limit/2 + 10, 10), 0)); + view.loadTask.delay(200); + } + }, + + init: function(view) { + var me = this; + + if (!view.url) { + throw "no url specified"; + } + + var viewModel = this.getViewModel(); + var since = new Date(); + since.setDate(since.getDate() - 3); + viewModel.set('until', new Date()); + viewModel.set('since', since); + viewModel.set('params.limit', view.pageSize); + viewModel.set('hide_timespan', !view.log_select_timespan); + me.lookup('content').setStyle('line-height', view.lineHeight + 'px'); + + view.loadTask = new Ext.util.DelayedTask(me.doLoad, me); + + me.updateParams(); + view.task = Ext.TaskManager.start({ + run: function() { + if (!view.isVisible() || !view.scrollToEnd) { + return; + } + + if (me.scrollPosBottom() <= 1) { + view.loadTask.delay(200); + } + }, + interval: 1000 + }); + } + }, + + onDestroy: function() { + var me = this; + me.loadTask.cancel(); + Ext.TaskManager.stop(me.task); + }, + + // for user to initiate a load from outside + requestUpdate: function() { + var me = this; + me.loadTask.delay(200); + }, + + viewModel: { + data: { + until: null, + since: null, + hide_timespan: false, + data: { + start: 0, + total: 0, + textlen: 0 + }, + params: { + start: 0, + limit: 500, + } + } + }, + + layout: 'auto', + bodyPadding: 5, + scrollable: { + x: 'auto', + y: 'auto', + listeners: { + // we have to have this here, since we cannot listen to events + // of the scroller in the viewcontroller (extjs bug?), nor does + // the panel have a 'scroll' event' + scroll: { + fn: function(scroller, x, y) { + var controller = this.component.getController(); + if (controller) { // on destroy, controller can be gone + controller.onScroll(x,y); + } + }, + buffer: 200 + }, + } + }, + + tbar: { + bind: { + hidden: '{hide_timespan}' + }, + items: [ + '->', + 'Since: ', + { + xtype: 'datefield', + name: 'since_date', + reference: 'since', + format: 'Y-m-d', + bind: { + value: '{since}', + maxValue: '{until}' + } + }, + 'Until: ', + { + xtype: 'datefield', + name: 'until_date', + reference: 'until', + format: 'Y-m-d', + bind: { + value: '{until}', + minValue: '{since}' + } + }, + { + xtype: 'button', + text: 'Update', + handler: 'updateParams' + } + ], + }, + + items: [ + { + xtype: 'box', + reference: 'content', + style: { + font: 'normal 11px tahoma, arial, verdana, sans-serif', + 'white-space': 'pre' + }, + } + ] +}); +/* + * Display log entries in a panel with scrollbar + * The log entries are automatically refreshed via a background task, + * with newest entries comming at the bottom + */ +Ext.define('Proxmox.panel.JournalView', { + extend: 'Ext.panel.Panel', + xtype: 'proxmoxJournalView', + + numEntries: 500, + lineHeight: 16, + + scrollToEnd: true, + + controller: { + xclass: 'Ext.app.ViewController', + + updateParams: function() { + var me = this; + var viewModel = me.getViewModel(); + var since = viewModel.get('since'); + var until = viewModel.get('until'); + + since.setHours(0, 0, 0, 0); + until.setHours(0, 0, 0, 0); + until.setDate(until.getDate()+1); + + me.getView().loadTask.delay(200, undefined, undefined, [ + false, + false, + Ext.Date.format(since, "U"), + Ext.Date.format(until, "U") + ]); + }, + + scrollPosBottom: function() { + var view = this.getView(); + var pos = view.getScrollY(); + var maxPos = view.getScrollable().getMaxPosition().y; + return maxPos - pos; + }, + + scrollPosTop: function() { + var view = this.getView(); + return view.getScrollY(); + }, + + updateScroll: function(livemode, num, scrollPos, scrollPosTop) { + var me = this; + var view = me.getView(); + + if (!livemode) { + setTimeout(function() { view.scrollTo(0, 0); }, 10); + } else if (view.scrollToEnd && scrollPos <= 0) { + setTimeout(function() { view.scrollTo(0, Infinity); }, 10); + } else if (!view.scrollToEnd && scrollPosTop < 20*view.lineHeight) { + setTimeout(function() { view.scrollTo(0, num*view.lineHeight + scrollPosTop); }, 10); + } + }, + + updateView: function(lines, livemode, top) { + var me = this; + var view = me.getView(); + var viewmodel = me.getViewModel(); + if (viewmodel.get('livemode') !== livemode) { + return; // we switched mode, do not update the content + } + var contentEl = me.lookup('content'); + + // save old scrollpositions + var scrollPos = me.scrollPosBottom(); + var scrollPosTop = me.scrollPosTop(); + + var newend = lines.shift(); + var newstart = lines.pop(); + + var num = lines.length; + var text = lines.map(Ext.htmlEncode).join('
'); + + if (!livemode) { + if (num) { + view.content = text; + } else { + view.content = 'nothing logged or no timespan selected'; + } + } else { + // update content + if (top && num) { + view.content = view.content ? text + '
' + view.content : text; + } else if (!top && num) { + view.content = view.content ? view.content + '
' + text : text; + } + + // update cursors + if (!top || !view.startcursor) { + view.startcursor = newstart; + } + + if (top || !view.endcursor) { + view.endcursor = newend; + } + } + + contentEl.update(view.content); + + me.updateScroll(livemode, num, scrollPos, scrollPosTop); + }, + + doLoad: function(livemode, top, since, until) { + var me = this; + if (me.running) { + me.requested = true; + return; + } + me.running = true; + var view = me.getView(); + var params = { + lastentries: view.numEntries || 500, + }; + if (livemode) { + if (!top && view.startcursor) { + params = { + startcursor: view.startcursor + }; + } else if (view.endcursor) { + params.endcursor = view.endcursor; + } + } else { + params = { + since: since, + until: until + }; + } + Proxmox.Utils.API2Request({ + url: view.url, + params: params, + waitMsgTarget: (!livemode) ? view : undefined, + method: 'GET', + success: function(response) { + Proxmox.Utils.setErrorMask(me, false); + var lines = response.result.data; + me.updateView(lines, livemode, top); + me.running = false; + if (me.requested) { + me.requested = false; + view.loadTask.delay(200); + } + }, + failure: function(response) { + var msg = response.htmlStatus; + Proxmox.Utils.setErrorMask(me, msg); + me.running = false; + if (me.requested) { + me.requested = false; + view.loadTask.delay(200); + } + } + }); + }, + + onScroll: function(x, y) { + var me = this; + var view = me.getView(); + var viewmodel = me.getViewModel(); + var livemode = viewmodel.get('livemode'); + if (!livemode) { + return; + } + + if (me.scrollPosTop() < 20*view.lineHeight) { + view.scrollToEnd = false; + view.loadTask.delay(200, undefined, undefined, [true, true]); + } else if (me.scrollPosBottom() <= 1) { + view.scrollToEnd = true; + } + }, + + init: function(view) { + var me = this; + + if (!view.url) { + throw "no url specified"; + } + + var viewmodel = me.getViewModel(); + var viewModel = this.getViewModel(); + var since = new Date(); + since.setDate(since.getDate() - 3); + viewModel.set('until', new Date()); + viewModel.set('since', since); + me.lookup('content').setStyle('line-height', view.lineHeight + 'px'); + + view.loadTask = new Ext.util.DelayedTask(me.doLoad, me, [true, false]); + + me.updateParams(); + view.task = Ext.TaskManager.start({ + run: function() { + if (!view.isVisible() || !view.scrollToEnd || !viewmodel.get('livemode')) { + return; + } + + if (me.scrollPosBottom() <= 1) { + view.loadTask.delay(200, undefined, undefined, [true, false]); + } + }, + interval: 1000 + }); + }, + + onLiveMode: function() { + var me = this; + var view = me.getView(); + delete view.startcursor; + delete view.endcursor; + delete view.content; + me.getViewModel().set('livemode', true); + view.scrollToEnd = true; + me.updateView([], true, false); + }, + + onTimespan: function() { + var me = this; + me.getViewModel().set('livemode', false); + me.updateView([], false); + } + }, + + onDestroy: function() { + var me = this; + me.loadTask.cancel(); + Ext.TaskManager.stop(me.task); + delete me.content; + }, + + // for user to initiate a load from outside + requestUpdate: function() { + var me = this; + me.loadTask.delay(200); + }, + + viewModel: { + data: { + livemode: true, + until: null, + since: null + } + }, + + layout: 'auto', + bodyPadding: 5, + scrollable: { + x: 'auto', + y: 'auto', + listeners: { + // we have to have this here, since we cannot listen to events + // of the scroller in the viewcontroller (extjs bug?), nor does + // the panel have a 'scroll' event' + scroll: { + fn: function(scroller, x, y) { + var controller = this.component.getController(); + if (controller) { // on destroy, controller can be gone + controller.onScroll(x,y); + } + }, + buffer: 200 + }, + } + }, + + tbar: { + + items: [ + '->', + { + xtype: 'segmentedbutton', + items: [ + { + text: gettext('Live Mode'), + bind: { + pressed: '{livemode}' + }, + handler: 'onLiveMode', + }, + { + text: gettext('Select Timespan'), + bind: { + pressed: '{!livemode}' + }, + handler: 'onTimespan', + } + ] + }, + { + xtype: 'box', + bind: { disabled: '{livemode}' }, + autoEl: { cn: gettext('Since') + ':' } + }, + { + xtype: 'datefield', + name: 'since_date', + reference: 'since', + format: 'Y-m-d', + bind: { + disabled: '{livemode}', + value: '{since}', + maxValue: '{until}' + } + }, + { + xtype: 'box', + bind: { disabled: '{livemode}' }, + autoEl: { cn: gettext('Until') + ':' } + }, + { + xtype: 'datefield', + name: 'until_date', + reference: 'until', + format: 'Y-m-d', + bind: { + disabled: '{livemode}', + value: '{until}', + minValue: '{since}' + } + }, + { + xtype: 'button', + text: 'Update', + reference: 'updateBtn', + handler: 'updateParams', + bind: { + disabled: '{livemode}' + } + } + ] + }, + + items: [ + { + xtype: 'box', + reference: 'content', + style: { + font: 'normal 11px tahoma, arial, verdana, sans-serif', + 'white-space': 'pre' + }, + } + ] +}); +Ext.define('Proxmox.widget.RRDChart', { + extend: 'Ext.chart.CartesianChart', + alias: 'widget.proxmoxRRDChart', + + unit: undefined, // bytes, bytespersecond, percent + + controller: { + xclass: 'Ext.app.ViewController', + + convertToUnits: function(value) { + var units = ['', 'k','M','G','T', 'P']; + var si = 0; + while(value >= 1000 && si < (units.length -1)){ + value = value / 1000; + si++; + } + + // javascript floating point weirdness + value = Ext.Number.correctFloat(value); + + // limit to 2 decimal points + value = Ext.util.Format.number(value, "0.##"); + + return value.toString() + " " + units[si]; + }, + + leftAxisRenderer: function(axis, label, layoutContext) { + var me = this; + + return me.convertToUnits(label); + }, + + onSeriesTooltipRender: function(tooltip, record, item) { + var me = this.getView(); + + var suffix = ''; + + if (me.unit === 'percent') { + suffix = '%'; + } else if (me.unit === 'bytes') { + suffix = 'B'; + } else if (me.unit === 'bytespersecond') { + suffix = 'B/s'; + } + + var prefix = item.field; + if (me.fieldTitles && me.fieldTitles[me.fields.indexOf(item.field)]) { + prefix = me.fieldTitles[me.fields.indexOf(item.field)]; + } + tooltip.setHtml(prefix + ': ' + this.convertToUnits(record.get(item.field)) + suffix + + '
' + new Date(record.get('time'))); + }, + + onAfterAnimation: function(chart, eopts) { + // if the undobuton is disabled, + // disable our tool + + var ourUndoZoomButton = chart.tools[0]; + var undoButton = chart.interactions[0].getUndoButton(); + ourUndoZoomButton.setDisabled(undoButton.isDisabled()); + } + }, + + width: 770, + height: 300, + animation: false, + interactions: [{ + type: 'crosszoom' + }], + axes: [{ + type: 'numeric', + position: 'left', + grid: true, + renderer: 'leftAxisRenderer', + //renderer: function(axis, label) { return label; }, + minimum: 0 + }, { + type: 'time', + position: 'bottom', + grid: true, + fields: ['time'] + }], + legend: { + docked: 'bottom' + }, + listeners: { + animationend: 'onAfterAnimation' + }, + + + initComponent: function() { + var me = this; + var series = {}; + + if (!me.store) { + throw "cannot work without store"; + } + + if (!me.fields) { + throw "cannot work without fields"; + } + + me.callParent(); + + // add correct label for left axis + var axisTitle = ""; + if (me.unit === 'percent') { + axisTitle = "%"; + } else if (me.unit === 'bytes') { + axisTitle = "Bytes"; + } else if (me.unit === 'bytespersecond') { + axisTitle = "Bytes/s"; + } else if (me.fieldTitles && me.fieldTitles.length === 1) { + axisTitle = me.fieldTitles[0]; + } else if (me.fields.length === 1) { + axisTitle = me.fields[0]; + } + + me.axes[0].setTitle(axisTitle); + + if (!me.noTool) { + me.addTool([{ + type: 'minus', + disabled: true, + tooltip: gettext('Undo Zoom'), + handler: function(){ + var undoButton = me.interactions[0].getUndoButton(); + if (undoButton.handler) { + undoButton.handler(); + } + } + },{ + type: 'restore', + tooltip: gettext('Toggle Legend'), + handler: function(){ + if (me.legend) { + me.legend.setVisible(!me.legend.isVisible()); + } + } + }]); + } + + // add a series for each field we get + me.fields.forEach(function(item, index){ + var title = item; + if (me.fieldTitles && me.fieldTitles[index]) { + title = me.fieldTitles[index]; + } + me.addSeries(Ext.apply( + { + type: 'line', + xField: 'time', + yField: item, + title: title, + fill: true, + style: { + lineWidth: 1.5, + opacity: 0.60 + }, + marker: { + opacity: 0, + scaling: 0.01, + fx: { + duration: 200, + easing: 'easeOut' + } + }, + highlightCfg: { + opacity: 1, + scaling: 1.5 + }, + tooltip: { + trackMouse: true, + renderer: 'onSeriesTooltipRender' + } + }, + me.seriesConfig + )); + }); + + // enable animation after the store is loaded + me.store.onAfter('load', function() { + me.setAnimation(true); + }, this, {single: true}); + } +}); +Ext.define('Proxmox.panel.GaugeWidget', { + extend: 'Ext.panel.Panel', + alias: 'widget.proxmoxGauge', + + defaults: { + style: { + 'text-align':'center' + } + }, + items: [ + { + xtype: 'box', + itemId: 'title', + data: { + title: '' + }, + tpl: '

{title}

' + }, + { + xtype: 'polar', + height: 120, + border: false, + itemId: 'chart', + series: [{ + type: 'gauge', + value: 0, + colors: ['#f5f5f5'], + sectors: [0], + donut: 90, + needleLength: 100, + totalAngle: Math.PI + }], + sprites: [{ + id: 'valueSprite', + type: 'text', + text: '', + textAlign: 'center', + textBaseline: 'bottom', + x: 125, + y: 110, + fontSize: 30 + }] + }, + { + xtype: 'box', + itemId: 'text' + } + ], + + header: false, + border: false, + + warningThreshold: 0.6, + criticalThreshold: 0.9, + warningColor: '#fc0', + criticalColor: '#FF6C59', + defaultColor: '#7289DA', + backgroundColor: '#2C2F33', + + initialValue: 0, + + + updateValue: function(value, text) { + var me = this; + var color = me.defaultColor; + var attr = {}; + + if (value >= me.criticalThreshold) { + color = me.criticalColor; + } else if (value >= me.warningThreshold) { + color = me.warningColor; + } + + me.chart.series[0].setColors([color, me.backgroundColor]); + me.chart.series[0].setValue(value*100); + + me.valueSprite.setText(' '+(value*100).toFixed(0) + '%'); + attr.x = me.chart.getWidth()/2; + attr.y = me.chart.getHeight()-20; + if (me.spriteFontSize) { + attr.fontSize = me.spriteFontSize; + } + me.valueSprite.setAttributes(attr, true); + + if (text !== undefined) { + me.text.setHtml(text); + } + }, + + initComponent: function() { + var me = this; + + me.callParent(); + + if (me.title) { + me.getComponent('title').update({title: me.title}); + } + me.text = me.getComponent('text'); + me.chart = me.getComponent('chart'); + me.valueSprite = me.chart.getSurface('chart').get('valueSprite'); + } +}); +// fixme: how can we avoid those lint errors? +/*jslint confusion: true */ +Ext.define('Proxmox.window.Edit', { + extend: 'Ext.window.Window', + alias: 'widget.proxmoxWindowEdit', + + // autoLoad trigger a load() after component creation + autoLoad: false, + + resizable: false, + + // use this tio atimatically generate a title like + // Create: + subject: undefined, + + // set isCreate to true if you want a Create button (instead + // OK and RESET) + isCreate: false, + + // set to true if you want an Add button (instead of Create) + isAdd: false, + + // set to true if you want an Remove button (instead of Create) + isRemove: false, + + // custom submitText + submitText: undefined, + + backgroundDelay: 0, + + // needed for finding the reference to submitbutton + // because we do not have a controller + referenceHolder: true, + defaultButton: 'submitbutton', + + // finds the first form field + defaultFocus: 'field[disabled=false][hidden=false]', + + showProgress: false, + + showTaskViewer: false, + + // gets called if we have a progress bar or taskview and it detected that + // the task finished. function(success) + taskDone: Ext.emptyFn, + + // gets called when the api call is finished, right at the beginning + // function(success, response, options) + apiCallDone: Ext.emptyFn, + + // assign a reference from docs, to add a help button docked to the + // bottom of the window. If undefined we magically fall back to the + // onlineHelp of our first item, if set. + onlineHelp: undefined, + + isValid: function() { + var me = this; + + var form = me.formPanel.getForm(); + return form.isValid(); + }, + + getValues: function(dirtyOnly) { + var me = this; + + var values = {}; + + var form = me.formPanel.getForm(); + + form.getFields().each(function(field) { + if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) { + Proxmox.Utils.assemble_field_data(values, field.getSubmitData()); + } + }); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly)); + }); + + return values; + }, + + setValues: function(values) { + var me = this; + + var form = me.formPanel.getForm(); + + Ext.iterate(values, function(fieldId, val) { + var field = form.findField(fieldId); + if (field && !field.up('inputpanel')) { + field.setValue(val); + if (form.trackResetOnLoad) { + field.resetOriginalValue(); + } + } + }); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + panel.setValues(values); + }); + }, + + submit: function() { + var me = this; + + var form = me.formPanel.getForm(); + + var values = me.getValues(); + Ext.Object.each(values, function(name, val) { + if (values.hasOwnProperty(name)) { + if (Ext.isArray(val) && !val.length) { + values[name] = ''; + } + } + }); + + if (me.digest) { + values.digest = me.digest; + } + + if (me.backgroundDelay) { + values.background_delay = me.backgroundDelay; + } + + var url = me.url; + if (me.method === 'DELETE') { + url = url + "?" + Ext.Object.toQueryString(values); + values = undefined; + } + + Proxmox.Utils.API2Request({ + url: url, + waitMsgTarget: me, + method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'), + params: values, + failure: function(response, options) { + me.apiCallDone(false, response, options); + + if (response.result && response.result.errors) { + form.markInvalid(response.result.errors); + } + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var hasProgressBar = (me.backgroundDelay || me.showProgress || me.showTaskViewer) && + response.result.data ? true : false; + + me.apiCallDone(true, response, options); + + if (hasProgressBar) { + // stay around so we can trigger our close events + // when background action is completed + me.hide(); + + var upid = response.result.data; + var viewerClass = me.showTaskViewer ? 'Viewer' : 'Progress'; + var win = Ext.create('Proxmox.window.Task' + viewerClass, { + upid: upid, + taskDone: me.taskDone, + listeners: { + destroy: function () { + me.close(); + } + } + }); + win.show(); + } else { + me.close(); + } + } + }); + }, + + load: function(options) { + var me = this; + + var form = me.formPanel.getForm(); + + options = options || {}; + + var newopts = Ext.apply({ + waitMsgTarget: me + }, options); + + var createWrapper = function(successFn) { + Ext.apply(newopts, { + url: me.url, + method: 'GET', + success: function(response, opts) { + form.clearInvalid(); + me.digest = response.result.data.digest; + if (successFn) { + successFn(response, opts); + } else { + me.setValues(response.result.data); + } + // hack: fix ExtJS bug + Ext.Array.each(me.query('radiofield'), function(f) { + f.resetOriginalValue(); + }); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus, function() { + me.close(); + }); + } + }); + }; + + createWrapper(options.success); + + Proxmox.Utils.API2Request(newopts); + }, + + initComponent : function() { + var me = this; + + if (!me.url) { + throw "no url specified"; + } + + if (me.create) {throw "deprecated parameter, use isCreate";} + + var items = Ext.isArray(me.items) ? me.items : [ me.items ]; + + me.items = undefined; + + me.formPanel = Ext.create('Ext.form.Panel', { + url: me.url, + method: me.method || 'PUT', + trackResetOnLoad: true, + bodyPadding: 10, + border: false, + defaults: Ext.apply({}, me.defaults, { + border: false + }), + fieldDefaults: Ext.apply({}, me.fieldDefaults, { + labelWidth: 100, + anchor: '100%' + }), + items: items + }); + + var inputPanel = me.formPanel.down('inputpanel'); + + var form = me.formPanel.getForm(); + + var submitText; + if (me.isCreate) { + if (me.submitText) { + submitText = me.submitText; + } else if (me.isAdd) { + submitText = gettext('Add'); + } else if (me.isRemove) { + submitText = gettext('Remove'); + } else { + submitText = gettext('Create'); + } + } else { + submitText = me.submitText || gettext('OK'); + } + + var submitBtn = Ext.create('Ext.Button', { + reference: 'submitbutton', + text: submitText, + disabled: !me.isCreate, + handler: function() { + me.submit(); + } + }); + + var resetBtn = Ext.create('Ext.Button', { + text: 'Reset', + disabled: true, + handler: function(){ + form.reset(); + } + }); + + var set_button_status = function() { + var valid = form.isValid(); + var dirty = form.isDirty(); + submitBtn.setDisabled(!valid || !(dirty || me.isCreate)); + resetBtn.setDisabled(!dirty); + + if (inputPanel && inputPanel.hasAdvanced) { + // we want to show the advanced options + // as soon as some of it is not valid + var advancedItems = me.down('#advancedContainer').query('field'); + var valid = true; + advancedItems.forEach(function(field) { + if (!field.isValid()) { + valid = false; + } + }); + + if (!valid) { + inputPanel.setAdvancedVisible(true); + me.down('#advancedcb').setValue(true); + } + } + }; + + form.on('dirtychange', set_button_status); + form.on('validitychange', set_button_status); + + var colwidth = 300; + if (me.fieldDefaults && me.fieldDefaults.labelWidth) { + colwidth += me.fieldDefaults.labelWidth - 100; + } + + var twoColumn = inputPanel && + (inputPanel.column1 || inputPanel.column2); + + if (me.subject && !me.title) { + me.title = Proxmox.Utils.dialog_title(me.subject, me.isCreate, me.isAdd); + } + + if (me.isCreate) { + me.buttons = [ submitBtn ] ; + } else { + me.buttons = [ submitBtn, resetBtn ]; + } + + if (inputPanel && inputPanel.hasAdvanced) { + var sp = Ext.state.Manager.getProvider(); + var advchecked = sp.get('proxmox-advanced-cb'); + inputPanel.setAdvancedVisible(advchecked); + me.buttons.unshift( + { + xtype: 'proxmoxcheckbox', + itemId: 'advancedcb', + boxLabelAlign: 'before', + boxLabel: gettext('Advanced'), + stateId: 'proxmox-advanced-cb', + value: advchecked, + listeners: { + change: function(cb, val) { + inputPanel.setAdvancedVisible(val); + sp.set('proxmox-advanced-cb', val); + } + } + } + ); + } + + var onlineHelp = me.onlineHelp; + if (!onlineHelp && inputPanel && inputPanel.onlineHelp) { + onlineHelp = inputPanel.onlineHelp; + } + + if (onlineHelp) { + var helpButton = Ext.create('Proxmox.button.Help'); + me.buttons.unshift(helpButton, '->'); + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', onlineHelp); + } + + Ext.applyIf(me, { + modal: true, + width: twoColumn ? colwidth*2 : colwidth, + border: false, + items: [ me.formPanel ] + }); + + me.callParent(); + + // always mark invalid fields + me.on('afterlayout', function() { + // on touch devices, the isValid function + // triggers a layout, which triggers an isValid + // and so on + // to prevent this we disable the layouting here + // and enable it afterwards + me.suspendLayout = true; + me.isValid(); + me.suspendLayout = false; + }); + + if (me.autoLoad) { + me.load(); + } + } +}); +Ext.define('Proxmox.window.PasswordEdit', { + extend: 'Proxmox.window.Edit', + alias: 'proxmoxWindowPasswordEdit', + + subject: gettext('Password'), + + url: '/api2/extjs/access/password', + + fieldDefaults: { + labelWidth: 120 + }, + + items: [ + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Password'), + minLength: 5, + allowBlank: false, + name: 'password', + listeners: { + change: function(field){ + field.next().validate(); + }, + blur: function(field){ + field.next().validate(); + } + } + }, + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Confirm password'), + name: 'verifypassword', + allowBlank: false, + vtype: 'password', + initialPassField: 'password', + submitValue: false + }, + { + xtype: 'hiddenfield', + name: 'userid' + } + ], + + initComponent : function() { + var me = this; + + if (!me.userid) { + throw "no userid specified"; + } + + me.callParent(); + me.down('[name=userid]').setValue(me.userid); + } +}); +Ext.define('Proxmox.window.TaskProgress', { + extend: 'Ext.window.Window', + alias: 'widget.proxmoxTaskProgress', + + taskDone: Ext.emptyFn, + + initComponent: function() { + var me = this; + + if (!me.upid) { + throw "no task specified"; + } + + var task = Proxmox.Utils.parse_task_upid(me.upid); + + var statstore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status", + interval: 1000, + rows: { + status: { defaultValue: 'unknown' }, + exitstatus: { defaultValue: 'unknown' } + } + }); + + me.on('destroy', statstore.stopUpdate); + + var getObjectValue = function(key, defaultValue) { + var rec = statstore.getById(key); + if (rec) { + return rec.data.value; + } + return defaultValue; + }; + + var pbar = Ext.create('Ext.ProgressBar', { text: 'running...' }); + + me.mon(statstore, 'load', function() { + var status = getObjectValue('status'); + if (status === 'stopped') { + var exitstatus = getObjectValue('exitstatus'); + if (exitstatus == 'OK') { + pbar.reset(); + pbar.updateText("Done!"); + Ext.Function.defer(me.close, 1000, me); + } else { + me.close(); + Ext.Msg.alert('Task failed', exitstatus); + } + me.taskDone(exitstatus == 'OK'); + } + }); + + var descr = Proxmox.Utils.format_task_description(task.type, task.id); + + Ext.apply(me, { + title: gettext('Task') + ': ' + descr, + width: 300, + layout: 'auto', + modal: true, + bodyPadding: 5, + items: pbar, + buttons: [ + { + text: gettext('Details'), + handler: function() { + var win = Ext.create('Proxmox.window.TaskViewer', { + taskDone: me.taskDone, + upid: me.upid + }); + win.show(); + me.close(); + } + } + ] + }); + + me.callParent(); + + statstore.startUpdate(); + + pbar.wait(); + } +}); + +// fixme: how can we avoid those lint errors? +/*jslint confusion: true */ + +Ext.define('Proxmox.window.TaskViewer', { + extend: 'Ext.window.Window', + alias: 'widget.proxmoxTaskViewer', + + extraTitle: '', // string to prepend after the generic task title + + taskDone: Ext.emptyFn, + + initComponent: function() { + var me = this; + + if (!me.upid) { + throw "no task specified"; + } + + var task = Proxmox.Utils.parse_task_upid(me.upid); + + var statgrid; + + var rows = { + status: { + header: gettext('Status'), + defaultValue: 'unknown', + renderer: function(value) { + if (value != 'stopped') { + return value; + } + var es = statgrid.getObjectValue('exitstatus'); + if (es) { + return value + ': ' + es; + } + } + }, + exitstatus: { + visible: false + }, + type: { + header: gettext('Task type'), + required: true + }, + user: { + header: gettext('User name'), + required: true + }, + node: { + header: gettext('Node'), + required: true + }, + pid: { + header: gettext('Process ID'), + required: true + }, + starttime: { + header: gettext('Start Time'), + required: true, + renderer: Proxmox.Utils.render_timestamp + }, + upid: { + header: gettext('Unique task ID') + } + }; + + var statstore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status", + interval: 1000, + rows: rows + }); + + me.on('destroy', statstore.stopUpdate); + + var stop_task = function() { + Proxmox.Utils.API2Request({ + url: "/nodes/" + task.node + "/tasks/" + me.upid, + waitMsgTarget: me, + method: 'DELETE', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + var stop_btn1 = new Ext.Button({ + text: gettext('Stop'), + disabled: true, + handler: stop_task + }); + + var stop_btn2 = new Ext.Button({ + text: gettext('Stop'), + disabled: true, + handler: stop_task + }); + + statgrid = Ext.create('Proxmox.grid.ObjectGrid', { + title: gettext('Status'), + layout: 'fit', + tbar: [ stop_btn1 ], + rstore: statstore, + rows: rows, + border: false + }); + + var logView = Ext.create('Proxmox.panel.LogView', { + title: gettext('Output'), + tbar: [ stop_btn2 ], + border: false, + url: "/api2/extjs/nodes/" + task.node + "/tasks/" + me.upid + "/log" + }); + + me.mon(statstore, 'load', function() { + var status = statgrid.getObjectValue('status'); + + if (status === 'stopped') { + logView.scrollToEnd = false; + logView.requestUpdate(); + statstore.stopUpdate(); + me.taskDone(statgrid.getObjectValue('exitstatus') == 'OK'); + } + + stop_btn1.setDisabled(status !== 'running'); + stop_btn2.setDisabled(status !== 'running'); + }); + + statstore.startUpdate(); + + Ext.apply(me, { + title: "Task viewer: " + task.desc + me.extraTitle, + width: 800, + height: 400, + layout: 'fit', + modal: true, + items: [{ + xtype: 'tabpanel', + region: 'center', + items: [ logView, statgrid ] + }] + }); + + me.callParent(); + + logView.fireEvent('show', logView); + } +}); + +Ext.define('apt-pkglist', { + extend: 'Ext.data.Model', + fields: [ 'Package', 'Title', 'Description', 'Section', 'Arch', + 'Priority', 'Version', 'OldVersion', 'ChangeLogUrl', 'Origin' ], + idProperty: 'Package' +}); + +Ext.define('Proxmox.node.APT', { + extend: 'Ext.grid.GridPanel', + + xtype: 'proxmoxNodeAPT', + + upgradeBtn: undefined, + + columns: [ + { + header: gettext('Package'), + width: 200, + sortable: true, + dataIndex: 'Package' + }, + { + text: gettext('Version'), + columns: [ + { + header: gettext('current'), + width: 100, + sortable: false, + dataIndex: 'OldVersion' + }, + { + header: gettext('new'), + width: 100, + sortable: false, + dataIndex: 'Version' + } + ] + }, + { + header: gettext('Description'), + sortable: false, + dataIndex: 'Title', + flex: 1 + } + ], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var store = Ext.create('Ext.data.Store', { + model: 'apt-pkglist', + groupField: 'Origin', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + "/apt/update" + }, + sorters: [ + { + property : 'Package', + direction: 'ASC' + } + ] + }); + + var groupingFeature = Ext.create('Ext.grid.feature.Grouping', { + groupHeaderTpl: '{[ "Origin: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})', + enableGroupingMenu: false + }); + + var rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', { + getAdditionalData: function (data, rowIndex, record, orig) { + var headerCt = this.view.headerCt; + var colspan = headerCt.getColumnCount(); + return { + rowBody: '
' + + Ext.String.htmlEncode(data.Description) + + '
', + rowBodyCls: me.full_description ? '' : Ext.baseCSSPrefix + 'grid-row-body-hidden', + rowBodyColspan: colspan + }; + } + }); + + var reload = function() { + store.load(); + }; + + Proxmox.Utils.monStoreErrors(me, store, true); + + var apt_command = function(cmd){ + Proxmox.Utils.API2Request({ + url: "/nodes/" + me.nodename + "/apt/" + cmd, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + me.mon(win, 'close', reload); + } + }); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var update_btn = new Ext.Button({ + text: gettext('Refresh'), + handler: function() { + Proxmox.Utils.checked_command(function() { apt_command('update'); }); + } + }); + + var show_changelog = function(rec) { + if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) { + return; + } + + var view = Ext.createWidget('component', { + autoScroll: true, + style: { + 'background-color': 'white', + 'white-space': 'pre', + 'font-family': 'monospace', + padding: '5px' + } + }); + + var win = Ext.create('Ext.window.Window', { + title: gettext('Changelog') + ": " + rec.data.Package, + width: 800, + height: 400, + layout: 'fit', + modal: true, + items: [ view ] + }); + + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: "/nodes/" + me.nodename + "/apt/changelog", + params: { + name: rec.data.Package, + version: rec.data.Version + }, + method: 'GET', + failure: function(response, opts) { + win.close(); + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + win.show(); + view.update(Ext.htmlEncode(response.result.data)); + } + }); + + }; + + var changelog_btn = new Proxmox.button.Button({ + text: gettext('Changelog'), + selModel: sm, + disabled: true, + enableFn: function(rec) { + if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) { + return false; + } + return true; + }, + handler: function(b, e, rec) { + show_changelog(rec); + } + }); + + var verbose_desc_checkbox = new Ext.form.field.Checkbox({ + boxLabel: gettext('Show details'), + value: false, + listeners: { + change: (f, val) => { + me.full_description = val; + me.getView().refresh(); + } + } + }); + + if (me.upgradeBtn) { + me.tbar = [ update_btn, me.upgradeBtn, changelog_btn, '->', verbose_desc_checkbox ]; + } else { + me.tbar = [ update_btn, changelog_btn, '->', verbose_desc_checkbox ]; + } + + Ext.apply(me, { + store: store, + stateful: true, + stateId: 'grid-update', + selModel: sm, + viewConfig: { + stripeRows: false, + emptyText: '
' + gettext('No updates available.') + '
' + }, + features: [ groupingFeature, rowBodyFeature ], + listeners: { + activate: reload, + itemdblclick: function(v, rec) { + show_changelog(rec); + } + } + }); + + me.callParent(); + } +}); +Ext.define('Proxmox.node.NetworkEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.proxmoxNodeNetworkEdit'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.iftype) { + throw "no network device type specified"; + } + + me.isCreate = !me.iface; + + var iface_vtype; + + if (me.iftype === 'bridge') { + iface_vtype = 'BridgeName'; + } else if (me.iftype === 'bond') { + iface_vtype = 'BondName'; + } else if (me.iftype === 'eth' && !me.isCreate) { + iface_vtype = 'InterfaceName'; + } else if (me.iftype === 'vlan' && !me.isCreate) { + iface_vtype = 'InterfaceName'; + } else if (me.iftype === 'OVSBridge') { + iface_vtype = 'BridgeName'; + } else if (me.iftype === 'OVSBond') { + iface_vtype = 'BondName'; + } else if (me.iftype === 'OVSIntPort') { + iface_vtype = 'InterfaceName'; + } else if (me.iftype === 'OVSPort') { + iface_vtype = 'InterfaceName'; + } else { + console.log(me.iftype); + throw "unknown network device type specified"; + } + + me.subject = Proxmox.Utils.render_network_iface_type(me.iftype); + + var column2 = []; + + if (!(me.iftype === 'OVSIntPort' || me.iftype === 'OVSPort' || + me.iftype === 'OVSBond')) { + column2.push({ + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Autostart'), + name: 'autostart', + uncheckedValue: 0, + checked: me.isCreate ? true : undefined + }); + } + + if (me.iftype === 'bridge') { + column2.push({ + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('VLAN aware'), + name: 'bridge_vlan_aware', + deleteEmpty: !me.isCreate + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('Bridge ports'), + name: 'bridge_ports' + }); + } else if (me.iftype === 'OVSBridge') { + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('Bridge ports'), + name: 'ovs_ports' + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('OVS options'), + name: 'ovs_options' + }); + } else if (me.iftype === 'OVSPort' || me.iftype === 'OVSIntPort') { + column2.push({ + xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield', + fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'), + allowBlank: false, + nodename: me.nodename, + bridgeType: 'OVSBridge', + name: 'ovs_bridge' + }); + column2.push({ + xtype: 'pveVlanField', + deleteEmpty: !me.isCreate, + name: 'ovs_tag', + value: '' + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('OVS options'), + name: 'ovs_options' + }); + } else if (me.iftype === 'bond') { + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('Slaves'), + name: 'slaves' + }); + + var policySelector = Ext.createWidget('bondPolicySelector', { + fieldLabel: gettext('Hash policy'), + name: 'bond_xmit_hash_policy', + deleteEmpty: !me.isCreate, + disabled: true + }); + + column2.push({ + xtype: 'bondModeSelector', + fieldLabel: gettext('Mode'), + name: 'bond_mode', + value: me.isCreate ? 'balance-rr' : undefined, + listeners: { + change: function(f, value) { + if (value === 'balance-xor' || + value === '802.3ad') { + policySelector.setDisabled(false); + } else { + policySelector.setDisabled(true); + policySelector.setValue(''); + } + } + }, + allowBlank: false + }); + + column2.push(policySelector); + + } else if (me.iftype === 'OVSBond') { + column2.push({ + xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield', + fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'), + allowBlank: false, + nodename: me.nodename, + bridgeType: 'OVSBridge', + name: 'ovs_bridge' + }); + column2.push({ + xtype: 'pveVlanField', + deleteEmpty: !me.isCreate, + name: 'ovs_tag', + value: '' + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('OVS options'), + name: 'ovs_options' + }); + } + + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('Comment'), + allowBlank: true, + nodename: me.nodename, + name: 'comments' + }); + + var url; + var method; + + if (me.isCreate) { + url = "/api2/extjs/nodes/" + me.nodename + "/network"; + method = 'POST'; + } else { + url = "/api2/extjs/nodes/" + me.nodename + "/network/" + me.iface; + method = 'PUT'; + } + + var column1 = [ + { + xtype: 'hiddenfield', + name: 'type', + value: me.iftype + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + fieldLabel: gettext('Name'), + name: 'iface', + value: me.iface, + vtype: iface_vtype, + allowBlank: false + } + ]; + + if (me.iftype === 'OVSBond') { + column1.push( + { + xtype: 'bondModeSelector', + fieldLabel: gettext('Mode'), + name: 'bond_mode', + openvswitch: true, + value: me.isCreate ? 'active-backup' : undefined, + allowBlank: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Slaves'), + name: 'ovs_bonds' + } + ); + } else { + + column1.push( + { + xtype: 'proxmoxtextfield', + deleteEmpty: !me.isCreate, + fieldLabel: 'IPv4/CIDR', + vtype: 'IPCIDRAddress', + name: 'cidr' + }, + { + xtype: 'proxmoxtextfield', + deleteEmpty: !me.isCreate, + fieldLabel: gettext('Gateway') + ' (IPv4)', + vtype: 'IPAddress', + name: 'gateway' + }, + { + xtype: 'proxmoxtextfield', + deleteEmpty: !me.isCreate, + fieldLabel: 'IPv6/CIDR', + vtype: 'IP6CIDRAddress', + name: 'cidr6' + }, + { + xtype: 'proxmoxtextfield', + deleteEmpty: !me.isCreate, + fieldLabel: gettext('Gateway') + ' (IPv6)', + vtype: 'IP6Address', + name: 'gateway6' + } + ); + } + + Ext.applyIf(me, { + url: url, + method: method, + items: { + xtype: 'inputpanel', + column1: column1, + column2: column2 + } + }); + + me.callParent(); + + if (me.isCreate) { + me.down('field[name=iface]').setValue(me.iface_default); + } else { + me.load({ + success: function(response, options) { + var data = response.result.data; + if (data.type !== me.iftype) { + var msg = "Got unexpected device type"; + Ext.Msg.alert(gettext('Error'), msg, function() { + me.close(); + }); + return; + } + me.setValues(data); + me.isValid(); // trigger validation + } + }); + } + } +}); +Ext.define('proxmox-networks', { + extend: 'Ext.data.Model', + fields: [ + 'iface', 'type', 'active', 'autostart', + 'bridge_ports', 'slaves', + 'address', 'netmask', 'gateway', + 'address6', 'netmask6', 'gateway6', + 'cidr', 'cidr6', + 'comments' + ], + idProperty: 'iface' +}); + +Ext.define('Proxmox.node.NetworkView', { + extend: 'Ext.panel.Panel', + + alias: ['widget.proxmoxNodeNetworkView'], + + // defines what types of network devices we want to create + // order is always the same + types: ['bridge', 'bond', 'ovs'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var baseUrl = '/nodes/' + me.nodename + '/network'; + + var store = Ext.create('Ext.data.Store', { + model: 'proxmox-networks', + proxy: { + type: 'proxmox', + url: '/api2/json' + baseUrl + }, + sorters: [ + { + property : 'iface', + direction: 'ASC' + } + ] + }); + + var reload = function() { + var changeitem = me.down('#changes'); + Proxmox.Utils.API2Request({ + url: baseUrl, + failure: function(response, opts) { + store.loadData({}); + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + changeitem.update(''); + changeitem.setHidden(true); + }, + success: function(response, opts) { + var result = Ext.decode(response.responseText); + store.loadData(result.data); + var changes = result.changes; + if (changes === undefined || changes === '') { + changes = gettext("No changes"); + changeitem.setHidden(true); + } else { + changeitem.update("
" + Ext.htmlEncode(changes) + "
"); + changeitem.setHidden(false); + } + } + }); + }; + + var run_editor = function() { + var grid = me.down('gridpanel'); + var sm = grid.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iface: rec.data.iface, + iftype: rec.data.type + }); + win.show(); + win.on('destroy', reload); + }; + + var edit_btn = new Ext.Button({ + text: gettext('Edit'), + disabled: true, + handler: run_editor + }); + + var del_btn = new Ext.Button({ + text: gettext('Remove'), + disabled: true, + handler: function(){ + var grid = me.down('gridpanel'); + var sm = grid.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var iface = rec.data.iface; + + Proxmox.Utils.API2Request({ + url: baseUrl + '/' + iface, + method: 'DELETE', + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + var set_button_status = function() { + var grid = me.down('gridpanel'); + var sm = grid.getSelectionModel(); + var rec = sm.getSelection()[0]; + + edit_btn.setDisabled(!rec); + del_btn.setDisabled(!rec); + }; + + var render_ports = function(value, metaData, record) { + if (value === 'bridge') { + return record.data.bridge_ports; + } else if (value === 'bond') { + return record.data.slaves; + } else if (value === 'OVSBridge') { + return record.data.ovs_ports; + } else if (value === 'OVSBond') { + return record.data.ovs_bonds; + } + }; + + var find_next_iface_id = function(prefix) { + var next; + for (next = 0; next <= 9999; next++) { + if (!store.getById(prefix + next.toString())) { + break; + } + } + return prefix + next.toString(); + }; + + var menu_items = []; + + if (me.types.indexOf('bridge') !== -1) { + menu_items.push({ + text: Proxmox.Utils.render_network_iface_type('bridge'), + handler: function() { + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iftype: 'bridge', + iface_default: find_next_iface_id('vmbr') + }); + win.on('destroy', reload); + win.show(); + } + }); + } + + if (me.types.indexOf('bond') !== -1) { + menu_items.push({ + text: Proxmox.Utils.render_network_iface_type('bond'), + handler: function() { + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iftype: 'bond', + iface_default: find_next_iface_id('bond') + }); + win.on('destroy', reload); + win.show(); + } + }); + } + + if (me.types.indexOf('ovs') !== -1) { + if (menu_items.length > 0) { + menu_items.push({ xtype: 'menuseparator' }); + } + + menu_items.push( + { + text: Proxmox.Utils.render_network_iface_type('OVSBridge'), + handler: function() { + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iftype: 'OVSBridge', + iface_default: find_next_iface_id('vmbr') + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: Proxmox.Utils.render_network_iface_type('OVSBond'), + handler: function() { + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iftype: 'OVSBond', + iface_default: find_next_iface_id('bond') + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: Proxmox.Utils.render_network_iface_type('OVSIntPort'), + handler: function() { + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iftype: 'OVSIntPort' + }); + win.on('destroy', reload); + win.show(); + } + } + ); + } + + var renderer_generator = function(fieldname) { + return function(val, metaData, rec) { + var tmp = []; + if (rec.data[fieldname]) { + tmp.push(rec.data[fieldname]); + } + if (rec.data[fieldname + '6']) { + tmp.push(rec.data[fieldname + '6']); + } + return tmp.join('
') || ''; + }; + }; + + Ext.apply(me, { + layout: 'border', + tbar: [ + { + text: gettext('Create'), + menu: { + plain: true, + items: menu_items + } + }, ' ', + { + text: gettext('Revert'), + handler: function() { + Proxmox.Utils.API2Request({ + url: baseUrl, + method: 'DELETE', + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }, + edit_btn, + del_btn + ], + items: [ + { + xtype: 'gridpanel', + stateful: true, + stateId: 'grid-node-network', + store: store, + region: 'center', + border: false, + columns: [ + { + header: gettext('Name'), + sortable: true, + dataIndex: 'iface' + }, + { + header: gettext('Type'), + sortable: true, + width: 120, + renderer: Proxmox.Utils.render_network_iface_type, + dataIndex: 'type' + }, + { + xtype: 'booleancolumn', + header: gettext('Active'), + width: 80, + sortable: true, + dataIndex: 'active', + trueText: Proxmox.Utils.yesText, + falseText: Proxmox.Utils.noText, + undefinedText: Proxmox.Utils.noText, + }, + { + xtype: 'booleancolumn', + header: gettext('Autostart'), + width: 80, + sortable: true, + dataIndex: 'autostart', + trueText: Proxmox.Utils.yesText, + falseText: Proxmox.Utils.noText, + undefinedText: Proxmox.Utils.noText + }, + { + xtype: 'booleancolumn', + header: gettext('VLAN aware'), + width: 80, + sortable: true, + dataIndex: 'bridge_vlan_aware', + trueText: Proxmox.Utils.yesText, + falseText: Proxmox.Utils.noText, + undefinedText: Proxmox.Utils.noText + }, + { + header: gettext('Ports/Slaves'), + dataIndex: 'type', + renderer: render_ports + }, + { + header: gettext('Bond Mode'), + dataIndex: 'bond_mode', + renderer: Proxmox.Utils.render_bond_mode, + }, + { + header: gettext('Hash Policy'), + hidden: true, + dataIndex: 'bond_xmit_hash_policy', + }, + { + header: gettext('IP address'), + sortable: true, + width: 120, + hidden: true, + dataIndex: 'address', + renderer: renderer_generator('address'), + }, + { + header: gettext('Subnet mask'), + width: 120, + sortable: true, + hidden: true, + dataIndex: 'netmask', + renderer: renderer_generator('netmask'), + }, + { + header: gettext('CIDR'), + width: 120, + sortable: true, + dataIndex: 'cidr', + renderer: renderer_generator('cidr'), + }, + { + header: gettext('Gateway'), + width: 120, + sortable: true, + dataIndex: 'gateway', + renderer: renderer_generator('gateway'), + }, + { + header: gettext('Comment'), + dataIndex: 'comments', + flex: 1, + renderer: Ext.String.htmlEncode + } + ], + listeners: { + selectionchange: set_button_status, + itemdblclick: run_editor + } + }, + { + border: false, + region: 'south', + autoScroll: true, + hidden: true, + itemId: 'changes', + tbar: [ + gettext('Pending changes') + ' (' + + gettext('Please reboot to activate changes') + ')' + ], + split: true, + bodyPadding: 5, + flex: 0.6, + html: gettext("No changes") + } + ], + }); + + me.callParent(); + reload(); + } +}); +Ext.define('Proxmox.node.DNSEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.proxmoxNodeDNSEdit'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.items = [ + { + xtype: 'textfield', + fieldLabel: gettext('Search domain'), + name: 'search', + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('DNS server') + " 1", + vtype: 'IP64Address', + skipEmptyText: true, + name: 'dns1' + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('DNS server') + " 2", + vtype: 'IP64Address', + skipEmptyText: true, + name: 'dns2' + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('DNS server') + " 3", + vtype: 'IP64Address', + skipEmptyText: true, + name: 'dns3' + } + ]; + + Ext.applyIf(me, { + subject: gettext('DNS'), + url: "/api2/extjs/nodes/" + me.nodename + "/dns", + fieldDefaults: { + labelWidth: 120 + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('Proxmox.node.HostsView', { + extend: 'Ext.panel.Panel', + xtype: 'proxmoxNodeHostsView', + + reload: function() { + var me = this; + me.store.load(); + }, + + tbar: [ + { + text: gettext('Save'), + disabled: true, + itemId: 'savebtn', + handler: function() { + var me = this.up('panel'); + Proxmox.Utils.API2Request({ + params: { + digest: me.digest, + data: me.down('#hostsfield').getValue() + }, + method: 'POST', + url: '/nodes/' + me.nodename + '/hosts', + waitMsgTarget: me, + success: function(response, opts) { + me.reload(); + }, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + } + }, + { + text: gettext('Revert'), + disabled: true, + itemId: 'resetbtn', + handler: function() { + var me = this.up('panel'); + me.down('#hostsfield').reset(); + } + } + ], + + layout: 'fit', + + items: [ + { + xtype: 'textarea', + itemId: 'hostsfield', + fieldStyle: { + 'font-family': 'monospace', + 'white-space': 'pre' + }, + listeners: { + dirtychange: function(ta, dirty) { + var me = this.up('panel'); + me.down('#savebtn').setDisabled(!dirty); + me.down('#resetbtn').setDisabled(!dirty); + } + } + } + ], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.store = Ext.create('Ext.data.Store', { + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + "/hosts", + } + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.store); + + me.mon(me.store, 'load', function(store, records, success) { + if (!success || records.length < 1) { + return; + } + me.digest = records[0].data.digest; + var data = records[0].data.data; + me.down('#hostsfield').setValue(data); + me.down('#hostsfield').resetOriginalValue(); + }); + + me.reload(); + } +}); +Ext.define('Proxmox.node.DNSView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.proxmoxNodeDNSView'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var run_editor = function() { + var win = Ext.create('Proxmox.node.DNSEdit', { + nodename: me.nodename + }); + win.show(); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + me.nodename + "/dns", + cwidth1: 130, + interval: 1000, + run_editor: run_editor, + rows: { + search: { + header: 'Search domain', + required: true, + renderer: Ext.htmlEncode + }, + dns1: { + header: gettext('DNS server') + " 1", + required: true, + renderer: Ext.htmlEncode + }, + dns2: { + header: gettext('DNS server') + " 2", + renderer: Ext.htmlEncode + }, + dns3: { + header: gettext('DNS server') + " 3", + renderer: Ext.htmlEncode + } + }, + tbar: [ + { + text: gettext("Edit"), + handler: run_editor + } + ], + listeners: { + itemdblclick: run_editor + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('deactivate', me.rstore.stopUpdate); + me.on('destroy', me.rstore.stopUpdate); + } +}); +Ext.define('Proxmox.node.Tasks', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.proxmoxNodeTasks'], + stateful: true, + stateId: 'grid-node-tasks', + loadMask: true, + sortableColumns: false, + vmidFilter: 0, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var store = Ext.create('Ext.data.BufferedStore', { + pageSize: 500, + autoLoad: true, + remoteFilter: true, + model: 'proxmox-tasks', + proxy: { + type: 'proxmox', + startParam: 'start', + limitParam: 'limit', + url: "/api2/json/nodes/" + me.nodename + "/tasks" + } + }); + + var userfilter = ''; + var filter_errors = 0; + + var updateProxyParams = function() { + var params = { + errors: filter_errors + }; + if (userfilter) { + params.userfilter = userfilter; + } + if (me.vmidFilter) { + params.vmid = me.vmidFilter; + } + store.proxy.extraParams = params; + }; + + updateProxyParams(); + + var reload_task = Ext.create('Ext.util.DelayedTask',function() { + updateProxyParams(); + store.reload(); + }); + + var run_task_viewer = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: rec.data.upid + }); + win.show(); + }; + + var view_btn = new Ext.Button({ + text: gettext('View'), + disabled: true, + handler: run_task_viewer + }); + + Proxmox.Utils.monStoreErrors(me, store, true); + + Ext.apply(me, { + store: store, + viewConfig: { + trackOver: false, + stripeRows: false, // does not work with getRowClass() + + getRowClass: function(record, index) { + var status = record.get('status'); + + if (status && status != 'OK') { + return "proxmox-invalid-row"; + } + } + }, + tbar: [ + view_btn, '->', gettext('User name') +':', ' ', + { + xtype: 'textfield', + width: 200, + value: userfilter, + enableKeyEvents: true, + listeners: { + keyup: function(field, e) { + userfilter = field.getValue(); + reload_task.delay(500); + } + } + }, ' ', gettext('Only Errors') + ':', ' ', + { + xtype: 'checkbox', + hideLabel: true, + checked: filter_errors, + listeners: { + change: function(field, checked) { + filter_errors = checked ? 1 : 0; + reload_task.delay(10); + } + } + }, ' ' + ], + columns: [ + { + header: gettext("Start Time"), + dataIndex: 'starttime', + width: 100, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("End Time"), + dataIndex: 'endtime', + width: 100, + renderer: function(value, metaData, record) { + return Ext.Date.format(value,"M d H:i:s"); + } + }, + { + header: gettext("Node"), + dataIndex: 'node', + width: 100 + }, + { + header: gettext("User name"), + dataIndex: 'user', + width: 150 + }, + { + header: gettext("Description"), + dataIndex: 'upid', + flex: 1, + renderer: Proxmox.Utils.render_upid + }, + { + header: gettext("Status"), + dataIndex: 'status', + width: 200, + renderer: function(value, metaData, record) { + if (value == 'OK') { + return 'OK'; + } + // metaData.attr = 'style="color:red;"'; + return "ERROR: " + value; + } + } + ], + listeners: { + itemdblclick: run_task_viewer, + selectionchange: function(v, selections) { + view_btn.setDisabled(!(selections && selections[0])); + }, + show: function() { reload_task.delay(10); }, + destroy: function() { reload_task.cancel(); } + } + }); + + me.callParent(); + + } +}); +Ext.define('proxmox-services', { + extend: 'Ext.data.Model', + fields: [ 'service', 'name', 'desc', 'state' ], + idProperty: 'service' +}); + +Ext.define('Proxmox.node.ServiceView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.proxmoxNodeServiceView'], + + startOnlyServices: {}, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var rstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 1000, + storeid: 'proxmox-services' + me.nodename, + model: 'proxmox-services', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + "/services" + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: rstore, + sortAfterUpdate: true, + sorters: [ + { + property : 'name', + direction: 'ASC' + } + ] + }); + + var view_service_log = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + var win = Ext.create('Ext.window.Window', { + title: gettext('Syslog') + ': ' + rec.data.service, + modal: true, + width: 800, + height: 400, + layout: 'fit', + items: { + xtype: 'proxmoxLogView', + url: "/api2/extjs/nodes/" + me.nodename + "/syslog?service=" + + rec.data.service, + log_select_timespan: 1 + } + }); + win.show(); + }; + + var service_cmd = function(cmd) { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + Proxmox.Utils.API2Request({ + url: "/nodes/" + me.nodename + "/services/" + rec.data.service + "/" + cmd, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.loading = true; + }, + success: function(response, opts) { + rstore.startUpdate(); + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid + }); + win.show(); + } + }); + }; + + var start_btn = new Ext.Button({ + text: gettext('Start'), + disabled: true, + handler: function(){ + service_cmd("start"); + } + }); + + var stop_btn = new Ext.Button({ + text: gettext('Stop'), + disabled: true, + handler: function(){ + service_cmd("stop"); + } + }); + + var restart_btn = new Ext.Button({ + text: gettext('Restart'), + disabled: true, + handler: function(){ + service_cmd("restart"); + } + }); + + var syslog_btn = new Ext.Button({ + text: gettext('Syslog'), + disabled: true, + handler: view_service_log + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + start_btn.disable(); + stop_btn.disable(); + restart_btn.disable(); + syslog_btn.disable(); + return; + } + var service = rec.data.service; + var state = rec.data.state; + + syslog_btn.enable(); + + if (me.startOnlyServices[service]) { + if (state == 'running') { + start_btn.disable(); + restart_btn.enable(); + } else { + start_btn.enable(); + restart_btn.disable(); + } + stop_btn.disable(); + } else { + if (state == 'running') { + start_btn.disable(); + restart_btn.enable(); + stop_btn.enable(); + } else { + start_btn.enable(); + restart_btn.disable(); + stop_btn.disable(); + } + } + }; + + me.mon(store, 'refresh', set_button_status); + + Proxmox.Utils.monStoreErrors(me, rstore); + + Ext.apply(me, { + store: store, + stateful: false, + tbar: [ start_btn, stop_btn, restart_btn, syslog_btn ], + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + dataIndex: 'name' + }, + { + header: gettext('Status'), + width: 100, + sortable: true, + dataIndex: 'state' + }, + { + header: gettext('Description'), + renderer: Ext.String.htmlEncode, + dataIndex: 'desc', + flex: 2 + } + ], + listeners: { + selectionchange: set_button_status, + itemdblclick: view_service_log, + activate: rstore.startUpdate, + destroy: rstore.stopUpdate + } + }); + + me.callParent(); + } +}); +Ext.define('Proxmox.node.TimeEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.proxmoxNodeTimeEdit'], + + subject: gettext('Time zone'), + + width: 400, + + autoLoad: true, + + fieldDefaults: { + labelWidth: 70 + }, + + items: { + xtype: 'combo', + fieldLabel: gettext('Time zone'), + name: 'timezone', + queryMode: 'local', + store: Ext.create('Proxmox.data.TimezoneStore'), + displayField: 'zone', + editable: true, + anyMatch: true, + forceSelection: true, + allowBlank: false + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + me.url = "/api2/extjs/nodes/" + me.nodename + "/time"; + + me.callParent(); + } +}); +Ext.define('Proxmox.node.TimeView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.proxmoxNodeTimeView'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var tzoffset = (new Date()).getTimezoneOffset()*60000; + var renderlocaltime = function(value) { + var servertime = new Date((value * 1000) + tzoffset); + return Ext.Date.format(servertime, 'Y-m-d H:i:s'); + }; + + var run_editor = function() { + var win = Ext.create('Proxmox.node.TimeEdit', { + nodename: me.nodename + }); + win.show(); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + me.nodename + "/time", + cwidth1: 150, + interval: 1000, + run_editor: run_editor, + rows: { + timezone: { + header: gettext('Time zone'), + required: true + }, + localtime: { + header: gettext('Server time'), + required: true, + renderer: renderlocaltime + } + }, + tbar: [ + { + text: gettext("Edit"), + handler: run_editor + } + ], + listeners: { + itemdblclick: run_editor + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('deactivate', me.rstore.stopUpdate); + me.on('destroy', me.rstore.stopUpdate); + } +}); diff --git a/serverside/jsmod/6.0-4/proxmoxlib.js.original b/serverside/jsmod/6.0-4/proxmoxlib.js.original new file mode 100644 index 0000000..e4e71b7 --- /dev/null +++ b/serverside/jsmod/6.0-4/proxmoxlib.js.original @@ -0,0 +1,7357 @@ +// 2.0-5 +Ext.ns('Proxmox'); +Ext.ns('Proxmox.Setup'); + +if (!Ext.isDefined(Proxmox.Setup.auth_cookie_name)) { + throw "Proxmox library not initialized"; +} + +// avoid errors related to Accessible Rich Internet Applications +// (access for people with disabilities) +// TODO reenable after all components are upgraded +Ext.enableAria = false; +Ext.enableAriaButtons = false; +Ext.enableAriaPanels = false; + +// avoid errors when running without development tools +if (!Ext.isDefined(Ext.global.console)) { + var console = { + dir: function() {}, + log: function() {} + }; +} + +Ext.Ajax.defaultHeaders = { + 'Accept': 'application/json' +}; + +Ext.Ajax.on('beforerequest', function(conn, options) { + if (Proxmox.CSRFPreventionToken) { + if (!options.headers) { + options.headers = {}; + } + options.headers.CSRFPreventionToken = Proxmox.CSRFPreventionToken; + } +}); + +Ext.define('Proxmox.Utils', { utilities: { + + // this singleton contains miscellaneous utilities + + yesText: gettext('Yes'), + noText: gettext('No'), + enabledText: gettext('Enabled'), + disabledText: gettext('Disabled'), + noneText: gettext('none'), + errorText: gettext('Error'), + unknownText: gettext('Unknown'), + defaultText: gettext('Default'), + daysText: gettext('days'), + dayText: gettext('day'), + runningText: gettext('running'), + stoppedText: gettext('stopped'), + neverText: gettext('never'), + totalText: gettext('Total'), + usedText: gettext('Used'), + directoryText: gettext('Directory'), + stateText: gettext('State'), + groupText: gettext('Group'), + + language_map: { + zh_CN: 'Chinese (Simplified)', + zh_TW: 'Chinese (Traditional)', + ca: 'Catalan', + da: 'Danish', + en: 'English', + eu: 'Euskera (Basque)', + fr: 'French', + de: 'German', + it: 'Italian', + es: 'Spanish', + ja: 'Japanese', + nb: 'Norwegian (Bokmal)', + nn: 'Norwegian (Nynorsk)', + fa: 'Persian (Farsi)', + pl: 'Polish', + pt_BR: 'Portuguese (Brazil)', + ru: 'Russian', + sl: 'Slovenian', + sv: 'Swedish', + tr: 'Turkish' + }, + + render_language: function (value) { + if (!value) { + return Proxmox.Utils.defaultText + ' (English)'; + } + var text = Proxmox.Utils.language_map[value]; + if (text) { + return text + ' (' + value + ')'; + } + return value; + }, + + language_array: function() { + var data = [['__default__', Proxmox.Utils.render_language('')]]; + Ext.Object.each(Proxmox.Utils.language_map, function(key, value) { + data.push([key, Proxmox.Utils.render_language(value)]); + }); + + return data; + }, + + bond_mode_gettext_map: { + '802.3ad': 'LACP (802.3ad)', + 'lacp-balance-slb': 'LACP (balance-slb)', + 'lacp-balance-tcp': 'LACP (balance-tcp)', + }, + + render_bond_mode: value => Proxmox.Utils.bond_mode_gettext_map[value] || value || '', + + bond_mode_array: function(modes) { + return modes.map(mode => [mode, Proxmox.Utils.render_bond_mode(mode)]); + }, + + getNoSubKeyHtml: function(url) { + // url http://www.proxmox.com/products/proxmox-ve/subscription-service-plans + return Ext.String.format('You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.', url || 'https://www.proxmox.com'); + }, + + format_boolean_with_default: function(value) { + if (Ext.isDefined(value) && value !== '__default__') { + return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText; + } + return Proxmox.Utils.defaultText; + }, + + format_boolean: function(value) { + return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText; + }, + + format_neg_boolean: function(value) { + return !value ? Proxmox.Utils.yesText : Proxmox.Utils.noText; + }, + + format_enabled_toggle: function(value) { + return value ? Proxmox.Utils.enabledText : Proxmox.Utils.disabledText; + }, + + format_expire: function(date) { + if (!date) { + return Proxmox.Utils.neverText; + } + return Ext.Date.format(date, "Y-m-d"); + }, + + format_duration_long: function(ut) { + + var days = Math.floor(ut / 86400); + ut -= days*86400; + var hours = Math.floor(ut / 3600); + ut -= hours*3600; + var mins = Math.floor(ut / 60); + ut -= mins*60; + + var hours_str = '00' + hours.toString(); + hours_str = hours_str.substr(hours_str.length - 2); + var mins_str = "00" + mins.toString(); + mins_str = mins_str.substr(mins_str.length - 2); + var ut_str = "00" + ut.toString(); + ut_str = ut_str.substr(ut_str.length - 2); + + if (days) { + var ds = days > 1 ? Proxmox.Utils.daysText : Proxmox.Utils.dayText; + return days.toString() + ' ' + ds + ' ' + + hours_str + ':' + mins_str + ':' + ut_str; + } else { + return hours_str + ':' + mins_str + ':' + ut_str; + } + }, + + format_subscription_level: function(level) { + if (level === 'c') { + return 'Community'; + } else if (level === 'b') { + return 'Basic'; + } else if (level === 's') { + return 'Standard'; + } else if (level === 'p') { + return 'Premium'; + } else { + return Proxmox.Utils.noneText; + } + }, + + compute_min_label_width: function(text, width) { + + if (width === undefined) { width = 100; } + + var tm = new Ext.util.TextMetrics(); + var min = tm.getWidth(text + ':'); + + return min < width ? width : min; + }, + + setAuthData: function(data) { + Proxmox.CSRFPreventionToken = data.CSRFPreventionToken; + Proxmox.UserName = data.username; + Proxmox.LoggedOut = data.LoggedOut; + // creates a session cookie (expire = null) + // that way the cookie gets deleted after the browser window is closed + Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true); + }, + + authOK: function() { + if (Proxmox.LoggedOut) { + return undefined; + } + return (Proxmox.UserName !== '') && Ext.util.Cookies.get(Proxmox.Setup.auth_cookie_name); + }, + + authClear: function() { + if (Proxmox.LoggedOut) { + return undefined; + } + Ext.util.Cookies.clear(Proxmox.Setup.auth_cookie_name); + }, + + // comp.setLoading() is buggy in ExtJS 4.0.7, so we + // use el.mask() instead + setErrorMask: function(comp, msg) { + var el = comp.el; + if (!el) { + return; + } + if (!msg) { + el.unmask(); + } else { + if (msg === true) { + el.mask(gettext("Loading...")); + } else { + el.mask(msg); + } + } + }, + + monStoreErrors: function(me, store, clearMaskBeforeLoad) { + if (clearMaskBeforeLoad) { + me.mon(store, 'beforeload', function(s, operation, eOpts) { + Proxmox.Utils.setErrorMask(me, false); + }); + } else { + me.mon(store, 'beforeload', function(s, operation, eOpts) { + if (!me.loadCount) { + me.loadCount = 0; // make sure it is numeric + Proxmox.Utils.setErrorMask(me, true); + } + }); + } + + // only works with 'proxmox' proxy + me.mon(store.proxy, 'afterload', function(proxy, request, success) { + me.loadCount++; + + if (success) { + Proxmox.Utils.setErrorMask(me, false); + return; + } + + var msg; + /*jslint nomen: true */ + var operation = request._operation; + var error = operation.getError(); + if (error.statusText) { + msg = error.statusText + ' (' + error.status + ')'; + } else { + msg = gettext('Connection error'); + } + Proxmox.Utils.setErrorMask(me, msg); + }); + }, + + extractRequestError: function(result, verbose) { + var msg = gettext('Successful'); + + if (!result.success) { + msg = gettext("Unknown error"); + if (result.message) { + msg = result.message; + if (result.status) { + msg += ' (' + result.status + ')'; + } + } + if (verbose && Ext.isObject(result.errors)) { + msg += "
"; + Ext.Object.each(result.errors, function(prop, desc) { + msg += "
" + Ext.htmlEncode(prop) + ": " + + Ext.htmlEncode(desc); + }); + } + } + + return msg; + }, + + // Ext.Ajax.request + API2Request: function(reqOpts) { + + var newopts = Ext.apply({ + waitMsg: gettext('Please wait...') + }, reqOpts); + + if (!newopts.url.match(/^\/api2/)) { + newopts.url = '/api2/extjs' + newopts.url; + } + delete newopts.callback; + + var createWrapper = function(successFn, callbackFn, failureFn) { + Ext.apply(newopts, { + success: function(response, options) { + if (options.waitMsgTarget) { + if (Proxmox.Utils.toolkit === 'touch') { + options.waitMsgTarget.setMasked(false); + } else { + options.waitMsgTarget.setLoading(false); + } + } + var result = Ext.decode(response.responseText); + response.result = result; + if (!result.success) { + response.htmlStatus = Proxmox.Utils.extractRequestError(result, true); + Ext.callback(callbackFn, options.scope, [options, false, response]); + Ext.callback(failureFn, options.scope, [response, options]); + return; + } + Ext.callback(callbackFn, options.scope, [options, true, response]); + Ext.callback(successFn, options.scope, [response, options]); + }, + failure: function(response, options) { + if (options.waitMsgTarget) { + if (Proxmox.Utils.toolkit === 'touch') { + options.waitMsgTarget.setMasked(false); + } else { + options.waitMsgTarget.setLoading(false); + } + } + response.result = {}; + try { + response.result = Ext.decode(response.responseText); + } catch(e) {} + var msg = gettext('Connection error') + ' - server offline?'; + if (response.aborted) { + msg = gettext('Connection error') + ' - aborted.'; + } else if (response.timedout) { + msg = gettext('Connection error') + ' - Timeout.'; + } else if (response.status && response.statusText) { + msg = gettext('Connection error') + ' ' + response.status + ': ' + response.statusText; + } + response.htmlStatus = msg; + Ext.callback(callbackFn, options.scope, [options, false, response]); + Ext.callback(failureFn, options.scope, [response, options]); + } + }); + }; + + createWrapper(reqOpts.success, reqOpts.callback, reqOpts.failure); + + var target = newopts.waitMsgTarget; + if (target) { + if (Proxmox.Utils.toolkit === 'touch') { + target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg} ); + } else { + // Note: ExtJS bug - this does not work when component is not rendered + target.setLoading(newopts.waitMsg); + } + } + Ext.Ajax.request(newopts); + }, + + checked_command: function(orig_cmd) { + Proxmox.Utils.API2Request({ + url: '/nodes/localhost/subscription', + method: 'GET', + //waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + var data = response.result.data; + + if (data.status !== 'Active') { + Ext.Msg.show({ + title: gettext('No valid subscription'), + icon: Ext.Msg.WARNING, + msg: Proxmox.Utils.getNoSubKeyHtml(data.url), + buttons: Ext.Msg.OK, + callback: function(btn) { + if (btn !== 'ok') { + return; + } + orig_cmd(); + } + }); + } else { + orig_cmd(); + } + } + }); + }, + + assemble_field_data: function(values, data) { + if (Ext.isObject(data)) { + Ext.Object.each(data, function(name, val) { + if (values.hasOwnProperty(name)) { + var bucket = values[name]; + if (!Ext.isArray(bucket)) { + bucket = values[name] = [bucket]; + } + if (Ext.isArray(val)) { + values[name] = bucket.concat(val); + } else { + bucket.push(val); + } + } else { + values[name] = val; + } + }); + } + }, + + dialog_title: function(subject, create, isAdd) { + if (create) { + if (isAdd) { + return gettext('Add') + ': ' + subject; + } else { + return gettext('Create') + ': ' + subject; + } + } else { + return gettext('Edit') + ': ' + subject; + } + }, + + network_iface_types: { + eth: gettext("Network Device"), + bridge: 'Linux Bridge', + bond: 'Linux Bond', + vlan: 'Linux VLAN', + OVSBridge: 'OVS Bridge', + OVSBond: 'OVS Bond', + OVSPort: 'OVS Port', + OVSIntPort: 'OVS IntPort' + }, + + render_network_iface_type: function(value) { + return Proxmox.Utils.network_iface_types[value] || + Proxmox.Utils.unknownText; + }, + + task_desc_table: { + acmenewcert: [ 'SRV', gettext('Order Certificate') ], + acmeregister: [ 'ACME Account', gettext('Register') ], + acmedeactivate: [ 'ACME Account', gettext('Deactivate') ], + acmeupdate: [ 'ACME Account', gettext('Update') ], + acmerefresh: [ 'ACME Account', gettext('Refresh') ], + acmerenew: [ 'SRV', gettext('Renew Certificate') ], + acmerevoke: [ 'SRV', gettext('Revoke Certificate') ], + 'move_volume': [ 'CT', gettext('Move Volume') ], + clustercreate: [ '', gettext('Create Cluster') ], + clusterjoin: [ '', gettext('Join Cluster') ], + diskinit: [ 'Disk', gettext('Initialize Disk with GPT') ], + vncproxy: [ 'VM/CT', gettext('Console') ], + spiceproxy: [ 'VM/CT', gettext('Console') + ' (Spice)' ], + vncshell: [ '', gettext('Shell') ], + spiceshell: [ '', gettext('Shell') + ' (Spice)' ], + qmsnapshot: [ 'VM', gettext('Snapshot') ], + qmrollback: [ 'VM', gettext('Rollback') ], + qmdelsnapshot: [ 'VM', gettext('Delete Snapshot') ], + qmcreate: [ 'VM', gettext('Create') ], + qmrestore: [ 'VM', gettext('Restore') ], + qmdestroy: [ 'VM', gettext('Destroy') ], + qmigrate: [ 'VM', gettext('Migrate') ], + qmclone: [ 'VM', gettext('Clone') ], + qmmove: [ 'VM', gettext('Move disk') ], + qmtemplate: [ 'VM', gettext('Convert to template') ], + qmstart: [ 'VM', gettext('Start') ], + qmstop: [ 'VM', gettext('Stop') ], + qmreset: [ 'VM', gettext('Reset') ], + qmshutdown: [ 'VM', gettext('Shutdown') ], + qmsuspend: [ 'VM', gettext('Hibernate') ], + qmpause: [ 'VM', gettext('Pause') ], + qmresume: [ 'VM', gettext('Resume') ], + qmconfig: [ 'VM', gettext('Configure') ], + vzsnapshot: [ 'CT', gettext('Snapshot') ], + vzrollback: [ 'CT', gettext('Rollback') ], + vzdelsnapshot: [ 'CT', gettext('Delete Snapshot') ], + vzcreate: ['CT', gettext('Create') ], + vzrestore: ['CT', gettext('Restore') ], + vzdestroy: ['CT', gettext('Destroy') ], + vzmigrate: [ 'CT', gettext('Migrate') ], + vzclone: [ 'CT', gettext('Clone') ], + vztemplate: [ 'CT', gettext('Convert to template') ], + vzstart: ['CT', gettext('Start') ], + vzstop: ['CT', gettext('Stop') ], + vzmount: ['CT', gettext('Mount') ], + vzumount: ['CT', gettext('Unmount') ], + vzshutdown: ['CT', gettext('Shutdown') ], + vzsuspend: [ 'CT', gettext('Suspend') ], + vzresume: [ 'CT', gettext('Resume') ], + hamigrate: [ 'HA', gettext('Migrate') ], + hastart: [ 'HA', gettext('Start') ], + hastop: [ 'HA', gettext('Stop') ], + srvstart: ['SRV', gettext('Start') ], + srvstop: ['SRV', gettext('Stop') ], + srvrestart: ['SRV', gettext('Restart') ], + srvreload: ['SRV', gettext('Reload') ], + cephcreatemgr: ['Ceph Manager', gettext('Create') ], + cephdestroymgr: ['Ceph Manager', gettext('Destroy') ], + cephcreatemon: ['Ceph Monitor', gettext('Create') ], + cephdestroymon: ['Ceph Monitor', gettext('Destroy') ], + cephcreateosd: ['Ceph OSD', gettext('Create') ], + cephdestroyosd: ['Ceph OSD', gettext('Destroy') ], + cephcreatepool: ['Ceph Pool', gettext('Create') ], + cephdestroypool: ['Ceph Pool', gettext('Destroy') ], + cephfscreate: ['CephFS', gettext('Create') ], + cephcreatemds: ['Ceph Metadata Server', gettext('Create') ], + cephdestroymds: ['Ceph Metadata Server', gettext('Destroy') ], + imgcopy: ['', gettext('Copy data') ], + imgdel: ['', gettext('Erase data') ], + unknownimgdel: ['', gettext('Destroy image from unknown guest') ], + download: ['', gettext('Download') ], + vzdump: ['VM/CT', gettext('Backup') ], + aptupdate: ['', gettext('Update package database') ], + startall: [ '', gettext('Start all VMs and Containers') ], + stopall: [ '', gettext('Stop all VMs and Containers') ], + migrateall: [ '', gettext('Migrate all VMs and Containers') ], + dircreate: [ gettext('Directory Storage'), gettext('Create') ], + lvmcreate: [ gettext('LVM Storage'), gettext('Create') ], + lvmthincreate: [ gettext('LVM-Thin Storage'), gettext('Create') ], + zfscreate: [ gettext('ZFS Storage'), gettext('Create') ] + }, + + format_task_description: function(type, id) { + var farray = Proxmox.Utils.task_desc_table[type]; + var text; + if (!farray) { + text = type; + if (id) { + type += ' ' + id; + } + return text; + } + var prefix = farray[0]; + text = farray[1]; + if (prefix) { + return prefix + ' ' + id + ' - ' + text; + } + return text; + }, + + format_size: function(size) { + /*jslint confusion: true */ + + var units = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi']; + var num = 0; + + while (size >= 1024 && ((num++)+1) < units.length) { + size = size / 1024; + } + + return size.toFixed((num > 0)?2:0) + " " + units[num] + "B"; + }, + + render_upid: function(value, metaData, record) { + var type = record.data.type; + var id = record.data.id; + + return Proxmox.Utils.format_task_description(type, id); + }, + + render_uptime: function(value) { + + var uptime = value; + + if (uptime === undefined) { + return ''; + } + + if (uptime <= 0) { + return '-'; + } + + return Proxmox.Utils.format_duration_long(uptime); + }, + + parse_task_upid: function(upid) { + var task = {}; + + var res = upid.match(/^UPID:(\S+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/); + if (!res) { + throw "unable to parse upid '" + upid + "'"; + } + task.node = res[1]; + task.pid = parseInt(res[2], 16); + task.pstart = parseInt(res[3], 16); + task.starttime = parseInt(res[4], 16); + task.type = res[5]; + task.id = res[6]; + task.user = res[7]; + + task.desc = Proxmox.Utils.format_task_description(task.type, task.id); + + return task; + }, + + render_timestamp: function(value, metaData, record, rowIndex, colIndex, store) { + var servertime = new Date(value * 1000); + return Ext.Date.format(servertime, 'Y-m-d H:i:s'); + }, + + get_help_info: function(section) { + var helpMap; + if (typeof proxmoxOnlineHelpInfo !== 'undefined') { + helpMap = proxmoxOnlineHelpInfo; + } else if (typeof pveOnlineHelpInfo !== 'undefined') { + // be backward compatible with older pve-doc-generators + helpMap = pveOnlineHelpInfo; + } else { + throw "no global OnlineHelpInfo map declared"; + } + + return helpMap[section]; + }, + + get_help_link: function(section) { + var info = Proxmox.Utils.get_help_info(section); + if (!info) { + return; + } + + return window.location.origin + info.link; + }, + + openXtermJsViewer: function(vmtype, vmid, nodename, vmname, cmd) { + var url = Ext.Object.toQueryString({ + console: vmtype, // kvm, lxc, upgrade or shell + xtermjs: 1, + vmid: vmid, + vmname: vmname, + node: nodename, + cmd: cmd, + + }); + var nw = window.open("?" + url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,resizable=yes,width=800,height=420'); + if (nw) { + nw.focus(); + } + } + +}, + + singleton: true, + constructor: function() { + var me = this; + Ext.apply(me, me.utilities); + + var IPV4_OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])"; + var IPV4_REGEXP = "(?:(?:" + IPV4_OCTET + "\\.){3}" + IPV4_OCTET + ")"; + var IPV6_H16 = "(?:[0-9a-fA-F]{1,4})"; + var IPV6_LS32 = "(?:(?:" + IPV6_H16 + ":" + IPV6_H16 + ")|" + IPV4_REGEXP + ")"; + var IPV4_CIDR_MASK = "([0-9]{1,2})"; + var IPV6_CIDR_MASK = "([0-9]{1,3})"; + + + me.IP4_match = new RegExp("^(?:" + IPV4_REGEXP + ")$"); + me.IP4_cidr_match = new RegExp("^(?:" + IPV4_REGEXP + ")\/" + IPV4_CIDR_MASK + "$"); + + var IPV6_REGEXP = "(?:" + + "(?:(?:" + "(?:" + IPV6_H16 + ":){6})" + IPV6_LS32 + ")|" + + "(?:(?:" + "::" + "(?:" + IPV6_H16 + ":){5})" + IPV6_LS32 + ")|" + + "(?:(?:(?:" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){4})" + IPV6_LS32 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,1}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){3})" + IPV6_LS32 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,2}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){2})" + IPV6_LS32 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,3}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){1})" + IPV6_LS32 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,4}" + IPV6_H16 + ")?::" + ")" + IPV6_LS32 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,5}" + IPV6_H16 + ")?::" + ")" + IPV6_H16 + ")|" + + "(?:(?:(?:(?:" + IPV6_H16 + ":){0,7}" + IPV6_H16 + ")?::" + ")" + ")" + + ")"; + + me.IP6_match = new RegExp("^(?:" + IPV6_REGEXP + ")$"); + me.IP6_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + ")\/" + IPV6_CIDR_MASK + "$"); + me.IP6_bracket_match = new RegExp("^\\[(" + IPV6_REGEXP + ")\\]"); + + me.IP64_match = new RegExp("^(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + ")$"); + me.IP64_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + "\/" + IPV6_CIDR_MASK + ")|(?:" + IPV4_REGEXP + "\/" + IPV4_CIDR_MASK + ")$"); + + var DnsName_REGEXP = "(?:(([a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)\\.)*([A-Za-z0-9]([A-Za-z0-9\\-]*[A-Za-z0-9])?))"; + me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$"); + + me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(:\\d+)?$"); + me.HostPortBrackets_match = new RegExp("^\\[(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](:\\d+)?$"); + me.IP6_dotnotation_match = new RegExp("^" + IPV6_REGEXP + "(\\.\\d+)?$"); + } +}); +// ExtJS related things + + // do not send '_dc' parameter +Ext.Ajax.disableCaching = false; + +// custom Vtypes +Ext.apply(Ext.form.field.VTypes, { + IPAddress: function(v) { + return Proxmox.Utils.IP4_match.test(v); + }, + IPAddressText: gettext('Example') + ': 192.168.1.1', + IPAddressMask: /[\d\.]/i, + + IPCIDRAddress: function(v) { + var result = Proxmox.Utils.IP4_cidr_match.exec(v); + // limits according to JSON Schema see + // pve-common/src/PVE/JSONSchema.pm + return (result !== null && result[1] >= 8 && result[1] <= 32); + }, + IPCIDRAddressText: gettext('Example') + ': 192.168.1.1/24' + "
" + gettext('Valid CIDR Range') + ': 8-32', + IPCIDRAddressMask: /[\d\.\/]/i, + + IP6Address: function(v) { + return Proxmox.Utils.IP6_match.test(v); + }, + IP6AddressText: gettext('Example') + ': 2001:DB8::42', + IP6AddressMask: /[A-Fa-f0-9:]/, + + IP6CIDRAddress: function(v) { + var result = Proxmox.Utils.IP6_cidr_match.exec(v); + // limits according to JSON Schema see + // pve-common/src/PVE/JSONSchema.pm + return (result !== null && result[1] >= 8 && result[1] <= 128); + }, + IP6CIDRAddressText: gettext('Example') + ': 2001:DB8::42/64' + "
" + gettext('Valid CIDR Range') + ': 8-128', + IP6CIDRAddressMask: /[A-Fa-f0-9:\/]/, + + IP6PrefixLength: function(v) { + return v >= 0 && v <= 128; + }, + IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128', + IP6PrefixLengthMask: /[0-9]/, + + IP64Address: function(v) { + return Proxmox.Utils.IP64_match.test(v); + }, + IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42', + IP64AddressMask: /[A-Fa-f0-9\.:]/, + + IP64CIDRAddress: function(v) { + var result = Proxmox.Utils.IP64_cidr_match.exec(v); + if (result === null) { + return false; + } + if (result[1] !== undefined) { + return result[1] >= 8 && result[1] <= 128; + } else if (result[2] !== undefined) { + return result[2] >= 8 && result[2] <= 32; + } else { + return false; + } + }, + IP64CIDRAddressText: gettext('Example') + ': 192.168.1.1/24 2001:DB8::42/64', + IP64CIDRAddressMask: /[A-Fa-f0-9\.:\/]/, + + MacAddress: function(v) { + return (/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/).test(v); + }, + MacAddressMask: /[a-fA-F0-9:]/, + MacAddressText: gettext('Example') + ': 01:23:45:67:89:ab', + + MacPrefix: function(v) { + return (/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i).test(v); + }, + MacPrefixMask: /[a-fA-F0-9:]/, + MacPrefixText: gettext('Example') + ': 02:8f - ' + gettext('only unicast addresses are allowed'), + + BridgeName: function(v) { + return (/^vmbr\d{1,4}$/).test(v); + }, + BridgeNameText: gettext('Format') + ': vmbrN, where 0 <= N <= 9999', + + BondName: function(v) { + return (/^bond\d{1,4}$/).test(v); + }, + BondNameText: gettext('Format') + ': bondN, where 0 <= N <= 9999', + + InterfaceName: function(v) { + return (/^[a-z][a-z0-9_]{1,20}$/).test(v); + }, + InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'" + "
" + + gettext("Minimum characters") + ": 2" + "
" + + gettext("Maximum characters") + ": 21" + "
" + + gettext("Must start with") + ": 'a-z'", + + StorageId: function(v) { + return (/^[a-z][a-z0-9\-\_\.]*[a-z0-9]$/i).test(v); + }, + StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'" + "
" + + gettext("Minimum characters") + ": 2" + "
" + + gettext("Must start with") + ": 'A-Z', 'a-z'
" + + gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'
", + + ConfigId: function(v) { + return (/^[a-z][a-z0-9\_]+$/i).test(v); + }, + ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'" + "
" + + gettext("Minimum characters") + ": 2" + "
" + + gettext("Must start with") + ": " + gettext("letter"), + + HttpProxy: function(v) { + return (/^http:\/\/.*$/).test(v); + }, + HttpProxyText: gettext('Example') + ": http://username:password@host:port/", + + DnsName: function(v) { + return Proxmox.Utils.DnsName_match.test(v); + }, + DnsNameText: gettext('This is not a valid DNS name'), + + // workaround for https://www.sencha.com/forum/showthread.php?302150 + proxmoxMail: function(v) { + return (/^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,63}$/).test(v); + }, + proxmoxMailText: gettext('Example') + ": user@example.com", + + DnsOrIp: function(v) { + if (!Proxmox.Utils.DnsName_match.test(v) && + !Proxmox.Utils.IP64_match.test(v)) { + return false; + } + + return true; + }, + DnsOrIpText: gettext('Not a valid DNS name or IP address.'), + + HostList: function(v) { + var list = v.split(/[\ \,\;]+/); + var i; + for (i = 0; i < list.length; i++) { + if (list[i] == "") { + continue; + } + + if (!Proxmox.Utils.HostPort_match.test(list[i]) && + !Proxmox.Utils.HostPortBrackets_match.test(list[i]) && + !Proxmox.Utils.IP6_dotnotation_match.test(list[i])) { + return false; + } + } + + return true; + }, + HostListText: gettext('Not a valid list of hosts'), + + password: function(val, field) { + if (field.initialPassField) { + var pwd = field.up('form').down( + '[name=' + field.initialPassField + ']'); + return (val == pwd.getValue()); + } + return true; + }, + + passwordText: gettext('Passwords do not match') +}); + +// Firefox 52+ Touchscreen bug +// see https://www.sencha.com/forum/showthread.php?336762-Examples-don-t-work-in-Firefox-52-touchscreen/page2 +// and https://bugzilla.proxmox.com/show_bug.cgi?id=1223 +Ext.define('EXTJS_23846.Element', { + override: 'Ext.dom.Element' +}, function(Element) { + var supports = Ext.supports, + proto = Element.prototype, + eventMap = proto.eventMap, + additiveEvents = proto.additiveEvents; + + if (Ext.os.is.Desktop && supports.TouchEvents && !supports.PointerEvents) { + eventMap.touchstart = 'mousedown'; + eventMap.touchmove = 'mousemove'; + eventMap.touchend = 'mouseup'; + eventMap.touchcancel = 'mouseup'; + + additiveEvents.mousedown = 'mousedown'; + additiveEvents.mousemove = 'mousemove'; + additiveEvents.mouseup = 'mouseup'; + additiveEvents.touchstart = 'touchstart'; + additiveEvents.touchmove = 'touchmove'; + additiveEvents.touchend = 'touchend'; + additiveEvents.touchcancel = 'touchcancel'; + + additiveEvents.pointerdown = 'mousedown'; + additiveEvents.pointermove = 'mousemove'; + additiveEvents.pointerup = 'mouseup'; + additiveEvents.pointercancel = 'mouseup'; + } +}); + +Ext.define('EXTJS_23846.Gesture', { + override: 'Ext.event.publisher.Gesture' +}, function(Gesture) { + var me = Gesture.instance; + + if (Ext.supports.TouchEvents && !Ext.isWebKit && Ext.os.is.Desktop) { + me.handledDomEvents.push('mousedown', 'mousemove', 'mouseup'); + me.registerEvents(); + } +}); + +Ext.define('EXTJS_18900.Pie', { + override: 'Ext.chart.series.Pie', + + // from 6.0.2 + betweenAngle: function (x, a, b) { + var pp = Math.PI * 2, + offset = this.rotationOffset; + + if (a === b) { + return false; + } + + if (!this.getClockwise()) { + x *= -1; + a *= -1; + b *= -1; + a -= offset; + b -= offset; + } else { + a += offset; + b += offset; + } + + x -= a; + b -= a; + + // Normalize, so that both x and b are in the [0,360) interval. + x %= pp; + b %= pp; + x += pp; + b += pp; + x %= pp; + b %= pp; + + // Because 360 * n angles will be normalized to 0, + // we need to treat b === 0 as a special case. + return x < b || b === 0; + }, +}); + +// we always want the number in x.y format and never in, e.g., x,y +Ext.define('PVE.form.field.Number', { + override: 'Ext.form.field.Number', + submitLocaleSeparator: false +}); + +// ExtJs 5-6 has an issue with caching +// see https://www.sencha.com/forum/showthread.php?308989 +Ext.define('Proxmox.UnderlayPool', { + override: 'Ext.dom.UnderlayPool', + + checkOut: function () { + var cache = this.cache, + len = cache.length, + el; + + // do cleanup because some of the objects might have been destroyed + while (len--) { + if (cache[len].destroyed) { + cache.splice(len, 1); + } + } + // end do cleanup + + el = cache.shift(); + + if (!el) { + el = Ext.Element.create(this.elementConfig); + el.setVisibilityMode(2); + // + // tell the spec runner to ignore this element when checking if the dom is clean + el.dom.setAttribute('data-sticky', true); + // + } + + return el; + } +}); + +// 'Enter' in Textareas and aria multiline fields should not activate the +// defaultbutton, fixed in extjs 6.0.2 +Ext.define('PVE.panel.Panel', { + override: 'Ext.panel.Panel', + + fireDefaultButton: function(e) { + if (e.target.getAttribute('aria-multiline') === 'true' || + e.target.tagName === "TEXTAREA") { + return true; + } + return this.callParent(arguments); + } +}); + +// if the order of the values are not the same in originalValue and value +// extjs will not overwrite value, but marks the field dirty and thus +// the reset button will be enabled (but clicking it changes nothing) +// so if the arrays are not the same after resetting, we +// clear and set it +Ext.define('Proxmox.form.ComboBox', { + override: 'Ext.form.field.ComboBox', + + reset: function() { + // copied from combobox + var me = this; + me.callParent(); + + // clear and set when not the same + var value = me.getValue(); + if (Ext.isArray(me.originalValue) && Ext.isArray(value) && !Ext.Array.equals(value, me.originalValue)) { + me.clearValue(); + me.setValue(me.originalValue); + } + } +}); + +// when refreshing a grid/tree view, restoring the focus moves the view back to +// the previously focused item. Save scroll position before refocusing. +Ext.define(null, { + override: 'Ext.view.Table', + + jumpToFocus: false, + + saveFocusState: function() { + var me = this, + store = me.dataSource, + actionableMode = me.actionableMode, + navModel = me.getNavigationModel(), + focusPosition = actionableMode ? me.actionPosition : navModel.getPosition(true), + refocusRow, refocusCol; + + if (focusPosition) { + // Separate this from the instance that the nav model is using. + focusPosition = focusPosition.clone(); + + // Exit actionable mode. + // We must inform any Actionables that they must relinquish control. + // Tabbability must be reset. + if (actionableMode) { + me.ownerGrid.setActionableMode(false); + } + + // Blur the focused descendant, but do not trigger focusLeave. + me.el.dom.focus(); + + // Exiting actionable mode navigates to the owning cell, so in either focus mode we must + // clear the navigation position + navModel.setPosition(); + + // The following function will attempt to refocus back in the same mode to the same cell + // as it was at before based upon the previous record (if it's still inthe store), or the row index. + return function() { + // If we still have data, attempt to refocus in the same mode. + if (store.getCount()) { + + // Adjust expectations of where we are able to refocus according to what kind of destruction + // might have been wrought on this view's DOM during focus save. + refocusRow = Math.min(focusPosition.rowIdx, me.all.getCount() - 1); + refocusCol = Math.min(focusPosition.colIdx, me.getVisibleColumnManager().getColumns().length - 1); + focusPosition = new Ext.grid.CellContext(me).setPosition( + store.contains(focusPosition.record) ? focusPosition.record : refocusRow, refocusCol); + + if (actionableMode) { + me.ownerGrid.setActionableMode(true, focusPosition); + } else { + me.cellFocused = true; + + // we sometimes want to scroll back to where we were + var x = me.getScrollX(); + var y = me.getScrollY(); + + // Pass "preventNavigation" as true so that that does not cause selection. + navModel.setPosition(focusPosition, null, null, null, true); + + if (!me.jumpToFocus) { + me.scrollTo(x,y); + } + } + } + // No rows - focus associated column header + else { + focusPosition.column.focus(); + } + }; + } + return Ext.emptyFn; + } +}); + +// should be fixed with ExtJS 6.0.2, see: +// https://www.sencha.com/forum/showthread.php?307244-Bug-with-datefield-in-window-with-scroll +Ext.define('Proxmox.Datepicker', { + override: 'Ext.picker.Date', + hideMode: 'visibility' +}); + +// ExtJS 6.0.1 has no setSubmitValue() (although you find it in the docs). +// Note: this.submitValue is a boolean flag, whereas getSubmitValue() returns +// data to be submitted. +Ext.define('Proxmox.form.field.Text', { + override: 'Ext.form.field.Text', + + setSubmitValue: function(v) { + this.submitValue = v; + }, +}); + +// this should be fixed with ExtJS 6.0.2 +// make mousescrolling work in firefox in the containers overflowhandler +Ext.define(null, { + override: 'Ext.layout.container.boxOverflow.Scroller', + + createWheelListener: function() { + var me = this; + if (Ext.isFirefox) { + me.wheelListener = me.layout.innerCt.on('wheel', me.onMouseWheelFirefox, me, {destroyable: true}); + } else { + me.wheelListener = me.layout.innerCt.on('mousewheel', me.onMouseWheel, me, {destroyable: true}); + } + }, + + // special wheel handler for firefox. differs from the default onMouseWheel + // handler by using deltaY instead of wheelDeltaY and no normalizing, + // because it is already + onMouseWheelFirefox: function(e) { + e.stopEvent(); + var delta = e.browserEvent.deltaY || 0; + this.scrollBy(delta * this.wheelIncrement, false); + } + +}); + +// add '@' to the valid id +Ext.define('Proxmox.validIdReOverride', { + override: 'Ext.Component', + validIdRe: /^[a-z_][a-z0-9\-_\@]*$/i, +}); + +// force alert boxes to be rendered with an Error Icon +// since Ext.Msg is an object and not a prototype, we need to override it +// after the framework has been initiated +Ext.onReady(function() { +/*jslint confusion: true */ + Ext.override(Ext.Msg, { + alert: function(title, message, fn, scope) { + if (Ext.isString(title)) { + var config = { + title: title, + message: message, + icon: this.ERROR, + buttons: this.OK, + fn: fn, + scope : scope, + minWidth: this.minWidth + }; + return this.show(config); + } + } + }); +/*jslint confusion: false */ +}); +Ext.define('Ext.ux.IFrame', { + extend: 'Ext.Component', + + alias: 'widget.uxiframe', + + loadMask: 'Loading...', + + src: 'about:blank', + + renderTpl: [ + '' + ], + childEls: ['iframeEl'], + + initComponent: function () { + this.callParent(); + + this.frameName = this.frameName || this.id + '-frame'; + }, + + initEvents : function() { + var me = this; + me.callParent(); + me.iframeEl.on('load', me.onLoad, me); + }, + + initRenderData: function() { + return Ext.apply(this.callParent(), { + src: this.src, + frameName: this.frameName + }); + }, + + getBody: function() { + var doc = this.getDoc(); + return doc.body || doc.documentElement; + }, + + getDoc: function() { + try { + return this.getWin().document; + } catch (ex) { + return null; + } + }, + + getWin: function() { + var me = this, + name = me.frameName, + win = Ext.isIE + ? me.iframeEl.dom.contentWindow + : window.frames[name]; + return win; + }, + + getFrame: function() { + var me = this; + return me.iframeEl.dom; + }, + + beforeDestroy: function () { + this.cleanupListeners(true); + this.callParent(); + }, + + cleanupListeners: function(destroying){ + var doc, prop; + + if (this.rendered) { + try { + doc = this.getDoc(); + if (doc) { + /*jslint nomen: true*/ + Ext.get(doc).un(this._docListeners); + /*jslint nomen: false*/ + if (destroying && doc.hasOwnProperty) { + for (prop in doc) { + if (doc.hasOwnProperty(prop)) { + delete doc[prop]; + } + } + } + } + } catch(e) { } + } + }, + + onLoad: function() { + var me = this, + doc = me.getDoc(), + fn = me.onRelayedEvent; + + if (doc) { + try { + // These events need to be relayed from the inner document (where they stop + // bubbling) up to the outer document. This has to be done at the DOM level so + // the event reaches listeners on elements like the document body. The effected + // mechanisms that depend on this bubbling behavior are listed to the right + // of the event. + /*jslint nomen: true*/ + Ext.get(doc).on( + me._docListeners = { + mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront) + mousemove: fn, // window resize drag detection + mouseup: fn, // window resize termination + click: fn, // not sure, but just to be safe + dblclick: fn, // not sure again + scope: me + } + ); + /*jslint nomen: false*/ + } catch(e) { + // cannot do this xss + } + + // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK! + Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me); + + this.el.unmask(); + this.fireEvent('load', this); + + } else if (me.src) { + + this.el.unmask(); + this.fireEvent('error', this); + } + + + }, + + onRelayedEvent: function (event) { + // relay event from the iframe's document to the document that owns the iframe... + + var iframeEl = this.iframeEl, + + // Get the left-based iframe position + iframeXY = iframeEl.getTrueXY(), + originalEventXY = event.getXY(), + + // Get the left-based XY position. + // This is because the consumer of the injected event will + // perform its own RTL normalization. + eventXY = event.getTrueXY(); + + // the event from the inner document has XY relative to that document's origin, + // so adjust it to use the origin of the iframe in the outer document: + event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]]; + + event.injectEvent(iframeEl); // blame the iframe for the event... + + event.xy = originalEventXY; // restore the original XY (just for safety) + }, + + load: function (src) { + var me = this, + text = me.loadMask, + frame = me.getFrame(); + + if (me.fireEvent('beforeload', me, src) !== false) { + if (text && me.el) { + me.el.mask(text); + } + + frame.src = me.src = (src || me.src); + } + } +}); +Ext.define('Proxmox.Mixin.CBind', { + extend: 'Ext.Mixin', + + mixinConfig: { + before: { + initComponent: 'cloneTemplates' + } + }, + + cloneTemplates: function() { + var me = this; + + if (typeof(me.cbindData) == "function") { + me.cbindData = me.cbindData(me.initialConfig) || {}; + } + + var getConfigValue = function(cname) { + + if (cname in me.initialConfig) { + return me.initialConfig[cname]; + } + if (cname in me.cbindData) { + return me.cbindData[cname]; + } + if (cname in me) { + return me[cname]; + } + throw "unable to get cbind data for '" + cname + "'"; + }; + + var applyCBind = function(obj) { + var cbind = obj.cbind, prop, cdata, cvalue, match, found; + if (!cbind) return; + + for (prop in cbind) { + cdata = cbind[prop]; + + found = false; + if (match = /^\{(!)?([a-z_][a-z0-9_]*)\}$/i.exec(cdata)) { + var cvalue = getConfigValue(match[2]); + if (match[1]) cvalue = !cvalue; + obj[prop] = cvalue; + found = true; + } else if (match = /^\{(!)?([a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)+)\}$/i.exec(cdata)) { + var keys = match[2].split('.'); + var cvalue = getConfigValue(keys.shift()); + keys.forEach(function(k) { + if (k in cvalue) { + cvalue = cvalue[k]; + } else { + throw "unable to get cbind data for '" + match[2] + "'"; + } + }); + if (match[1]) cvalue = !cvalue; + obj[prop] = cvalue; + found = true; + } else { + obj[prop] = cdata.replace(/{([a-z_][a-z0-9_]*)\}/ig, function(match, cname) { + var cvalue = getConfigValue(cname); + found = true; + return cvalue; + }); + } + if (!found) { + throw "unable to parse cbind template '" + cdata + "'"; + } + + } + }; + + if (me.cbind) { + applyCBind(me); + } + + var cloneTemplateArray = function(org) { + var copy, i, found, el, elcopy, arrayLength; + + arrayLength = org.length; + found = false; + for (i = 0; i < arrayLength; i++) { + el = org[i]; + if (el.constructor == Object && el.xtype) { + found = true; + break; + } + } + + if (!found) return org; // no need to copy + + copy = []; + for (i = 0; i < arrayLength; i++) { + el = org[i]; + if (el.constructor == Object && el.xtype) { + elcopy = cloneTemplateObject(el); + if (elcopy.cbind) { + applyCBind(elcopy); + } + copy.push(elcopy); + } else if (el.constructor == Array) { + elcopy = cloneTemplateArray(el); + copy.push(elcopy); + } else { + copy.push(el); + } + } + return copy; + }; + + var cloneTemplateObject = function(org) { + var res = {}, prop, el, copy; + for (prop in org) { + el = org[prop]; + if (el.constructor == Object && el.xtype) { + copy = cloneTemplateObject(el); + if (copy.cbind) { + applyCBind(copy); + } + res[prop] = copy; + } else if (el.constructor == Array) { + copy = cloneTemplateArray(el); + res[prop] = copy; + } else { + res[prop] = el; + } + } + return res; + }; + + var condCloneProperties = function() { + var prop, el, i, tmp; + + for (prop in me) { + el = me[prop]; + if (el === undefined || el === null) continue; + if (typeof(el) === 'object' && el.constructor == Object) { + if (el.xtype && prop != 'config') { + me[prop] = cloneTemplateObject(el); + } + } else if (el.constructor == Array) { + tmp = cloneTemplateArray(el); + me[prop] = tmp; + } + } + }; + + condCloneProperties(); + } +}); +/* A reader to store a single JSON Object (hash) into a storage. + * Also accepts an array containing a single hash. + * + * So it can read: + * + * example1: {data1: "xyz", data2: "abc"} + * returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}] + * + * example2: [ {data1: "xyz", data2: "abc"} ] + * returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}] + * + * If you set 'readArray', the reader expexts the object as array: + * + * example3: [ { key: "data1", value: "xyz", p2: "cde" }, { key: "data2", value: "abc", p2: "efg" }] + * returns [{key: "data1", value: "xyz", p2: "cde}, {key: "data2", value: "abc", p2: "efg"}] + * + * Note: The records can contain additional properties (like 'p2' above) when you use 'readArray' + * + * Additional feature: specify allowed properties with default values with 'rows' object + * + * var rows = { + * memory: { + * required: true, + * defaultValue: 512 + * } + * } + * + */ + +Ext.define('Proxmox.data.reader.JsonObject', { + extend: 'Ext.data.reader.Json', + alias : 'reader.jsonobject', + + readArray: false, + + rows: undefined, + + constructor: function(config) { + var me = this; + + Ext.apply(me, config || {}); + + me.callParent([config]); + }, + + getResponseData: function(response) { + var me = this; + + var data = []; + try { + var result = Ext.decode(response.responseText); + // get our data items inside the server response + var root = result[me.getRootProperty()]; + + if (me.readArray) { + + var rec_hash = {}; + Ext.Array.each(root, function(rec) { + if (Ext.isDefined(rec.key)) { + rec_hash[rec.key] = rec; + } + }); + + if (me.rows) { + Ext.Object.each(me.rows, function(key, rowdef) { + var rec = rec_hash[key]; + if (Ext.isDefined(rec)) { + if (!Ext.isDefined(rec.value)) { + rec.value = rowdef.defaultValue; + } + data.push(rec); + } else if (Ext.isDefined(rowdef.defaultValue)) { + data.push({key: key, value: rowdef.defaultValue} ); + } else if (rowdef.required) { + data.push({key: key, value: undefined }); + } + }); + } else { + Ext.Array.each(root, function(rec) { + if (Ext.isDefined(rec.key)) { + data.push(rec); + } + }); + } + + } else { + + var org_root = root; + + if (Ext.isArray(org_root)) { + if (root.length == 1) { + root = org_root[0]; + } else { + root = {}; + } + } + + if (me.rows) { + Ext.Object.each(me.rows, function(key, rowdef) { + if (Ext.isDefined(root[key])) { + data.push({key: key, value: root[key]}); + } else if (Ext.isDefined(rowdef.defaultValue)) { + data.push({key: key, value: rowdef.defaultValue}); + } else if (rowdef.required) { + data.push({key: key, value: undefined}); + } + }); + } else { + Ext.Object.each(root, function(key, value) { + data.push({key: key, value: value }); + }); + } + } + } + catch (ex) { + Ext.Error.raise({ + response: response, + json: response.responseText, + parseError: ex, + msg: 'Unable to parse the JSON returned by the server: ' + ex.toString() + }); + } + + return data; + } +}); + +Ext.define('Proxmox.RestProxy', { + extend: 'Ext.data.RestProxy', + alias : 'proxy.proxmox', + + pageParam : null, + startParam: null, + limitParam: null, + groupParam: null, + sortParam: null, + filterParam: null, + noCache : false, + + afterRequest: function(request, success) { + this.fireEvent('afterload', this, request, success); + return; + }, + + constructor: function(config) { + + Ext.applyIf(config, { + reader: { + type: 'json', + rootProperty: config.root || 'data' + } + }); + + this.callParent([config]); + } +}, function() { + + Ext.define('KeyValue', { + extend: "Ext.data.Model", + fields: [ 'key', 'value' ], + idProperty: 'key' + }); + + Ext.define('KeyValuePendingDelete', { + extend: "Ext.data.Model", + fields: [ 'key', 'value', 'pending', 'delete' ], + idProperty: 'key' + }); + + Ext.define('proxmox-tasks', { + extend: 'Ext.data.Model', + fields: [ + { name: 'starttime', type : 'date', dateFormat: 'timestamp' }, + { name: 'endtime', type : 'date', dateFormat: 'timestamp' }, + { name: 'pid', type: 'int' }, + 'node', 'upid', 'user', 'status', 'type', 'id' + ], + idProperty: 'upid' + }); + + Ext.define('proxmox-cluster-log', { + extend: 'Ext.data.Model', + fields: [ + { name: 'uid' , type: 'int' }, + { name: 'time', type : 'date', dateFormat: 'timestamp' }, + { name: 'pri', type: 'int' }, + { name: 'pid', type: 'int' }, + 'node', 'user', 'tag', 'msg', + { + name: 'id', + convert: function(value, record) { + var info = record.data; + var text; + + if (value) { + return value; + } + // compute unique ID + return info.uid + ':' + info.node; + } + } + ], + idProperty: 'id' + }); + +}); +/* Extends the Ext.data.Store type + * with startUpdate() and stopUpdate() methods + * to refresh the store data in the background + * Components using this store directly will flicker + * due to the redisplay of the element ater 'config.interval' ms + * + * Note that you have to call yourself startUpdate() for the background load + * to begin + */ +Ext.define('Proxmox.data.UpdateStore', { + extend: 'Ext.data.Store', + alias: 'store.update', + + isStopped: true, + + autoStart: false, + + destroy: function() { + var me = this; + me.stopUpdate(); + me.callParent(); + }, + + constructor: function(config) { + var me = this; + + config = config || {}; + + if (!config.interval) { + config.interval = 3000; + } + + if (!config.storeid) { + throw "no storeid specified"; + } + + var load_task = new Ext.util.DelayedTask(); + + var run_load_task = function() { + if (me.isStopped) { + return; + } + + if (Proxmox.Utils.authOK()) { + var start = new Date(); + me.load(function() { + var runtime = (new Date()) - start; + var interval = config.interval + runtime*2; + load_task.delay(interval, run_load_task); + }); + } else { + load_task.delay(200, run_load_task); + } + }; + + Ext.apply(config, { + startUpdate: function() { + me.isStopped = false; + // run_load_task(); this makes problems with chrome + load_task.delay(1, run_load_task); + }, + stopUpdate: function() { + me.isStopped = true; + load_task.cancel(); + } + }); + + me.callParent([config]); + + me.load_task = load_task; + + if (me.autoStart) { + me.startUpdate(); + } + } +}); +/* + * The DiffStore is a in-memory store acting as proxy between a real store + * instance and a component. + * Its purpose is to redisplay the component *only* if the data has been changed + * inside the real store, to avoid the annoying visual flickering of using + * the real store directly. + * + * Implementation: + * The DiffStore monitors via mon() the 'load' events sent by the real store. + * On each 'load' event, the DiffStore compares its own content with the target + * store (call to cond_add_item()) and then fires a 'refresh' event. + * The 'refresh' event will automatically trigger a view refresh on the component + * who binds to this store. + */ + +/* Config properties: + * rstore: the realstore which will autorefresh its content from the API + * Only works if rstore has a model and use 'idProperty' + * sortAfterUpdate: sort the diffstore before rendering the view + */ +Ext.define('Proxmox.data.DiffStore', { + extend: 'Ext.data.Store', + alias: 'store.diff', + + sortAfterUpdate: false, + + constructor: function(config) { + var me = this; + + config = config || {}; + + if (!config.rstore) { + throw "no rstore specified"; + } + + if (!config.rstore.model) { + throw "no rstore model specified"; + } + + var rstore = config.rstore; + + Ext.apply(config, { + model: rstore.model, + proxy: { type: 'memory' } + }); + + me.callParent([config]); + + var first_load = true; + + var cond_add_item = function(data, id) { + var olditem = me.getById(id); + if (olditem) { + olditem.beginEdit(); + Ext.Array.each(me.model.prototype.fields, function(field) { + if (olditem.data[field.name] !== data[field.name]) { + olditem.set(field.name, data[field.name]); + } + }); + olditem.endEdit(true); + olditem.commit(); + } else { + var newrec = Ext.create(me.model, data); + var pos = (me.appendAtStart && !first_load) ? 0 : me.data.length; + me.insert(pos, newrec); + } + }; + + var loadFn = function(s, records, success) { + + if (!success) { + return; + } + + me.suspendEvents(); + + // getSource returns null if data is not filtered + // if it is filtered it returns all records + var allItems = me.getData().getSource() || me.getData(); + + // remove vanished items + allItems.each(function(olditem) { + var item = rstore.getById(olditem.getId()); + if (!item) { + me.remove(olditem); + } + }); + + rstore.each(function(item) { + cond_add_item(item.data, item.getId()); + }); + + me.filter(); + + if (me.sortAfterUpdate) { + me.sort(); + } + + first_load = false; + + me.resumeEvents(); + me.fireEvent('refresh', me); + me.fireEvent('datachanged', me); + }; + + if (rstore.isLoaded()) { + // if store is already loaded, + // insert items instantly + loadFn(rstore, [], true); + } + + me.mon(rstore, 'load', loadFn); + } +}); +/* This store encapsulates data items which are organized as an Array of key-values Objects + * ie data[0] contains something like {key: "keyboard", value: "da"} +* +* Designed to work with the KeyValue model and the JsonObject data reader +*/ +Ext.define('Proxmox.data.ObjectStore', { + extend: 'Proxmox.data.UpdateStore', + + getRecord: function() { + var me = this; + var record = Ext.create('Ext.data.Model'); + me.getData().each(function(item) { + record.set(item.data.key, item.data.value); + }); + record.commit(true); + return record; + }, + + constructor: function(config) { + var me = this; + + config = config || {}; + + if (!config.storeid) { + config.storeid = 'proxmox-store-' + (++Ext.idSeed); + } + + Ext.applyIf(config, { + model: 'KeyValue', + proxy: { + type: 'proxmox', + url: config.url, + extraParams: config.extraParams, + reader: { + type: 'jsonobject', + rows: config.rows, + readArray: config.readArray, + rootProperty: config.root || 'data' + } + } + }); + + me.callParent([config]); + } +}); +/* Extends the Proxmox.data.UpdateStore type + * + * + */ +Ext.define('Proxmox.data.RRDStore', { + extend: 'Proxmox.data.UpdateStore', + alias: 'store.proxmoxRRDStore', + + setRRDUrl: function(timeframe, cf) { + var me = this; + if (!timeframe) { + timeframe = me.timeframe; + } + + if (!cf) { + cf = me.cf; + } + + me.proxy.url = me.rrdurl + "?timeframe=" + timeframe + "&cf=" + cf; + }, + + proxy: { + type: 'proxmox' + }, + + timeframe: 'hour', + + cf: 'AVERAGE', + + constructor: function(config) { + var me = this; + + config = config || {}; + + // set default interval to 30seconds + if (!config.interval) { + config.interval = 30000; + } + + // set a new storeid + if (!config.storeid) { + config.storeid = 'rrdstore-' + (++Ext.idSeed); + } + + // rrdurl is required + if (!config.rrdurl) { + throw "no rrdurl specified"; + } + + var stateid = 'proxmoxRRDTypeSelection'; + var sp = Ext.state.Manager.getProvider(); + var stateinit = sp.get(stateid); + + if (stateinit) { + if(stateinit.timeframe !== me.timeframe || stateinit.cf !== me.rrdcffn){ + me.timeframe = stateinit.timeframe; + me.rrdcffn = stateinit.cf; + } + } + + me.callParent([config]); + + me.setRRDUrl(); + me.mon(sp, 'statechange', function(prov, key, state){ + if (key === stateid) { + if (state && state.id) { + if (state.timeframe !== me.timeframe || state.cf !== me.cf) { + me.timeframe = state.timeframe; + me.cf = state.cf; + me.setRRDUrl(); + me.reload(); + } + } + } + }); + } +}); +Ext.define('Timezone', { + extend: 'Ext.data.Model', + fields: ['zone'] +}); + +Ext.define('Proxmox.data.TimezoneStore', { + extend: 'Ext.data.Store', + model: 'Timezone', + data: [ + ['Africa/Abidjan'], + ['Africa/Accra'], + ['Africa/Addis_Ababa'], + ['Africa/Algiers'], + ['Africa/Asmara'], + ['Africa/Bamako'], + ['Africa/Bangui'], + ['Africa/Banjul'], + ['Africa/Bissau'], + ['Africa/Blantyre'], + ['Africa/Brazzaville'], + ['Africa/Bujumbura'], + ['Africa/Cairo'], + ['Africa/Casablanca'], + ['Africa/Ceuta'], + ['Africa/Conakry'], + ['Africa/Dakar'], + ['Africa/Dar_es_Salaam'], + ['Africa/Djibouti'], + ['Africa/Douala'], + ['Africa/El_Aaiun'], + ['Africa/Freetown'], + ['Africa/Gaborone'], + ['Africa/Harare'], + ['Africa/Johannesburg'], + ['Africa/Kampala'], + ['Africa/Khartoum'], + ['Africa/Kigali'], + ['Africa/Kinshasa'], + ['Africa/Lagos'], + ['Africa/Libreville'], + ['Africa/Lome'], + ['Africa/Luanda'], + ['Africa/Lubumbashi'], + ['Africa/Lusaka'], + ['Africa/Malabo'], + ['Africa/Maputo'], + ['Africa/Maseru'], + ['Africa/Mbabane'], + ['Africa/Mogadishu'], + ['Africa/Monrovia'], + ['Africa/Nairobi'], + ['Africa/Ndjamena'], + ['Africa/Niamey'], + ['Africa/Nouakchott'], + ['Africa/Ouagadougou'], + ['Africa/Porto-Novo'], + ['Africa/Sao_Tome'], + ['Africa/Tripoli'], + ['Africa/Tunis'], + ['Africa/Windhoek'], + ['America/Adak'], + ['America/Anchorage'], + ['America/Anguilla'], + ['America/Antigua'], + ['America/Araguaina'], + ['America/Argentina/Buenos_Aires'], + ['America/Argentina/Catamarca'], + ['America/Argentina/Cordoba'], + ['America/Argentina/Jujuy'], + ['America/Argentina/La_Rioja'], + ['America/Argentina/Mendoza'], + ['America/Argentina/Rio_Gallegos'], + ['America/Argentina/Salta'], + ['America/Argentina/San_Juan'], + ['America/Argentina/San_Luis'], + ['America/Argentina/Tucuman'], + ['America/Argentina/Ushuaia'], + ['America/Aruba'], + ['America/Asuncion'], + ['America/Atikokan'], + ['America/Bahia'], + ['America/Bahia_Banderas'], + ['America/Barbados'], + ['America/Belem'], + ['America/Belize'], + ['America/Blanc-Sablon'], + ['America/Boa_Vista'], + ['America/Bogota'], + ['America/Boise'], + ['America/Cambridge_Bay'], + ['America/Campo_Grande'], + ['America/Cancun'], + ['America/Caracas'], + ['America/Cayenne'], + ['America/Cayman'], + ['America/Chicago'], + ['America/Chihuahua'], + ['America/Costa_Rica'], + ['America/Cuiaba'], + ['America/Curacao'], + ['America/Danmarkshavn'], + ['America/Dawson'], + ['America/Dawson_Creek'], + ['America/Denver'], + ['America/Detroit'], + ['America/Dominica'], + ['America/Edmonton'], + ['America/Eirunepe'], + ['America/El_Salvador'], + ['America/Fortaleza'], + ['America/Glace_Bay'], + ['America/Godthab'], + ['America/Goose_Bay'], + ['America/Grand_Turk'], + ['America/Grenada'], + ['America/Guadeloupe'], + ['America/Guatemala'], + ['America/Guayaquil'], + ['America/Guyana'], + ['America/Halifax'], + ['America/Havana'], + ['America/Hermosillo'], + ['America/Indiana/Indianapolis'], + ['America/Indiana/Knox'], + ['America/Indiana/Marengo'], + ['America/Indiana/Petersburg'], + ['America/Indiana/Tell_City'], + ['America/Indiana/Vevay'], + ['America/Indiana/Vincennes'], + ['America/Indiana/Winamac'], + ['America/Inuvik'], + ['America/Iqaluit'], + ['America/Jamaica'], + ['America/Juneau'], + ['America/Kentucky/Louisville'], + ['America/Kentucky/Monticello'], + ['America/La_Paz'], + ['America/Lima'], + ['America/Los_Angeles'], + ['America/Maceio'], + ['America/Managua'], + ['America/Manaus'], + ['America/Marigot'], + ['America/Martinique'], + ['America/Matamoros'], + ['America/Mazatlan'], + ['America/Menominee'], + ['America/Merida'], + ['America/Mexico_City'], + ['America/Miquelon'], + ['America/Moncton'], + ['America/Monterrey'], + ['America/Montevideo'], + ['America/Montreal'], + ['America/Montserrat'], + ['America/Nassau'], + ['America/New_York'], + ['America/Nipigon'], + ['America/Nome'], + ['America/Noronha'], + ['America/North_Dakota/Center'], + ['America/North_Dakota/New_Salem'], + ['America/Ojinaga'], + ['America/Panama'], + ['America/Pangnirtung'], + ['America/Paramaribo'], + ['America/Phoenix'], + ['America/Port-au-Prince'], + ['America/Port_of_Spain'], + ['America/Porto_Velho'], + ['America/Puerto_Rico'], + ['America/Rainy_River'], + ['America/Rankin_Inlet'], + ['America/Recife'], + ['America/Regina'], + ['America/Resolute'], + ['America/Rio_Branco'], + ['America/Santa_Isabel'], + ['America/Santarem'], + ['America/Santiago'], + ['America/Santo_Domingo'], + ['America/Sao_Paulo'], + ['America/Scoresbysund'], + ['America/Shiprock'], + ['America/St_Barthelemy'], + ['America/St_Johns'], + ['America/St_Kitts'], + ['America/St_Lucia'], + ['America/St_Thomas'], + ['America/St_Vincent'], + ['America/Swift_Current'], + ['America/Tegucigalpa'], + ['America/Thule'], + ['America/Thunder_Bay'], + ['America/Tijuana'], + ['America/Toronto'], + ['America/Tortola'], + ['America/Vancouver'], + ['America/Whitehorse'], + ['America/Winnipeg'], + ['America/Yakutat'], + ['America/Yellowknife'], + ['Antarctica/Casey'], + ['Antarctica/Davis'], + ['Antarctica/DumontDUrville'], + ['Antarctica/Macquarie'], + ['Antarctica/Mawson'], + ['Antarctica/McMurdo'], + ['Antarctica/Palmer'], + ['Antarctica/Rothera'], + ['Antarctica/South_Pole'], + ['Antarctica/Syowa'], + ['Antarctica/Vostok'], + ['Arctic/Longyearbyen'], + ['Asia/Aden'], + ['Asia/Almaty'], + ['Asia/Amman'], + ['Asia/Anadyr'], + ['Asia/Aqtau'], + ['Asia/Aqtobe'], + ['Asia/Ashgabat'], + ['Asia/Baghdad'], + ['Asia/Bahrain'], + ['Asia/Baku'], + ['Asia/Bangkok'], + ['Asia/Beirut'], + ['Asia/Bishkek'], + ['Asia/Brunei'], + ['Asia/Choibalsan'], + ['Asia/Chongqing'], + ['Asia/Colombo'], + ['Asia/Damascus'], + ['Asia/Dhaka'], + ['Asia/Dili'], + ['Asia/Dubai'], + ['Asia/Dushanbe'], + ['Asia/Gaza'], + ['Asia/Harbin'], + ['Asia/Ho_Chi_Minh'], + ['Asia/Hong_Kong'], + ['Asia/Hovd'], + ['Asia/Irkutsk'], + ['Asia/Jakarta'], + ['Asia/Jayapura'], + ['Asia/Jerusalem'], + ['Asia/Kabul'], + ['Asia/Kamchatka'], + ['Asia/Karachi'], + ['Asia/Kashgar'], + ['Asia/Kathmandu'], + ['Asia/Kolkata'], + ['Asia/Krasnoyarsk'], + ['Asia/Kuala_Lumpur'], + ['Asia/Kuching'], + ['Asia/Kuwait'], + ['Asia/Macau'], + ['Asia/Magadan'], + ['Asia/Makassar'], + ['Asia/Manila'], + ['Asia/Muscat'], + ['Asia/Nicosia'], + ['Asia/Novokuznetsk'], + ['Asia/Novosibirsk'], + ['Asia/Omsk'], + ['Asia/Oral'], + ['Asia/Phnom_Penh'], + ['Asia/Pontianak'], + ['Asia/Pyongyang'], + ['Asia/Qatar'], + ['Asia/Qyzylorda'], + ['Asia/Rangoon'], + ['Asia/Riyadh'], + ['Asia/Sakhalin'], + ['Asia/Samarkand'], + ['Asia/Seoul'], + ['Asia/Shanghai'], + ['Asia/Singapore'], + ['Asia/Taipei'], + ['Asia/Tashkent'], + ['Asia/Tbilisi'], + ['Asia/Tehran'], + ['Asia/Thimphu'], + ['Asia/Tokyo'], + ['Asia/Ulaanbaatar'], + ['Asia/Urumqi'], + ['Asia/Vientiane'], + ['Asia/Vladivostok'], + ['Asia/Yakutsk'], + ['Asia/Yekaterinburg'], + ['Asia/Yerevan'], + ['Atlantic/Azores'], + ['Atlantic/Bermuda'], + ['Atlantic/Canary'], + ['Atlantic/Cape_Verde'], + ['Atlantic/Faroe'], + ['Atlantic/Madeira'], + ['Atlantic/Reykjavik'], + ['Atlantic/South_Georgia'], + ['Atlantic/St_Helena'], + ['Atlantic/Stanley'], + ['Australia/Adelaide'], + ['Australia/Brisbane'], + ['Australia/Broken_Hill'], + ['Australia/Currie'], + ['Australia/Darwin'], + ['Australia/Eucla'], + ['Australia/Hobart'], + ['Australia/Lindeman'], + ['Australia/Lord_Howe'], + ['Australia/Melbourne'], + ['Australia/Perth'], + ['Australia/Sydney'], + ['Europe/Amsterdam'], + ['Europe/Andorra'], + ['Europe/Athens'], + ['Europe/Belgrade'], + ['Europe/Berlin'], + ['Europe/Bratislava'], + ['Europe/Brussels'], + ['Europe/Bucharest'], + ['Europe/Budapest'], + ['Europe/Chisinau'], + ['Europe/Copenhagen'], + ['Europe/Dublin'], + ['Europe/Gibraltar'], + ['Europe/Guernsey'], + ['Europe/Helsinki'], + ['Europe/Isle_of_Man'], + ['Europe/Istanbul'], + ['Europe/Jersey'], + ['Europe/Kaliningrad'], + ['Europe/Kiev'], + ['Europe/Lisbon'], + ['Europe/Ljubljana'], + ['Europe/London'], + ['Europe/Luxembourg'], + ['Europe/Madrid'], + ['Europe/Malta'], + ['Europe/Mariehamn'], + ['Europe/Minsk'], + ['Europe/Monaco'], + ['Europe/Moscow'], + ['Europe/Oslo'], + ['Europe/Paris'], + ['Europe/Podgorica'], + ['Europe/Prague'], + ['Europe/Riga'], + ['Europe/Rome'], + ['Europe/Samara'], + ['Europe/San_Marino'], + ['Europe/Sarajevo'], + ['Europe/Simferopol'], + ['Europe/Skopje'], + ['Europe/Sofia'], + ['Europe/Stockholm'], + ['Europe/Tallinn'], + ['Europe/Tirane'], + ['Europe/Uzhgorod'], + ['Europe/Vaduz'], + ['Europe/Vatican'], + ['Europe/Vienna'], + ['Europe/Vilnius'], + ['Europe/Volgograd'], + ['Europe/Warsaw'], + ['Europe/Zagreb'], + ['Europe/Zaporozhye'], + ['Europe/Zurich'], + ['Indian/Antananarivo'], + ['Indian/Chagos'], + ['Indian/Christmas'], + ['Indian/Cocos'], + ['Indian/Comoro'], + ['Indian/Kerguelen'], + ['Indian/Mahe'], + ['Indian/Maldives'], + ['Indian/Mauritius'], + ['Indian/Mayotte'], + ['Indian/Reunion'], + ['Pacific/Apia'], + ['Pacific/Auckland'], + ['Pacific/Chatham'], + ['Pacific/Chuuk'], + ['Pacific/Easter'], + ['Pacific/Efate'], + ['Pacific/Enderbury'], + ['Pacific/Fakaofo'], + ['Pacific/Fiji'], + ['Pacific/Funafuti'], + ['Pacific/Galapagos'], + ['Pacific/Gambier'], + ['Pacific/Guadalcanal'], + ['Pacific/Guam'], + ['Pacific/Honolulu'], + ['Pacific/Johnston'], + ['Pacific/Kiritimati'], + ['Pacific/Kosrae'], + ['Pacific/Kwajalein'], + ['Pacific/Majuro'], + ['Pacific/Marquesas'], + ['Pacific/Midway'], + ['Pacific/Nauru'], + ['Pacific/Niue'], + ['Pacific/Norfolk'], + ['Pacific/Noumea'], + ['Pacific/Pago_Pago'], + ['Pacific/Palau'], + ['Pacific/Pitcairn'], + ['Pacific/Pohnpei'], + ['Pacific/Port_Moresby'], + ['Pacific/Rarotonga'], + ['Pacific/Saipan'], + ['Pacific/Tahiti'], + ['Pacific/Tarawa'], + ['Pacific/Tongatapu'], + ['Pacific/Wake'], + ['Pacific/Wallis'], + ['UTC'] + ] +}); +Ext.define('Proxmox.form.field.Integer',{ + extend: 'Ext.form.field.Number', + alias: 'widget.proxmoxintegerfield', + + config: { + deleteEmpty: false + }, + + allowDecimals: false, + allowExponential: false, + step: 1, + + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue && !me.isFileUpload()) { + val = me.getSubmitValue(); + if (val !== undefined && val !== null && val !== '') { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + } + +}); +Ext.define('Proxmox.form.field.Textfield', { + extend: 'Ext.form.field.Text', + alias: ['widget.proxmoxtextfield'], + + config: { + skipEmptyText: true, + + deleteEmpty: false, + }, + + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue && !me.isFileUpload()) { + val = me.getSubmitValue(); + if (val !== null) { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + }, + + getSubmitValue: function() { + var me = this; + + var value = this.processRawValue(this.getRawValue()); + if (value !== '') { + return value; + } + + return me.getSkipEmptyText() ? null: value; + }, + + setAllowBlank: function(allowBlank) { + this.allowBlank = allowBlank; + this.validate(); + } +}); +Ext.define('Proxmox.DateTimeField', { + extend: 'Ext.form.FieldContainer', + xtype: 'promxoxDateTimeField', + + layout: 'hbox', + + referenceHolder: true, + + submitFormat: 'U', + + getValue: function() { + var me = this; + var d = me.lookupReference('dateentry').getValue(); + + if (d === undefined || d === null) { return null; } + + var t = me.lookupReference('timeentry').getValue(); + + if (t === undefined || t === null) { return null; } + + var offset = (t.getHours()*3600+t.getMinutes()*60)*1000; + + return new Date(d.getTime() + offset); + }, + + getSubmitValue: function() { + var me = this; + var format = me.submitFormat; + var value = me.getValue(); + + return value ? Ext.Date.format(value, format) : null; + }, + + items: [ + { + xtype: 'datefield', + editable: false, + reference: 'dateentry', + flex: 1, + format: 'Y-m-d' + }, + { + xtype: 'timefield', + reference: 'timeentry', + format: 'H:i', + width: 80, + value: '00:00', + increment: 60 + } + ], + + initComponent: function() { + var me = this; + + me.callParent(); + + var value = me.value || new Date(); + + me.lookupReference('dateentry').setValue(value); + me.lookupReference('timeentry').setValue(value); + + me.relayEvents(me.lookupReference('dateentry'), ['change']); + me.relayEvents(me.lookupReference('timeentry'), ['change']); + } +}); +Ext.define('Proxmox.form.Checkbox', { + extend: 'Ext.form.field.Checkbox', + alias: ['widget.proxmoxcheckbox'], + + config: { + defaultValue: undefined, + deleteDefaultValue: false, + deleteEmpty: false + }, + + inputValue: '1', + + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue) { + val = me.getSubmitValue(); + if (val !== null) { + data = {}; + if ((val == me.getDefaultValue()) && me.getDeleteDefaultValue()) { + data['delete'] = me.getName(); + } else { + data[me.getName()] = val; + } + } else if (me.getDeleteEmpty()) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + }, + + // also accept integer 1 as true + setRawValue: function(value) { + var me = this; + + if (value === 1) { + me.callParent([true]); + } else { + me.callParent([value]); + } + } + +}); +/* Key-Value ComboBox + * + * config properties: + * comboItems: an array of Key - Value pairs + * deleteEmpty: if set to true (default), an empty value received from the + * comboBox will reset the property to its default value + */ +Ext.define('Proxmox.form.KVComboBox', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.proxmoxKVComboBox', + + config: { + deleteEmpty: true + }, + + comboItems: undefined, + displayField: 'value', + valueField: 'key', + queryMode: 'local', + + // overide framework function to implement deleteEmpty behaviour + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue) { + val = me.getSubmitValue(); + if (val !== null && val !== '' && val !== '__default__') { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + }, + + validator: function(val) { + var me = this; + + if (me.editable || val === null || val === '') { + return true; + } + + if (me.store.getCount() > 0) { + var values = me.multiSelect ? val.split(me.delimiter) : [val]; + var items = me.store.getData().collect('value', 'data'); + if (Ext.Array.every(values, function(value) { + return Ext.Array.contains(items, value); + })) { + return true; + } + } + + // returns a boolean or string + /*jslint confusion: true */ + return "value '" + val + "' not allowed!"; + }, + + initComponent: function() { + var me = this; + + me.store = Ext.create('Ext.data.ArrayStore', { + model: 'KeyValue', + data : me.comboItems + }); + + if (me.initialConfig.editable === undefined) { + me.editable = false; + } + + me.callParent(); + }, + + setComboItems: function(items) { + var me = this; + + me.getStore().setData(items); + } + +}); +Ext.define('Proxmox.form.LanguageSelector', { + extend: 'Proxmox.form.KVComboBox', + xtype: 'proxmoxLanguageSelector', + + comboItems: Proxmox.Utils.language_array() +}); +/* + * ComboGrid component: a ComboBox where the dropdown menu (the + * "Picker") is a Grid with Rows and Columns expects a listConfig + * object with a columns property roughly based on the GridPicker from + * https://www.sencha.com/forum/showthread.php?299909 + * +*/ + +Ext.define('Proxmox.form.ComboGrid', { + extend: 'Ext.form.field.ComboBox', + alias: ['widget.proxmoxComboGrid'], + + // this value is used as default value after load() + preferredValue: undefined, + + // hack: allow to select empty value + // seems extjs does not allow that when 'editable == false' + onKeyUp: function(e, t) { + var me = this; + var key = e.getKey(); + + if (!me.editable && me.allowBlank && !me.multiSelect && + (key == e.BACKSPACE || key == e.DELETE)) { + me.setValue(''); + } + + me.callParent(arguments); + }, + + config: { + skipEmptyText: false, + deleteEmpty: false, + }, + + // needed to trigger onKeyUp etc. + enableKeyEvents: true, + + editable: false, + + // override ExtJS method + // if the field has multiSelect enabled, the store is not loaded, and + // the displayfield == valuefield, it saves the rawvalue as an array + // but the getRawValue method is only defined in the textfield class + // (which has not to deal with arrays) an returns the string in the + // field (not an array) + // + // so if we have multiselect enabled, return the rawValue (which + // should be an array) and else we do callParent so + // it should not impact any other use of the class + getRawValue: function() { + var me = this; + if (me.multiSelect) { + return me.rawValue; + } else { + return me.callParent(); + } + }, + + getSubmitData: function() { + var me = this; + + let data = null; + if (!me.disabled && me.submitValue) { + let val = me.getSubmitValue(); + if (val !== null) { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + }, + + getSubmitValue: function() { + var me = this; + + var value = me.callParent(); + if (value !== '') { + return value; + } + + return me.getSkipEmptyText() ? null: value; + }, + + setAllowBlank: function(allowBlank) { + this.allowBlank = allowBlank; + this.validate(); + }, + +// override ExtJS protected method + onBindStore: function(store, initial) { + var me = this, + picker = me.picker, + extraKeySpec, + valueCollectionConfig; + + // We're being bound, not unbound... + if (store) { + // If store was created from a 2 dimensional array with generated field names 'field1' and 'field2' + if (store.autoCreated) { + me.queryMode = 'local'; + me.valueField = me.displayField = 'field1'; + if (!store.expanded) { + me.displayField = 'field2'; + } + + // displayTpl config will need regenerating with the autogenerated displayField name 'field1' + me.setDisplayTpl(null); + } + if (!Ext.isDefined(me.valueField)) { + me.valueField = me.displayField; + } + + // Add a byValue index to the store so that we can efficiently look up records by the value field + // when setValue passes string value(s). + // The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys + // are found, they are all returned by the get call. + // This is so that findByText and findByValue are able to return the *FIRST* matching value. By default, + // if unique is true, CollectionKey keeps the *last* matching value. + extraKeySpec = { + byValue: { + rootProperty: 'data', + unique: false + } + }; + extraKeySpec.byValue.property = me.valueField; + store.setExtraKeys(extraKeySpec); + + if (me.displayField === me.valueField) { + store.byText = store.byValue; + } else { + extraKeySpec.byText = { + rootProperty: 'data', + unique: false + }; + extraKeySpec.byText.property = me.displayField; + store.setExtraKeys(extraKeySpec); + } + + // We hold a collection of the values which have been selected, keyed by this field's valueField. + // This collection also functions as the selected items collection for the BoundList's selection model + valueCollectionConfig = { + rootProperty: 'data', + extraKeys: { + byInternalId: { + property: 'internalId' + }, + byValue: { + property: me.valueField, + rootProperty: 'data' + } + }, + // Whenever this collection is changed by anyone, whether by this field adding to it, + // or the BoundList operating, we must refresh our value. + listeners: { + beginupdate: me.onValueCollectionBeginUpdate, + endupdate: me.onValueCollectionEndUpdate, + scope: me + } + }; + + // This becomes our collection of selected records for the Field. + me.valueCollection = new Ext.util.Collection(valueCollectionConfig); + + // We use the selected Collection as our value collection and the basis + // for rendering the tag list. + + //proxmox override: since the picker is represented by a grid panel, + // we changed here the selection to RowModel + me.pickerSelectionModel = new Ext.selection.RowModel({ + mode: me.multiSelect ? 'SIMPLE' : 'SINGLE', + // There are situations when a row is selected on mousedown but then the mouse is dragged to another row + // and released. In these situations, the event target for the click event won't be the row where the mouse + // was released but the boundview. The view will then determine that it should fire a container click, and + // the DataViewModel will then deselect all prior selections. Setting `deselectOnContainerClick` here will + // prevent the model from deselecting. + deselectOnContainerClick: false, + enableInitialSelection: false, + pruneRemoved: false, + selected: me.valueCollection, + store: store, + listeners: { + scope: me, + lastselectedchanged: me.updateBindSelection + } + }); + + if (!initial) { + me.resetToDefault(); + } + + if (picker) { + picker.setSelectionModel(me.pickerSelectionModel); + if (picker.getStore() !== store) { + picker.bindStore(store); + } + } + } + }, + + // copied from ComboBox + createPicker: function() { + var me = this; + var picker; + + var pickerCfg = Ext.apply({ + // proxmox overrides: display a grid for selection + xtype: 'gridpanel', + id: me.pickerId, + pickerField: me, + floating: true, + hidden: true, + store: me.store, + displayField: me.displayField, + preserveScrollOnRefresh: true, + pageSize: me.pageSize, + tpl: me.tpl, + selModel: me.pickerSelectionModel, + focusOnToFront: false + }, me.listConfig, me.defaultListConfig); + + picker = me.picker || Ext.widget(pickerCfg); + + if (picker.getStore() !== me.store) { + picker.bindStore(me.store); + } + + if (me.pageSize) { + picker.pagingToolbar.on('beforechange', me.onPageChange, me); + } + + // proxmox overrides: pass missing method in gridPanel to its view + picker.refresh = function() { + picker.getSelectionModel().select(me.valueCollection.getRange()); + picker.getView().refresh(); + }; + picker.getNodeByRecord = function() { + picker.getView().getNodeByRecord(arguments); + }; + + // We limit the height of the picker to fit in the space above + // or below this field unless the picker has its own ideas about that. + if (!picker.initialConfig.maxHeight) { + picker.on({ + beforeshow: me.onBeforePickerShow, + scope: me + }); + } + picker.getSelectionModel().on({ + beforeselect: me.onBeforeSelect, + beforedeselect: me.onBeforeDeselect, + focuschange: me.onFocusChange, + selectionChange: function (sm, selectedRecords) { + var me = this; + if (selectedRecords.length) { + me.setValue(selectedRecords); + me.fireEvent('select', me, selectedRecords); + } + }, + scope: me + }); + + // hack for extjs6 + // when the clicked item is the same as the previously selected, + // it does not select the item + // instead we hide the picker + if (!me.multiSelect) { + picker.on('itemclick', function (sm,record) { + if (picker.getSelection()[0] === record) { + picker.hide(); + } + }); + } + + // when our store is not yet loaded, we increase + // the height of the gridpanel, so that we can see + // the loading mask + // + // we save the minheight to reset it after the load + picker.on('show', function() { + if (me.enableLoadMask) { + me.savedMinHeight = picker.getMinHeight(); + picker.setMinHeight(100); + } + }); + + picker.getNavigationModel().navigateOnSpace = false; + + return picker; + }, + + initComponent: function() { + var me = this; + + Ext.apply(me, { + queryMode: 'local', + matchFieldWidth: false + }); + + Ext.applyIf(me, { value: ''}); // hack: avoid ExtJS validate() bug + + Ext.applyIf(me.listConfig, { width: 400 }); + + me.callParent(); + + // Create the picker at an early stage, so it is available to store the previous selection + if (!me.picker) { + me.createPicker(); + } + + if (me.editable) { + // The trigger.picker causes first a focus event on the field then + // toggles the selection picker. Thus skip expanding in this case, + // else our focus listner expands and the picker.trigger then + // collapses it directly afterwards. + Ext.override(me.triggers.picker, { + onMouseDown : function (e) { + // copied "should we focus" check from Ext.form.trigger.Trigger + if (e.pointerType !== 'touch' && !this.field.owns(Ext.Element.getActiveElement())) { + me.skip_expand_on_focus = true; + } + this.callParent(arguments); + } + }); + + me.on("focus", function(me) { + if (!me.isExpanded && !me.skip_expand_on_focus) { + me.expand(); + } + me.skip_expand_on_focus = false; + }); + } + + me.mon(me.store, 'beforeload', function() { + if (!me.isDisabled()) { + me.enableLoadMask = true; + } + }); + + // hack: autoSelect does not work + me.mon(me.store, 'load', function(store, r, success, o) { + if (success) { + me.clearInvalid(); + + if (me.enableLoadMask) { + delete me.enableLoadMask; + + // if the picker exists, + // we reset its minheight to the saved var/0 + // we have to update the layout, otherwise the height + // gets not recalculated + if (me.picker) { + me.picker.setMinHeight(me.savedMinHeight || 0); + delete me.savedMinHeight; + me.picker.updateLayout(); + } + } + + var def = me.getValue() || me.preferredValue; + if (def) { + me.setValue(def, true); // sync with grid + } + var found = false; + if (def) { + if (Ext.isArray(def)) { + Ext.Array.each(def, function(v) { + if (store.findRecord(me.valueField, v)) { + found = true; + return false; // break + } + }); + } else { + found = store.findRecord(me.valueField, def); + } + } + + if (!found) { + var rec = me.store.first(); + if (me.autoSelect && rec && rec.data) { + def = rec.data[me.valueField]; + me.setValue(def, true); + } else { + me.setValue(me.editable ? def : '', true); + } + } + } + }); + } +}); +Ext.define('Proxmox.form.RRDTypeSelector', { + extend: 'Ext.form.field.ComboBox', + alias: ['widget.proxmoxRRDTypeSelector'], + + displayField: 'text', + valueField: 'id', + editable: false, + queryMode: 'local', + value: 'hour', + stateEvents: [ 'select' ], + stateful: true, + stateId: 'proxmoxRRDTypeSelection', + store: { + type: 'array', + fields: [ 'id', 'timeframe', 'cf', 'text' ], + data : [ + [ 'hour', 'hour', 'AVERAGE', + gettext('Hour') + ' (' + gettext('average') +')' ], + [ 'hourmax', 'hour', 'MAX', + gettext('Hour') + ' (' + gettext('maximum') + ')' ], + [ 'day', 'day', 'AVERAGE', + gettext('Day') + ' (' + gettext('average') + ')' ], + [ 'daymax', 'day', 'MAX', + gettext('Day') + ' (' + gettext('maximum') + ')' ], + [ 'week', 'week', 'AVERAGE', + gettext('Week') + ' (' + gettext('average') + ')' ], + [ 'weekmax', 'week', 'MAX', + gettext('Week') + ' (' + gettext('maximum') + ')' ], + [ 'month', 'month', 'AVERAGE', + gettext('Month') + ' (' + gettext('average') + ')' ], + [ 'monthmax', 'month', 'MAX', + gettext('Month') + ' (' + gettext('maximum') + ')' ], + [ 'year', 'year', 'AVERAGE', + gettext('Year') + ' (' + gettext('average') + ')' ], + [ 'yearmax', 'year', 'MAX', + gettext('Year') + ' (' + gettext('maximum') + ')' ] + ] + }, + // save current selection in the state Provider so RRDView can read it + getState: function() { + var ind = this.getStore().findExact('id', this.getValue()); + var rec = this.getStore().getAt(ind); + if (!rec) { + return; + } + return { + id: rec.data.id, + timeframe: rec.data.timeframe, + cf: rec.data.cf + }; + }, + // set selection based on last saved state + applyState : function(state) { + if (state && state.id) { + this.setValue(state.id); + } + } +}); +Ext.define('Proxmox.form.BondModeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.bondModeSelector'], + + openvswitch: false, + + initComponent: function() { + var me = this; + + if (me.openvswitch) { + me.comboItems = Proxmox.Utils.bond_mode_array([ + 'active-backup', + 'balance-slb', + 'lacp-balance-slb', + 'lacp-balance-tcp', + ]); + } else { + me.comboItems = Proxmox.Utils.bond_mode_array([ + 'balance-rr', + 'active-backup', + 'balance-xor', + 'broadcast', + '802.3ad', + 'balance-tlb', + 'balance-alb', + ]); + } + + me.callParent(); + } +}); + +Ext.define('Proxmox.form.BondPolicySelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.bondPolicySelector'], + comboItems: [ + ['layer2', 'layer2'], + ['layer2+3', 'layer2+3'], + ['layer3+4', 'layer3+4'] + ] +}); + +Ext.define('Proxmox.form.NetworkSelectorController', { + extend: 'Ext.app.ViewController', + alias: 'controller.proxmoxNetworkSelectorController', + + init: function(view) { + var me = this; + + if (!view.nodename) { + throw "missing custom view config: nodename"; + } + view.getStore().getProxy().setUrl('/api2/json/nodes/'+ view.nodename + '/network'); + } +}); + +Ext.define('Proxmox.data.NetworkSelector', { + extend: 'Ext.data.Model', + fields: [ + {name: 'active'}, + {name: 'cidr'}, + {name: 'cidr6'}, + {name: 'address'}, + {name: 'address6'}, + {name: 'comments'}, + {name: 'iface'}, + {name: 'slaves'}, + {name: 'type'} + ] +}); + +Ext.define('Proxmox.form.NetworkSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.proxmoxNetworkSelector', + + controller: 'proxmoxNetworkSelectorController', + + nodename: 'localhost', + setNodename: function(nodename) { + this.nodename = nodename; + var networkSelectorStore = this.getStore(); + networkSelectorStore.removeAll(); + // because of manual local copy of data for ip4/6 + this.getPicker().refresh(); + if (networkSelectorStore && typeof networkSelectorStore.getProxy === 'function') { + networkSelectorStore.getProxy().setUrl('/api2/json/nodes/'+ nodename + '/network'); + networkSelectorStore.load(); + } + }, + // set default value to empty array, else it inits it with + // null and after the store load it is an empty array, + // triggering dirtychange + value: [], + valueField: 'cidr', + displayField: 'cidr', + store: { + autoLoad: true, + model: 'Proxmox.data.NetworkSelector', + proxy: { + type: 'proxmox' + }, + sorters: [ + { + property : 'iface', + direction: 'ASC' + } + ], + filters: [ + function(item) { + return item.data.cidr; + } + ], + listeners: { + load: function(store, records, successfull) { + + if (successfull) { + records.forEach(function(record) { + if (record.data.cidr6) { + let dest = (record.data.cidr) ? record.copy(null) : record; + dest.data.cidr = record.data.cidr6; + dest.data.address = record.data.address6; + delete record.data.cidr6; + dest.data.comments = record.data.comments6; + delete record.data.comments6; + store.add(dest); + } + }); + } + } + } + }, + listConfig: { + width: 600, + columns: [ + { + + header: gettext('CIDR'), + dataIndex: 'cidr', + hideable: false, + flex: 1 + }, + { + + header: gettext('IP'), + dataIndex: 'address', + hidden: true, + }, + { + header: gettext('Interface'), + width: 90, + dataIndex: 'iface' + }, + { + header: gettext('Active'), + renderer: Proxmox.Utils.format_boolean, + width: 60, + dataIndex: 'active' + }, + { + header: gettext('Type'), + width: 80, + hidden: true, + dataIndex: 'type' + }, + { + header: gettext('Comment'), + flex: 2, + dataIndex: 'comments' + } + ] + } +}); +/* Button features: + * - observe selection changes to enable/disable the button using enableFn() + * - pop up confirmation dialog using confirmMsg() + */ +Ext.define('Proxmox.button.Button', { + extend: 'Ext.button.Button', + alias: 'widget.proxmoxButton', + + // the selection model to observe + selModel: undefined, + + // if 'false' handler will not be called (button disabled) + enableFn: function(record) { }, + + // function(record) or text + confirmMsg: false, + + // take special care in confirm box (select no as default). + dangerous: false, + + initComponent: function() { + /*jslint confusion: true */ + + var me = this; + + if (me.handler) { + + // Note: me.realHandler may be a string (see named scopes) + var realHandler = me.handler; + + me.handler = function(button, event) { + var rec, msg; + if (me.selModel) { + rec = me.selModel.getSelection()[0]; + if (!rec || (me.enableFn(rec) === false)) { + return; + } + } + + if (me.confirmMsg) { + msg = me.confirmMsg; + if (Ext.isFunction(me.confirmMsg)) { + msg = me.confirmMsg(rec); + } + Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: msg, + buttons: Ext.Msg.YESNO, + defaultFocus: me.dangerous ? 'no' : 'yes', + callback: function(btn) { + if (btn !== 'yes') { + return; + } + Ext.callback(realHandler, me.scope, [button, event, rec], 0, me); + } + }); + } else { + Ext.callback(realHandler, me.scope, [button, event, rec], 0, me); + } + }; + } + + me.callParent(); + + var grid; + if (!me.selModel && me.selModel !== null) { + grid = me.up('grid'); + if (grid && grid.selModel) { + me.selModel = grid.selModel; + } + } + + if (me.waitMsgTarget === true) { + grid = me.up('grid'); + if (grid) { + me.waitMsgTarget = grid; + } else { + throw "unable to find waitMsgTarget"; + } + } + + if (me.selModel) { + + me.mon(me.selModel, "selectionchange", function() { + var rec = me.selModel.getSelection()[0]; + if (!rec || (me.enableFn(rec) === false)) { + me.setDisabled(true); + } else { + me.setDisabled(false); + } + }); + } + } +}); + + +Ext.define('Proxmox.button.StdRemoveButton', { + extend: 'Proxmox.button.Button', + alias: 'widget.proxmoxStdRemoveButton', + + text: gettext('Remove'), + + disabled: true, + + config: { + baseurl: undefined + }, + + getUrl: function(rec) { + var me = this; + + return me.baseurl + '/' + rec.getId(); + }, + + // also works with names scopes + callback: function(options, success, response) {}, + + getRecordName: function(rec) { return rec.getId() }, + + confirmMsg: function (rec) { + var me = this; + + var name = me.getRecordName(rec); + return Ext.String.format( + gettext('Are you sure you want to remove entry {0}'), + "'" + name + "'"); + }, + + handler: function(btn, event, rec) { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.getUrl(rec), + method: 'DELETE', + waitMsgTarget: me.waitMsgTarget, + callback: function(options, success, response) { + Ext.callback(me.callback, me.scope, [options, success, response], 0, me); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } +}); +/* help button pointing to an online documentation + for components contained in a modal window +*/ +/*global + proxmoxOnlineHelpInfo +*/ +Ext.define('Proxmox.button.Help', { + extend: 'Ext.button.Button', + xtype: 'proxmoxHelpButton', + + text: gettext('Help'), + + // make help button less flashy by styling it like toolbar buttons + iconCls: ' x-btn-icon-el-default-toolbar-small fa fa-question-circle', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + + hidden: true, + + listenToGlobalEvent: true, + + controller: { + xclass: 'Ext.app.ViewController', + listen: { + global: { + proxmoxShowHelp: 'onProxmoxShowHelp', + proxmoxHideHelp: 'onProxmoxHideHelp' + } + }, + onProxmoxShowHelp: function(helpLink) { + var me = this.getView(); + if (me.listenToGlobalEvent === true) { + me.setOnlineHelp(helpLink); + me.show(); + } + }, + onProxmoxHideHelp: function() { + var me = this.getView(); + if (me.listenToGlobalEvent === true) { + me.hide(); + } + } + }, + + // this sets the link and the tooltip text + setOnlineHelp:function(blockid) { + var me = this; + + var info = Proxmox.Utils.get_help_info(blockid); + if (info) { + me.onlineHelp = blockid; + var title = info.title; + if (info.subtitle) { + title += ' - ' + info.subtitle; + } + me.setTooltip(title); + } + }, + + // helper to set the onlineHelp via a config object + setHelpConfig: function(config) { + var me = this; + me.setOnlineHelp(config.onlineHelp); + }, + + handler: function() { + var me = this; + var docsURI; + + if (me.onlineHelp) { + docsURI = Proxmox.Utils.get_help_link(me.onlineHelp); + } + + if (docsURI) { + window.open(docsURI); + } else { + Ext.Msg.alert(gettext('Help'), gettext('No Help available')); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + me.callParent(); + + if (me.onlineHelp) { + me.setOnlineHelp(me.onlineHelp); // set tooltip + } + } +}); +/* Renders a list of key values objets + +mandatory config parameters: +rows: an object container where each propery is a key-value object we want to render + var rows = { + keyboard: { + header: gettext('Keyboard Layout'), + editor: 'Your.KeyboardEdit', + required: true + }, + +optional: +disabled: setting this parameter to true will disable selection and focus on the +proxmoxObjectGrid as well as greying out input elements. +Useful for a readonly tabular display + +*/ + +Ext.define('Proxmox.grid.ObjectGrid', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.proxmoxObjectGrid'], + disabled: false, + hideHeaders: true, + + monStoreErrors: false, + + add_combobox_row: function(name, text, opts) { + var me = this; + + opts = opts || {}; + me.rows = me.rows || {}; + + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue, + header: text, + renderer: opts.renderer, + editor: { + xtype: 'proxmoxWindowEdit', + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + items: { + xtype: 'proxmoxKVComboBox', + name: name, + comboItems: opts.comboItems, + value: opts.defaultValue, + deleteEmpty: opts.deleteEmpty ? true : false, + emptyText: opts.defaultValue, + labelWidth: Proxmox.Utils.compute_min_label_width( + text, opts.labelWidth), + fieldLabel: text + } + } + }; + }, + + add_text_row: function(name, text, opts) { + var me = this; + + opts = opts || {}; + me.rows = me.rows || {}; + + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue, + header: text, + renderer: opts.renderer, + editor: { + xtype: 'proxmoxWindowEdit', + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + items: { + xtype: 'proxmoxtextfield', + name: name, + deleteEmpty: opts.deleteEmpty ? true : false, + emptyText: opts.defaultValue, + labelWidth: Proxmox.Utils.compute_min_label_width( + text, opts.labelWidth), + vtype: opts.vtype, + fieldLabel: text + } + } + }; + }, + + add_boolean_row: function(name, text, opts) { + var me = this; + + opts = opts || {}; + me.rows = me.rows || {}; + + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue || 0, + header: text, + renderer: opts.renderer || Proxmox.Utils.format_boolean, + editor: { + xtype: 'proxmoxWindowEdit', + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + items: { + xtype: 'proxmoxcheckbox', + name: name, + uncheckedValue: 0, + defaultValue: opts.defaultValue || 0, + checked: opts.defaultValue ? true : false, + deleteDefaultValue: opts.deleteDefaultValue ? true : false, + labelWidth: Proxmox.Utils.compute_min_label_width( + text, opts.labelWidth), + fieldLabel: text + } + } + }; + }, + + add_integer_row: function(name, text, opts) { + var me = this; + + opts = opts || {} + me.rows = me.rows || {}; + + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue, + header: text, + renderer: opts.renderer, + editor: { + xtype: 'proxmoxWindowEdit', + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + items: { + xtype: 'proxmoxintegerfield', + name: name, + minValue: opts.minValue, + maxValue: opts.maxValue, + emptyText: gettext('Default'), + deleteEmpty: opts.deleteEmpty ? true : false, + value: opts.defaultValue, + labelWidth: Proxmox.Utils.compute_min_label_width( + text, opts.labelWidth), + fieldLabel: text + } + } + }; + }, + + editorConfig: {}, // default config passed to editor + + run_editor: function() { + var me = this; + + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rows = me.rows; + var rowdef = rows[rec.data.key]; + if (!rowdef.editor) { + return; + } + + var win; + var config; + if (Ext.isString(rowdef.editor)) { + config = Ext.apply({ + confid: rec.data.key, + }, me.editorConfig); + win = Ext.create(rowdef.editor, config); + } else { + config = Ext.apply({ + confid: rec.data.key, + }, me.editorConfig); + Ext.apply(config, rowdef.editor); + win = Ext.createWidget(rowdef.editor.xtype, config); + win.load(); + } + + win.show(); + win.on('destroy', me.reload, me); + }, + + reload: function() { + var me = this; + me.rstore.load(); + }, + + getObjectValue: function(key, defaultValue) { + var me = this; + var rec = me.store.getById(key); + if (rec) { + return rec.data.value; + } + return defaultValue; + }, + + renderKey: function(key, metaData, record, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = (rows && rows[key]) ? rows[key] : {}; + return rowdef.header || key; + }, + + renderValue: function(value, metaData, record, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var key = record.data.key; + var rowdef = (rows && rows[key]) ? rows[key] : {}; + + var renderer = rowdef.renderer; + if (renderer) { + return renderer(value, metaData, record, rowIndex, colIndex, store); + } + + return value; + }, + + listeners: { + itemkeydown: function(view, record, item, index, e) { + if (e.getKey() === e.ENTER) { + this.pressedIndex = index; + } + }, + itemkeyup: function(view, record, item, index, e) { + if (e.getKey() === e.ENTER && index == this.pressedIndex) { + this.run_editor(); + } + + this.pressedIndex = undefined; + } + }, + + initComponent : function() { + var me = this; + + var rows = me.rows; + + if (!me.rstore) { + if (!me.url) { + throw "no url specified"; + } + + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + url: me.url, + interval: me.interval, + extraParams: me.extraParams, + rows: me.rows + }); + } + + var rstore = me.rstore; + + var store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore, + sorters: [], + filters: [] + }); + + if (rows) { + Ext.Object.each(rows, function(key, rowdef) { + if (Ext.isDefined(rowdef.defaultValue)) { + store.add({ key: key, value: rowdef.defaultValue }); + } else if (rowdef.required) { + store.add({ key: key, value: undefined }); + } + }); + } + + if (me.sorterFn) { + store.sorters.add(Ext.create('Ext.util.Sorter', { + sorterFn: me.sorterFn + })); + } + + store.filters.add(Ext.create('Ext.util.Filter', { + filterFn: function(item) { + if (rows) { + var rowdef = rows[item.data.key]; + if (!rowdef || (rowdef.visible === false)) { + return false; + } + } + return true; + } + })); + + Proxmox.Utils.monStoreErrors(me, rstore); + + Ext.applyIf(me, { + store: store, + stateful: false, + columns: [ + { + header: gettext('Name'), + width: me.cwidth1 || 200, + dataIndex: 'key', + renderer: me.renderKey + }, + { + flex: 1, + header: gettext('Value'), + dataIndex: 'value', + renderer: me.renderValue + } + ] + }); + + me.callParent(); + + if (me.monStoreErrors) { + Proxmox.Utils.monStoreErrors(me, me.store); + } + } +}); +Ext.define('Proxmox.grid.PendingObjectGrid', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.proxmoxPendingObjectGrid'], + + getObjectValue: function(key, defaultValue, pending) { + var me = this; + var rec = me.store.getById(key); + if (rec) { + var value = rec.data.value; + if (pending) { + if (Ext.isDefined(rec.data.pending) && rec.data.pending !== '') { + value = rec.data.pending; + } else if (rec.data['delete'] === 1) { + value = defaultValue; + } + } + + if (Ext.isDefined(value) && (value !== '')) { + return value; + } else { + return defaultValue; + } + } + return defaultValue; + }, + + hasPendingChanges: function(key) { + var me = this; + var rows = me.rows; + var rowdef = (rows && rows[key]) ? rows[key] : {}; + var keys = rowdef.multiKey || [ key ]; + var pending = false; + + Ext.Array.each(keys, function(k) { + var rec = me.store.getById(k); + if (rec && rec.data && ( + (Ext.isDefined(rec.data.pending) && rec.data.pending !== '') || + rec.data['delete'] === 1 + )) { + pending = true; + return false; // break + } + }); + + return pending; + }, + + renderValue: function(value, metaData, record, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var key = record.data.key; + var rowdef = (rows && rows[key]) ? rows[key] : {}; + var renderer = rowdef.renderer; + var current = ''; + var pendingdelete = ''; + var pending = ''; + + if (renderer) { + current = renderer(value, metaData, record, rowIndex, colIndex, store, false); + if (me.hasPendingChanges(key)) { + pending = renderer(record.data.pending, metaData, record, rowIndex, colIndex, store, true); + } + if (pending == current) { + pending = undefined; + } + } else { + current = value || ''; + pending = record.data.pending; + } + + if (record.data['delete']) { + var delete_all = true; + if (rowdef.multiKey) { + Ext.Array.each(rowdef.multiKey, function(k) { + var rec = me.store.getById(k); + if (rec && rec.data && rec.data['delete'] !== 1) { + delete_all = false; + return false; // break + } + }); + } + if (delete_all) { + pending = '
'+ current +'
'; + } + } + + if (pending) { + return current + '
' + pending + '
'; + } else { + return current; + } + }, + + initComponent : function() { + var me = this; + + var rows = me.rows; + + if (!me.rstore) { + if (!me.url) { + throw "no url specified"; + } + + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + model: 'KeyValuePendingDelete', + readArray: true, + url: me.url, + interval: me.interval, + extraParams: me.extraParams, + rows: me.rows + }); + } + + me.callParent(); + } +}); +Ext.define('Proxmox.panel.InputPanel', { + extend: 'Ext.panel.Panel', + alias: ['widget.inputpanel'], + listeners: { + activate: function() { + // notify owning container that it should display a help button + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp); + } + }, + deactivate: function() { + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp); + } + } + }, + border: false, + + // override this with an URL to a relevant chapter of the pve manual + // setting this will display a help button in our parent panel + onlineHelp: undefined, + + // will be set if the inputpanel has advanced items + hasAdvanced: false, + + // if the panel has advanced items, + // this will determine if they are shown by default + showAdvanced: false, + + // overwrite this to modify submit data + onGetValues: function(values) { + return values; + }, + + getValues: function(dirtyOnly) { + var me = this; + + if (Ext.isFunction(me.onGetValues)) { + dirtyOnly = false; + } + + var values = {}; + + Ext.Array.each(me.query('[isFormField]'), function(field) { + if (!dirtyOnly || field.isDirty()) { + Proxmox.Utils.assemble_field_data(values, field.getSubmitData()); + } + }); + + return me.onGetValues(values); + }, + + setAdvancedVisible: function(visible) { + var me = this; + var advItems = me.getComponent('advancedContainer'); + if (advItems) { + advItems.setVisible(visible); + } + }, + + setValues: function(values) { + var me = this; + + var form = me.up('form'); + + Ext.iterate(values, function(fieldId, val) { + var field = me.query('[isFormField][name=' + fieldId + ']')[0]; + if (field) { + field.setValue(val); + if (form.trackResetOnLoad) { + field.resetOriginalValue(); + } + } + }); + }, + + initComponent: function() { + var me = this; + + var items; + + if (me.items) { + me.columns = 1; + items = [ + { + columnWidth: 1, + layout: 'anchor', + items: me.items + } + ]; + me.items = undefined; + } else if (me.column4) { + me.columns = 4; + items = [ + { + columnWidth: 0.25, + padding: '0 10 0 0', + layout: 'anchor', + items: me.column1 + }, + { + columnWidth: 0.25, + padding: '0 10 0 0', + layout: 'anchor', + items: me.column2 + }, + { + columnWidth: 0.25, + padding: '0 10 0 0', + layout: 'anchor', + items: me.column3 + }, + { + columnWidth: 0.25, + padding: '0 0 0 10', + layout: 'anchor', + items: me.column4 + } + ]; + if (me.columnB) { + items.push({ + columnWidth: 1, + padding: '10 0 0 0', + layout: 'anchor', + items: me.columnB + }); + } + } else if (me.column1) { + me.columns = 2; + items = [ + { + columnWidth: 0.5, + padding: '0 10 0 0', + layout: 'anchor', + items: me.column1 + }, + { + columnWidth: 0.5, + padding: '0 0 0 10', + layout: 'anchor', + items: me.column2 || [] // allow empty column + } + ]; + if (me.columnB) { + items.push({ + columnWidth: 1, + padding: '10 0 0 0', + layout: 'anchor', + items: me.columnB + }); + } + } else { + throw "unsupported config"; + } + + var advItems; + if (me.advancedItems) { + advItems = [ + { + columnWidth: 1, + layout: 'anchor', + items: me.advancedItems + } + ]; + me.advancedItems = undefined; + } else if (me.advancedColumn1) { + advItems = [ + { + columnWidth: 0.5, + padding: '0 10 0 0', + layout: 'anchor', + items: me.advancedColumn1 + }, + { + columnWidth: 0.5, + padding: '0 0 0 10', + layout: 'anchor', + items: me.advancedColumn2 || [] // allow empty column + } + ]; + + me.advancedColumn1 = undefined; + me.advancedColumn2 = undefined; + + if (me.advancedColumnB) { + advItems.push({ + columnWidth: 1, + padding: '10 0 0 0', + layout: 'anchor', + items: me.advancedColumnB + }); + me.advancedColumnB = undefined; + } + } + + if (advItems) { + me.hasAdvanced = true; + advItems.unshift({ + columnWidth: 1, + xtype: 'box', + hidden: false, + border: true, + autoEl: { + tag: 'hr' + } + }); + items.push({ + columnWidth: 1, + xtype: 'container', + itemId: 'advancedContainer', + hidden: !me.showAdvanced, + layout: 'column', + defaults: { + border: false + }, + items: advItems + }); + } + + if (me.useFieldContainer) { + Ext.apply(me, { + layout: 'fit', + items: Ext.apply(me.useFieldContainer, { + layout: 'column', + defaultType: 'container', + items: items + }) + }); + } else { + Ext.apply(me, { + layout: 'column', + defaultType: 'container', + items: items + }); + } + + me.callParent(); + } +}); +/* + * Display log entries in a panel with scrollbar + * The log entries are automatically refreshed via a background task, + * with newest entries comming at the bottom + */ +Ext.define('Proxmox.panel.LogView', { + extend: 'Ext.panel.Panel', + xtype: 'proxmoxLogView', + + pageSize: 500, + viewBuffer: 50, + lineHeight: 16, + + scrollToEnd: true, + + // callback for load failure, used for ceph + failCallback: undefined, + + controller: { + xclass: 'Ext.app.ViewController', + + updateParams: function() { + var me = this; + var viewModel = me.getViewModel(); + var since = viewModel.get('since'); + var until = viewModel.get('until'); + if (viewModel.get('hide_timespan')) { + return; + } + + if (since > until) { + Ext.Msg.alert('Error', 'Since date must be less equal than Until date.'); + return; + } + + viewModel.set('params.since', Ext.Date.format(since, 'Y-m-d')); + viewModel.set('params.until', Ext.Date.format(until, 'Y-m-d') + ' 23:59:59'); + me.getView().loadTask.delay(200); + }, + + scrollPosBottom: function() { + var view = this.getView(); + var pos = view.getScrollY(); + var maxPos = view.getScrollable().getMaxPosition().y; + return maxPos - pos; + }, + + updateView: function(text, first, total) { + var me = this; + var view = me.getView(); + var viewModel = me.getViewModel(); + var content = me.lookup('content'); + var data = viewModel.get('data'); + + if (first === data.first && total === data.total && text.length === data.textlen) { + return; // same content, skip setting and scrolling + } + viewModel.set('data', { + first: first, + total: total, + textlen: text.length + }); + + var scrollPos = me.scrollPosBottom(); + + content.update(text); + + if (view.scrollToEnd && scrollPos <= 0) { + // we use setTimeout to work around scroll handling on touchscreens + setTimeout(function() { view.scrollTo(0, Infinity); }, 10); + } + }, + + doLoad: function() { + var me = this; + if (me.running) { + me.requested = true; + return; + } + me.running = true; + var view = me.getView(); + var viewModel = me.getViewModel(); + Proxmox.Utils.API2Request({ + url: me.getView().url, + params: viewModel.get('params'), + method: 'GET', + success: function(response) { + Proxmox.Utils.setErrorMask(me, false); + var total = response.result.total; + var lines = new Array(); + var first = Infinity; + + Ext.Array.each(response.result.data, function(line) { + if (first > line.n) { + first = line.n; + } + lines[line.n - 1] = Ext.htmlEncode(line.t); + }); + + lines.length = total; + me.updateView(lines.join('
'), first - 1, total); + me.running = false; + if (me.requested) { + me.requested = false; + view.loadTask.delay(200); + } + }, + failure: function(response) { + if (view.failCallback) { + view.failCallback(response); + } else { + var msg = response.htmlStatus; + Proxmox.Utils.setErrorMask(me, msg); + } + me.running = false; + if (me.requested) { + me.requested = false; + view.loadTask.delay(200); + } + } + }); + }, + + onScroll: function(x, y) { + var me = this; + var view = me.getView(); + var viewModel = me.getViewModel(); + + var lineHeight = view.lineHeight; + var line = view.getScrollY()/lineHeight; + var start = viewModel.get('params.start'); + var limit = viewModel.get('params.limit'); + var viewLines = view.getHeight()/lineHeight; + + var viewStart = Math.max(parseInt(line - 1 - view.viewBuffer, 10), 0); + var viewEnd = parseInt(line + viewLines + 1 + view.viewBuffer, 10); + + if (viewStart < start || viewEnd > (start+limit)) { + viewModel.set('params.start', + Math.max(parseInt(line - limit/2 + 10, 10), 0)); + view.loadTask.delay(200); + } + }, + + init: function(view) { + var me = this; + + if (!view.url) { + throw "no url specified"; + } + + var viewModel = this.getViewModel(); + var since = new Date(); + since.setDate(since.getDate() - 3); + viewModel.set('until', new Date()); + viewModel.set('since', since); + viewModel.set('params.limit', view.pageSize); + viewModel.set('hide_timespan', !view.log_select_timespan); + me.lookup('content').setStyle('line-height', view.lineHeight + 'px'); + + view.loadTask = new Ext.util.DelayedTask(me.doLoad, me); + + me.updateParams(); + view.task = Ext.TaskManager.start({ + run: function() { + if (!view.isVisible() || !view.scrollToEnd) { + return; + } + + if (me.scrollPosBottom() <= 1) { + view.loadTask.delay(200); + } + }, + interval: 1000 + }); + } + }, + + onDestroy: function() { + var me = this; + me.loadTask.cancel(); + Ext.TaskManager.stop(me.task); + }, + + // for user to initiate a load from outside + requestUpdate: function() { + var me = this; + me.loadTask.delay(200); + }, + + viewModel: { + data: { + until: null, + since: null, + hide_timespan: false, + data: { + start: 0, + total: 0, + textlen: 0 + }, + params: { + start: 0, + limit: 500, + } + } + }, + + layout: 'auto', + bodyPadding: 5, + scrollable: { + x: 'auto', + y: 'auto', + listeners: { + // we have to have this here, since we cannot listen to events + // of the scroller in the viewcontroller (extjs bug?), nor does + // the panel have a 'scroll' event' + scroll: { + fn: function(scroller, x, y) { + var controller = this.component.getController(); + if (controller) { // on destroy, controller can be gone + controller.onScroll(x,y); + } + }, + buffer: 200 + }, + } + }, + + tbar: { + bind: { + hidden: '{hide_timespan}' + }, + items: [ + '->', + 'Since: ', + { + xtype: 'datefield', + name: 'since_date', + reference: 'since', + format: 'Y-m-d', + bind: { + value: '{since}', + maxValue: '{until}' + } + }, + 'Until: ', + { + xtype: 'datefield', + name: 'until_date', + reference: 'until', + format: 'Y-m-d', + bind: { + value: '{until}', + minValue: '{since}' + } + }, + { + xtype: 'button', + text: 'Update', + handler: 'updateParams' + } + ], + }, + + items: [ + { + xtype: 'box', + reference: 'content', + style: { + font: 'normal 11px tahoma, arial, verdana, sans-serif', + 'white-space': 'pre' + }, + } + ] +}); +/* + * Display log entries in a panel with scrollbar + * The log entries are automatically refreshed via a background task, + * with newest entries comming at the bottom + */ +Ext.define('Proxmox.panel.JournalView', { + extend: 'Ext.panel.Panel', + xtype: 'proxmoxJournalView', + + numEntries: 500, + lineHeight: 16, + + scrollToEnd: true, + + controller: { + xclass: 'Ext.app.ViewController', + + updateParams: function() { + var me = this; + var viewModel = me.getViewModel(); + var since = viewModel.get('since'); + var until = viewModel.get('until'); + + since.setHours(0, 0, 0, 0); + until.setHours(0, 0, 0, 0); + until.setDate(until.getDate()+1); + + me.getView().loadTask.delay(200, undefined, undefined, [ + false, + false, + Ext.Date.format(since, "U"), + Ext.Date.format(until, "U") + ]); + }, + + scrollPosBottom: function() { + var view = this.getView(); + var pos = view.getScrollY(); + var maxPos = view.getScrollable().getMaxPosition().y; + return maxPos - pos; + }, + + scrollPosTop: function() { + var view = this.getView(); + return view.getScrollY(); + }, + + updateScroll: function(livemode, num, scrollPos, scrollPosTop) { + var me = this; + var view = me.getView(); + + if (!livemode) { + setTimeout(function() { view.scrollTo(0, 0); }, 10); + } else if (view.scrollToEnd && scrollPos <= 0) { + setTimeout(function() { view.scrollTo(0, Infinity); }, 10); + } else if (!view.scrollToEnd && scrollPosTop < 20*view.lineHeight) { + setTimeout(function() { view.scrollTo(0, num*view.lineHeight + scrollPosTop); }, 10); + } + }, + + updateView: function(lines, livemode, top) { + var me = this; + var view = me.getView(); + var viewmodel = me.getViewModel(); + if (viewmodel.get('livemode') !== livemode) { + return; // we switched mode, do not update the content + } + var contentEl = me.lookup('content'); + + // save old scrollpositions + var scrollPos = me.scrollPosBottom(); + var scrollPosTop = me.scrollPosTop(); + + var newend = lines.shift(); + var newstart = lines.pop(); + + var num = lines.length; + var text = lines.map(Ext.htmlEncode).join('
'); + + if (!livemode) { + if (num) { + view.content = text; + } else { + view.content = 'nothing logged or no timespan selected'; + } + } else { + // update content + if (top && num) { + view.content = view.content ? text + '
' + view.content : text; + } else if (!top && num) { + view.content = view.content ? view.content + '
' + text : text; + } + + // update cursors + if (!top || !view.startcursor) { + view.startcursor = newstart; + } + + if (top || !view.endcursor) { + view.endcursor = newend; + } + } + + contentEl.update(view.content); + + me.updateScroll(livemode, num, scrollPos, scrollPosTop); + }, + + doLoad: function(livemode, top, since, until) { + var me = this; + if (me.running) { + me.requested = true; + return; + } + me.running = true; + var view = me.getView(); + var params = { + lastentries: view.numEntries || 500, + }; + if (livemode) { + if (!top && view.startcursor) { + params = { + startcursor: view.startcursor + }; + } else if (view.endcursor) { + params.endcursor = view.endcursor; + } + } else { + params = { + since: since, + until: until + }; + } + Proxmox.Utils.API2Request({ + url: view.url, + params: params, + waitMsgTarget: (!livemode) ? view : undefined, + method: 'GET', + success: function(response) { + Proxmox.Utils.setErrorMask(me, false); + var lines = response.result.data; + me.updateView(lines, livemode, top); + me.running = false; + if (me.requested) { + me.requested = false; + view.loadTask.delay(200); + } + }, + failure: function(response) { + var msg = response.htmlStatus; + Proxmox.Utils.setErrorMask(me, msg); + me.running = false; + if (me.requested) { + me.requested = false; + view.loadTask.delay(200); + } + } + }); + }, + + onScroll: function(x, y) { + var me = this; + var view = me.getView(); + var viewmodel = me.getViewModel(); + var livemode = viewmodel.get('livemode'); + if (!livemode) { + return; + } + + if (me.scrollPosTop() < 20*view.lineHeight) { + view.scrollToEnd = false; + view.loadTask.delay(200, undefined, undefined, [true, true]); + } else if (me.scrollPosBottom() <= 1) { + view.scrollToEnd = true; + } + }, + + init: function(view) { + var me = this; + + if (!view.url) { + throw "no url specified"; + } + + var viewmodel = me.getViewModel(); + var viewModel = this.getViewModel(); + var since = new Date(); + since.setDate(since.getDate() - 3); + viewModel.set('until', new Date()); + viewModel.set('since', since); + me.lookup('content').setStyle('line-height', view.lineHeight + 'px'); + + view.loadTask = new Ext.util.DelayedTask(me.doLoad, me, [true, false]); + + me.updateParams(); + view.task = Ext.TaskManager.start({ + run: function() { + if (!view.isVisible() || !view.scrollToEnd || !viewmodel.get('livemode')) { + return; + } + + if (me.scrollPosBottom() <= 1) { + view.loadTask.delay(200, undefined, undefined, [true, false]); + } + }, + interval: 1000 + }); + }, + + onLiveMode: function() { + var me = this; + var view = me.getView(); + delete view.startcursor; + delete view.endcursor; + delete view.content; + me.getViewModel().set('livemode', true); + view.scrollToEnd = true; + me.updateView([], true, false); + }, + + onTimespan: function() { + var me = this; + me.getViewModel().set('livemode', false); + me.updateView([], false); + } + }, + + onDestroy: function() { + var me = this; + me.loadTask.cancel(); + Ext.TaskManager.stop(me.task); + delete me.content; + }, + + // for user to initiate a load from outside + requestUpdate: function() { + var me = this; + me.loadTask.delay(200); + }, + + viewModel: { + data: { + livemode: true, + until: null, + since: null + } + }, + + layout: 'auto', + bodyPadding: 5, + scrollable: { + x: 'auto', + y: 'auto', + listeners: { + // we have to have this here, since we cannot listen to events + // of the scroller in the viewcontroller (extjs bug?), nor does + // the panel have a 'scroll' event' + scroll: { + fn: function(scroller, x, y) { + var controller = this.component.getController(); + if (controller) { // on destroy, controller can be gone + controller.onScroll(x,y); + } + }, + buffer: 200 + }, + } + }, + + tbar: { + + items: [ + '->', + { + xtype: 'segmentedbutton', + items: [ + { + text: gettext('Live Mode'), + bind: { + pressed: '{livemode}' + }, + handler: 'onLiveMode', + }, + { + text: gettext('Select Timespan'), + bind: { + pressed: '{!livemode}' + }, + handler: 'onTimespan', + } + ] + }, + { + xtype: 'box', + bind: { disabled: '{livemode}' }, + autoEl: { cn: gettext('Since') + ':' } + }, + { + xtype: 'datefield', + name: 'since_date', + reference: 'since', + format: 'Y-m-d', + bind: { + disabled: '{livemode}', + value: '{since}', + maxValue: '{until}' + } + }, + { + xtype: 'box', + bind: { disabled: '{livemode}' }, + autoEl: { cn: gettext('Until') + ':' } + }, + { + xtype: 'datefield', + name: 'until_date', + reference: 'until', + format: 'Y-m-d', + bind: { + disabled: '{livemode}', + value: '{until}', + minValue: '{since}' + } + }, + { + xtype: 'button', + text: 'Update', + reference: 'updateBtn', + handler: 'updateParams', + bind: { + disabled: '{livemode}' + } + } + ] + }, + + items: [ + { + xtype: 'box', + reference: 'content', + style: { + font: 'normal 11px tahoma, arial, verdana, sans-serif', + 'white-space': 'pre' + }, + } + ] +}); +Ext.define('Proxmox.widget.RRDChart', { + extend: 'Ext.chart.CartesianChart', + alias: 'widget.proxmoxRRDChart', + + unit: undefined, // bytes, bytespersecond, percent + + controller: { + xclass: 'Ext.app.ViewController', + + convertToUnits: function(value) { + var units = ['', 'k','M','G','T', 'P']; + var si = 0; + while(value >= 1000 && si < (units.length -1)){ + value = value / 1000; + si++; + } + + // javascript floating point weirdness + value = Ext.Number.correctFloat(value); + + // limit to 2 decimal points + value = Ext.util.Format.number(value, "0.##"); + + return value.toString() + " " + units[si]; + }, + + leftAxisRenderer: function(axis, label, layoutContext) { + var me = this; + + return me.convertToUnits(label); + }, + + onSeriesTooltipRender: function(tooltip, record, item) { + var me = this.getView(); + + var suffix = ''; + + if (me.unit === 'percent') { + suffix = '%'; + } else if (me.unit === 'bytes') { + suffix = 'B'; + } else if (me.unit === 'bytespersecond') { + suffix = 'B/s'; + } + + var prefix = item.field; + if (me.fieldTitles && me.fieldTitles[me.fields.indexOf(item.field)]) { + prefix = me.fieldTitles[me.fields.indexOf(item.field)]; + } + tooltip.setHtml(prefix + ': ' + this.convertToUnits(record.get(item.field)) + suffix + + '
' + new Date(record.get('time'))); + }, + + onAfterAnimation: function(chart, eopts) { + // if the undobuton is disabled, + // disable our tool + + var ourUndoZoomButton = chart.tools[0]; + var undoButton = chart.interactions[0].getUndoButton(); + ourUndoZoomButton.setDisabled(undoButton.isDisabled()); + } + }, + + width: 770, + height: 300, + animation: false, + interactions: [{ + type: 'crosszoom' + }], + axes: [{ + type: 'numeric', + position: 'left', + grid: true, + renderer: 'leftAxisRenderer', + //renderer: function(axis, label) { return label; }, + minimum: 0 + }, { + type: 'time', + position: 'bottom', + grid: true, + fields: ['time'] + }], + legend: { + docked: 'bottom' + }, + listeners: { + animationend: 'onAfterAnimation' + }, + + + initComponent: function() { + var me = this; + var series = {}; + + if (!me.store) { + throw "cannot work without store"; + } + + if (!me.fields) { + throw "cannot work without fields"; + } + + me.callParent(); + + // add correct label for left axis + var axisTitle = ""; + if (me.unit === 'percent') { + axisTitle = "%"; + } else if (me.unit === 'bytes') { + axisTitle = "Bytes"; + } else if (me.unit === 'bytespersecond') { + axisTitle = "Bytes/s"; + } else if (me.fieldTitles && me.fieldTitles.length === 1) { + axisTitle = me.fieldTitles[0]; + } else if (me.fields.length === 1) { + axisTitle = me.fields[0]; + } + + me.axes[0].setTitle(axisTitle); + + if (!me.noTool) { + me.addTool([{ + type: 'minus', + disabled: true, + tooltip: gettext('Undo Zoom'), + handler: function(){ + var undoButton = me.interactions[0].getUndoButton(); + if (undoButton.handler) { + undoButton.handler(); + } + } + },{ + type: 'restore', + tooltip: gettext('Toggle Legend'), + handler: function(){ + if (me.legend) { + me.legend.setVisible(!me.legend.isVisible()); + } + } + }]); + } + + // add a series for each field we get + me.fields.forEach(function(item, index){ + var title = item; + if (me.fieldTitles && me.fieldTitles[index]) { + title = me.fieldTitles[index]; + } + me.addSeries(Ext.apply( + { + type: 'line', + xField: 'time', + yField: item, + title: title, + fill: true, + style: { + lineWidth: 1.5, + opacity: 0.60 + }, + marker: { + opacity: 0, + scaling: 0.01, + fx: { + duration: 200, + easing: 'easeOut' + } + }, + highlightCfg: { + opacity: 1, + scaling: 1.5 + }, + tooltip: { + trackMouse: true, + renderer: 'onSeriesTooltipRender' + } + }, + me.seriesConfig + )); + }); + + // enable animation after the store is loaded + me.store.onAfter('load', function() { + me.setAnimation(true); + }, this, {single: true}); + } +}); +Ext.define('Proxmox.panel.GaugeWidget', { + extend: 'Ext.panel.Panel', + alias: 'widget.proxmoxGauge', + + defaults: { + style: { + 'text-align':'center' + } + }, + items: [ + { + xtype: 'box', + itemId: 'title', + data: { + title: '' + }, + tpl: '

{title}

' + }, + { + xtype: 'polar', + height: 120, + border: false, + itemId: 'chart', + series: [{ + type: 'gauge', + value: 0, + colors: ['#f5f5f5'], + sectors: [0], + donut: 90, + needleLength: 100, + totalAngle: Math.PI + }], + sprites: [{ + id: 'valueSprite', + type: 'text', + text: '', + textAlign: 'center', + textBaseline: 'bottom', + x: 125, + y: 110, + fontSize: 30 + }] + }, + { + xtype: 'box', + itemId: 'text' + } + ], + + header: false, + border: false, + + warningThreshold: 0.6, + criticalThreshold: 0.9, + warningColor: '#fc0', + criticalColor: '#FF6C59', + defaultColor: '#c2ddf2', + backgroundColor: '#f5f5f5', + + initialValue: 0, + + + updateValue: function(value, text) { + var me = this; + var color = me.defaultColor; + var attr = {}; + + if (value >= me.criticalThreshold) { + color = me.criticalColor; + } else if (value >= me.warningThreshold) { + color = me.warningColor; + } + + me.chart.series[0].setColors([color, me.backgroundColor]); + me.chart.series[0].setValue(value*100); + + me.valueSprite.setText(' '+(value*100).toFixed(0) + '%'); + attr.x = me.chart.getWidth()/2; + attr.y = me.chart.getHeight()-20; + if (me.spriteFontSize) { + attr.fontSize = me.spriteFontSize; + } + me.valueSprite.setAttributes(attr, true); + + if (text !== undefined) { + me.text.setHtml(text); + } + }, + + initComponent: function() { + var me = this; + + me.callParent(); + + if (me.title) { + me.getComponent('title').update({title: me.title}); + } + me.text = me.getComponent('text'); + me.chart = me.getComponent('chart'); + me.valueSprite = me.chart.getSurface('chart').get('valueSprite'); + } +}); +// fixme: how can we avoid those lint errors? +/*jslint confusion: true */ +Ext.define('Proxmox.window.Edit', { + extend: 'Ext.window.Window', + alias: 'widget.proxmoxWindowEdit', + + // autoLoad trigger a load() after component creation + autoLoad: false, + + resizable: false, + + // use this tio atimatically generate a title like + // Create: + subject: undefined, + + // set isCreate to true if you want a Create button (instead + // OK and RESET) + isCreate: false, + + // set to true if you want an Add button (instead of Create) + isAdd: false, + + // set to true if you want an Remove button (instead of Create) + isRemove: false, + + // custom submitText + submitText: undefined, + + backgroundDelay: 0, + + // needed for finding the reference to submitbutton + // because we do not have a controller + referenceHolder: true, + defaultButton: 'submitbutton', + + // finds the first form field + defaultFocus: 'field[disabled=false][hidden=false]', + + showProgress: false, + + showTaskViewer: false, + + // gets called if we have a progress bar or taskview and it detected that + // the task finished. function(success) + taskDone: Ext.emptyFn, + + // gets called when the api call is finished, right at the beginning + // function(success, response, options) + apiCallDone: Ext.emptyFn, + + // assign a reference from docs, to add a help button docked to the + // bottom of the window. If undefined we magically fall back to the + // onlineHelp of our first item, if set. + onlineHelp: undefined, + + isValid: function() { + var me = this; + + var form = me.formPanel.getForm(); + return form.isValid(); + }, + + getValues: function(dirtyOnly) { + var me = this; + + var values = {}; + + var form = me.formPanel.getForm(); + + form.getFields().each(function(field) { + if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) { + Proxmox.Utils.assemble_field_data(values, field.getSubmitData()); + } + }); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly)); + }); + + return values; + }, + + setValues: function(values) { + var me = this; + + var form = me.formPanel.getForm(); + + Ext.iterate(values, function(fieldId, val) { + var field = form.findField(fieldId); + if (field && !field.up('inputpanel')) { + field.setValue(val); + if (form.trackResetOnLoad) { + field.resetOriginalValue(); + } + } + }); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + panel.setValues(values); + }); + }, + + submit: function() { + var me = this; + + var form = me.formPanel.getForm(); + + var values = me.getValues(); + Ext.Object.each(values, function(name, val) { + if (values.hasOwnProperty(name)) { + if (Ext.isArray(val) && !val.length) { + values[name] = ''; + } + } + }); + + if (me.digest) { + values.digest = me.digest; + } + + if (me.backgroundDelay) { + values.background_delay = me.backgroundDelay; + } + + var url = me.url; + if (me.method === 'DELETE') { + url = url + "?" + Ext.Object.toQueryString(values); + values = undefined; + } + + Proxmox.Utils.API2Request({ + url: url, + waitMsgTarget: me, + method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'), + params: values, + failure: function(response, options) { + me.apiCallDone(false, response, options); + + if (response.result && response.result.errors) { + form.markInvalid(response.result.errors); + } + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var hasProgressBar = (me.backgroundDelay || me.showProgress || me.showTaskViewer) && + response.result.data ? true : false; + + me.apiCallDone(true, response, options); + + if (hasProgressBar) { + // stay around so we can trigger our close events + // when background action is completed + me.hide(); + + var upid = response.result.data; + var viewerClass = me.showTaskViewer ? 'Viewer' : 'Progress'; + var win = Ext.create('Proxmox.window.Task' + viewerClass, { + upid: upid, + taskDone: me.taskDone, + listeners: { + destroy: function () { + me.close(); + } + } + }); + win.show(); + } else { + me.close(); + } + } + }); + }, + + load: function(options) { + var me = this; + + var form = me.formPanel.getForm(); + + options = options || {}; + + var newopts = Ext.apply({ + waitMsgTarget: me + }, options); + + var createWrapper = function(successFn) { + Ext.apply(newopts, { + url: me.url, + method: 'GET', + success: function(response, opts) { + form.clearInvalid(); + me.digest = response.result.data.digest; + if (successFn) { + successFn(response, opts); + } else { + me.setValues(response.result.data); + } + // hack: fix ExtJS bug + Ext.Array.each(me.query('radiofield'), function(f) { + f.resetOriginalValue(); + }); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus, function() { + me.close(); + }); + } + }); + }; + + createWrapper(options.success); + + Proxmox.Utils.API2Request(newopts); + }, + + initComponent : function() { + var me = this; + + if (!me.url) { + throw "no url specified"; + } + + if (me.create) {throw "deprecated parameter, use isCreate";} + + var items = Ext.isArray(me.items) ? me.items : [ me.items ]; + + me.items = undefined; + + me.formPanel = Ext.create('Ext.form.Panel', { + url: me.url, + method: me.method || 'PUT', + trackResetOnLoad: true, + bodyPadding: 10, + border: false, + defaults: Ext.apply({}, me.defaults, { + border: false + }), + fieldDefaults: Ext.apply({}, me.fieldDefaults, { + labelWidth: 100, + anchor: '100%' + }), + items: items + }); + + var inputPanel = me.formPanel.down('inputpanel'); + + var form = me.formPanel.getForm(); + + var submitText; + if (me.isCreate) { + if (me.submitText) { + submitText = me.submitText; + } else if (me.isAdd) { + submitText = gettext('Add'); + } else if (me.isRemove) { + submitText = gettext('Remove'); + } else { + submitText = gettext('Create'); + } + } else { + submitText = me.submitText || gettext('OK'); + } + + var submitBtn = Ext.create('Ext.Button', { + reference: 'submitbutton', + text: submitText, + disabled: !me.isCreate, + handler: function() { + me.submit(); + } + }); + + var resetBtn = Ext.create('Ext.Button', { + text: 'Reset', + disabled: true, + handler: function(){ + form.reset(); + } + }); + + var set_button_status = function() { + var valid = form.isValid(); + var dirty = form.isDirty(); + submitBtn.setDisabled(!valid || !(dirty || me.isCreate)); + resetBtn.setDisabled(!dirty); + + if (inputPanel && inputPanel.hasAdvanced) { + // we want to show the advanced options + // as soon as some of it is not valid + var advancedItems = me.down('#advancedContainer').query('field'); + var valid = true; + advancedItems.forEach(function(field) { + if (!field.isValid()) { + valid = false; + } + }); + + if (!valid) { + inputPanel.setAdvancedVisible(true); + me.down('#advancedcb').setValue(true); + } + } + }; + + form.on('dirtychange', set_button_status); + form.on('validitychange', set_button_status); + + var colwidth = 300; + if (me.fieldDefaults && me.fieldDefaults.labelWidth) { + colwidth += me.fieldDefaults.labelWidth - 100; + } + + var twoColumn = inputPanel && + (inputPanel.column1 || inputPanel.column2); + + if (me.subject && !me.title) { + me.title = Proxmox.Utils.dialog_title(me.subject, me.isCreate, me.isAdd); + } + + if (me.isCreate) { + me.buttons = [ submitBtn ] ; + } else { + me.buttons = [ submitBtn, resetBtn ]; + } + + if (inputPanel && inputPanel.hasAdvanced) { + var sp = Ext.state.Manager.getProvider(); + var advchecked = sp.get('proxmox-advanced-cb'); + inputPanel.setAdvancedVisible(advchecked); + me.buttons.unshift( + { + xtype: 'proxmoxcheckbox', + itemId: 'advancedcb', + boxLabelAlign: 'before', + boxLabel: gettext('Advanced'), + stateId: 'proxmox-advanced-cb', + value: advchecked, + listeners: { + change: function(cb, val) { + inputPanel.setAdvancedVisible(val); + sp.set('proxmox-advanced-cb', val); + } + } + } + ); + } + + var onlineHelp = me.onlineHelp; + if (!onlineHelp && inputPanel && inputPanel.onlineHelp) { + onlineHelp = inputPanel.onlineHelp; + } + + if (onlineHelp) { + var helpButton = Ext.create('Proxmox.button.Help'); + me.buttons.unshift(helpButton, '->'); + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', onlineHelp); + } + + Ext.applyIf(me, { + modal: true, + width: twoColumn ? colwidth*2 : colwidth, + border: false, + items: [ me.formPanel ] + }); + + me.callParent(); + + // always mark invalid fields + me.on('afterlayout', function() { + // on touch devices, the isValid function + // triggers a layout, which triggers an isValid + // and so on + // to prevent this we disable the layouting here + // and enable it afterwards + me.suspendLayout = true; + me.isValid(); + me.suspendLayout = false; + }); + + if (me.autoLoad) { + me.load(); + } + } +}); +Ext.define('Proxmox.window.PasswordEdit', { + extend: 'Proxmox.window.Edit', + alias: 'proxmoxWindowPasswordEdit', + + subject: gettext('Password'), + + url: '/api2/extjs/access/password', + + fieldDefaults: { + labelWidth: 120 + }, + + items: [ + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Password'), + minLength: 5, + allowBlank: false, + name: 'password', + listeners: { + change: function(field){ + field.next().validate(); + }, + blur: function(field){ + field.next().validate(); + } + } + }, + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Confirm password'), + name: 'verifypassword', + allowBlank: false, + vtype: 'password', + initialPassField: 'password', + submitValue: false + }, + { + xtype: 'hiddenfield', + name: 'userid' + } + ], + + initComponent : function() { + var me = this; + + if (!me.userid) { + throw "no userid specified"; + } + + me.callParent(); + me.down('[name=userid]').setValue(me.userid); + } +}); +Ext.define('Proxmox.window.TaskProgress', { + extend: 'Ext.window.Window', + alias: 'widget.proxmoxTaskProgress', + + taskDone: Ext.emptyFn, + + initComponent: function() { + var me = this; + + if (!me.upid) { + throw "no task specified"; + } + + var task = Proxmox.Utils.parse_task_upid(me.upid); + + var statstore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status", + interval: 1000, + rows: { + status: { defaultValue: 'unknown' }, + exitstatus: { defaultValue: 'unknown' } + } + }); + + me.on('destroy', statstore.stopUpdate); + + var getObjectValue = function(key, defaultValue) { + var rec = statstore.getById(key); + if (rec) { + return rec.data.value; + } + return defaultValue; + }; + + var pbar = Ext.create('Ext.ProgressBar', { text: 'running...' }); + + me.mon(statstore, 'load', function() { + var status = getObjectValue('status'); + if (status === 'stopped') { + var exitstatus = getObjectValue('exitstatus'); + if (exitstatus == 'OK') { + pbar.reset(); + pbar.updateText("Done!"); + Ext.Function.defer(me.close, 1000, me); + } else { + me.close(); + Ext.Msg.alert('Task failed', exitstatus); + } + me.taskDone(exitstatus == 'OK'); + } + }); + + var descr = Proxmox.Utils.format_task_description(task.type, task.id); + + Ext.apply(me, { + title: gettext('Task') + ': ' + descr, + width: 300, + layout: 'auto', + modal: true, + bodyPadding: 5, + items: pbar, + buttons: [ + { + text: gettext('Details'), + handler: function() { + var win = Ext.create('Proxmox.window.TaskViewer', { + taskDone: me.taskDone, + upid: me.upid + }); + win.show(); + me.close(); + } + } + ] + }); + + me.callParent(); + + statstore.startUpdate(); + + pbar.wait(); + } +}); + +// fixme: how can we avoid those lint errors? +/*jslint confusion: true */ + +Ext.define('Proxmox.window.TaskViewer', { + extend: 'Ext.window.Window', + alias: 'widget.proxmoxTaskViewer', + + extraTitle: '', // string to prepend after the generic task title + + taskDone: Ext.emptyFn, + + initComponent: function() { + var me = this; + + if (!me.upid) { + throw "no task specified"; + } + + var task = Proxmox.Utils.parse_task_upid(me.upid); + + var statgrid; + + var rows = { + status: { + header: gettext('Status'), + defaultValue: 'unknown', + renderer: function(value) { + if (value != 'stopped') { + return value; + } + var es = statgrid.getObjectValue('exitstatus'); + if (es) { + return value + ': ' + es; + } + } + }, + exitstatus: { + visible: false + }, + type: { + header: gettext('Task type'), + required: true + }, + user: { + header: gettext('User name'), + required: true + }, + node: { + header: gettext('Node'), + required: true + }, + pid: { + header: gettext('Process ID'), + required: true + }, + starttime: { + header: gettext('Start Time'), + required: true, + renderer: Proxmox.Utils.render_timestamp + }, + upid: { + header: gettext('Unique task ID') + } + }; + + var statstore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status", + interval: 1000, + rows: rows + }); + + me.on('destroy', statstore.stopUpdate); + + var stop_task = function() { + Proxmox.Utils.API2Request({ + url: "/nodes/" + task.node + "/tasks/" + me.upid, + waitMsgTarget: me, + method: 'DELETE', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + var stop_btn1 = new Ext.Button({ + text: gettext('Stop'), + disabled: true, + handler: stop_task + }); + + var stop_btn2 = new Ext.Button({ + text: gettext('Stop'), + disabled: true, + handler: stop_task + }); + + statgrid = Ext.create('Proxmox.grid.ObjectGrid', { + title: gettext('Status'), + layout: 'fit', + tbar: [ stop_btn1 ], + rstore: statstore, + rows: rows, + border: false + }); + + var logView = Ext.create('Proxmox.panel.LogView', { + title: gettext('Output'), + tbar: [ stop_btn2 ], + border: false, + url: "/api2/extjs/nodes/" + task.node + "/tasks/" + me.upid + "/log" + }); + + me.mon(statstore, 'load', function() { + var status = statgrid.getObjectValue('status'); + + if (status === 'stopped') { + logView.scrollToEnd = false; + logView.requestUpdate(); + statstore.stopUpdate(); + me.taskDone(statgrid.getObjectValue('exitstatus') == 'OK'); + } + + stop_btn1.setDisabled(status !== 'running'); + stop_btn2.setDisabled(status !== 'running'); + }); + + statstore.startUpdate(); + + Ext.apply(me, { + title: "Task viewer: " + task.desc + me.extraTitle, + width: 800, + height: 400, + layout: 'fit', + modal: true, + items: [{ + xtype: 'tabpanel', + region: 'center', + items: [ logView, statgrid ] + }] + }); + + me.callParent(); + + logView.fireEvent('show', logView); + } +}); + +Ext.define('apt-pkglist', { + extend: 'Ext.data.Model', + fields: [ 'Package', 'Title', 'Description', 'Section', 'Arch', + 'Priority', 'Version', 'OldVersion', 'ChangeLogUrl', 'Origin' ], + idProperty: 'Package' +}); + +Ext.define('Proxmox.node.APT', { + extend: 'Ext.grid.GridPanel', + + xtype: 'proxmoxNodeAPT', + + upgradeBtn: undefined, + + columns: [ + { + header: gettext('Package'), + width: 200, + sortable: true, + dataIndex: 'Package' + }, + { + text: gettext('Version'), + columns: [ + { + header: gettext('current'), + width: 100, + sortable: false, + dataIndex: 'OldVersion' + }, + { + header: gettext('new'), + width: 100, + sortable: false, + dataIndex: 'Version' + } + ] + }, + { + header: gettext('Description'), + sortable: false, + dataIndex: 'Title', + flex: 1 + } + ], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var store = Ext.create('Ext.data.Store', { + model: 'apt-pkglist', + groupField: 'Origin', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + "/apt/update" + }, + sorters: [ + { + property : 'Package', + direction: 'ASC' + } + ] + }); + + var groupingFeature = Ext.create('Ext.grid.feature.Grouping', { + groupHeaderTpl: '{[ "Origin: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})', + enableGroupingMenu: false + }); + + var rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', { + getAdditionalData: function (data, rowIndex, record, orig) { + var headerCt = this.view.headerCt; + var colspan = headerCt.getColumnCount(); + return { + rowBody: '
' + + Ext.String.htmlEncode(data.Description) + + '
', + rowBodyCls: me.full_description ? '' : Ext.baseCSSPrefix + 'grid-row-body-hidden', + rowBodyColspan: colspan + }; + } + }); + + var reload = function() { + store.load(); + }; + + Proxmox.Utils.monStoreErrors(me, store, true); + + var apt_command = function(cmd){ + Proxmox.Utils.API2Request({ + url: "/nodes/" + me.nodename + "/apt/" + cmd, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + me.mon(win, 'close', reload); + } + }); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var update_btn = new Ext.Button({ + text: gettext('Refresh'), + handler: function() { + Proxmox.Utils.checked_command(function() { apt_command('update'); }); + } + }); + + var show_changelog = function(rec) { + if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) { + return; + } + + var view = Ext.createWidget('component', { + autoScroll: true, + style: { + 'background-color': 'white', + 'white-space': 'pre', + 'font-family': 'monospace', + padding: '5px' + } + }); + + var win = Ext.create('Ext.window.Window', { + title: gettext('Changelog') + ": " + rec.data.Package, + width: 800, + height: 400, + layout: 'fit', + modal: true, + items: [ view ] + }); + + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: "/nodes/" + me.nodename + "/apt/changelog", + params: { + name: rec.data.Package, + version: rec.data.Version + }, + method: 'GET', + failure: function(response, opts) { + win.close(); + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + win.show(); + view.update(Ext.htmlEncode(response.result.data)); + } + }); + + }; + + var changelog_btn = new Proxmox.button.Button({ + text: gettext('Changelog'), + selModel: sm, + disabled: true, + enableFn: function(rec) { + if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) { + return false; + } + return true; + }, + handler: function(b, e, rec) { + show_changelog(rec); + } + }); + + var verbose_desc_checkbox = new Ext.form.field.Checkbox({ + boxLabel: gettext('Show details'), + value: false, + listeners: { + change: (f, val) => { + me.full_description = val; + me.getView().refresh(); + } + } + }); + + if (me.upgradeBtn) { + me.tbar = [ update_btn, me.upgradeBtn, changelog_btn, '->', verbose_desc_checkbox ]; + } else { + me.tbar = [ update_btn, changelog_btn, '->', verbose_desc_checkbox ]; + } + + Ext.apply(me, { + store: store, + stateful: true, + stateId: 'grid-update', + selModel: sm, + viewConfig: { + stripeRows: false, + emptyText: '
' + gettext('No updates available.') + '
' + }, + features: [ groupingFeature, rowBodyFeature ], + listeners: { + activate: reload, + itemdblclick: function(v, rec) { + show_changelog(rec); + } + } + }); + + me.callParent(); + } +}); +Ext.define('Proxmox.node.NetworkEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.proxmoxNodeNetworkEdit'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.iftype) { + throw "no network device type specified"; + } + + me.isCreate = !me.iface; + + var iface_vtype; + + if (me.iftype === 'bridge') { + iface_vtype = 'BridgeName'; + } else if (me.iftype === 'bond') { + iface_vtype = 'BondName'; + } else if (me.iftype === 'eth' && !me.isCreate) { + iface_vtype = 'InterfaceName'; + } else if (me.iftype === 'vlan' && !me.isCreate) { + iface_vtype = 'InterfaceName'; + } else if (me.iftype === 'OVSBridge') { + iface_vtype = 'BridgeName'; + } else if (me.iftype === 'OVSBond') { + iface_vtype = 'BondName'; + } else if (me.iftype === 'OVSIntPort') { + iface_vtype = 'InterfaceName'; + } else if (me.iftype === 'OVSPort') { + iface_vtype = 'InterfaceName'; + } else { + console.log(me.iftype); + throw "unknown network device type specified"; + } + + me.subject = Proxmox.Utils.render_network_iface_type(me.iftype); + + var column2 = []; + + if (!(me.iftype === 'OVSIntPort' || me.iftype === 'OVSPort' || + me.iftype === 'OVSBond')) { + column2.push({ + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Autostart'), + name: 'autostart', + uncheckedValue: 0, + checked: me.isCreate ? true : undefined + }); + } + + if (me.iftype === 'bridge') { + column2.push({ + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('VLAN aware'), + name: 'bridge_vlan_aware', + deleteEmpty: !me.isCreate + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('Bridge ports'), + name: 'bridge_ports' + }); + } else if (me.iftype === 'OVSBridge') { + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('Bridge ports'), + name: 'ovs_ports' + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('OVS options'), + name: 'ovs_options' + }); + } else if (me.iftype === 'OVSPort' || me.iftype === 'OVSIntPort') { + column2.push({ + xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield', + fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'), + allowBlank: false, + nodename: me.nodename, + bridgeType: 'OVSBridge', + name: 'ovs_bridge' + }); + column2.push({ + xtype: 'pveVlanField', + deleteEmpty: !me.isCreate, + name: 'ovs_tag', + value: '' + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('OVS options'), + name: 'ovs_options' + }); + } else if (me.iftype === 'bond') { + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('Slaves'), + name: 'slaves' + }); + + var policySelector = Ext.createWidget('bondPolicySelector', { + fieldLabel: gettext('Hash policy'), + name: 'bond_xmit_hash_policy', + deleteEmpty: !me.isCreate, + disabled: true + }); + + column2.push({ + xtype: 'bondModeSelector', + fieldLabel: gettext('Mode'), + name: 'bond_mode', + value: me.isCreate ? 'balance-rr' : undefined, + listeners: { + change: function(f, value) { + if (value === 'balance-xor' || + value === '802.3ad') { + policySelector.setDisabled(false); + } else { + policySelector.setDisabled(true); + policySelector.setValue(''); + } + } + }, + allowBlank: false + }); + + column2.push(policySelector); + + } else if (me.iftype === 'OVSBond') { + column2.push({ + xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield', + fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'), + allowBlank: false, + nodename: me.nodename, + bridgeType: 'OVSBridge', + name: 'ovs_bridge' + }); + column2.push({ + xtype: 'pveVlanField', + deleteEmpty: !me.isCreate, + name: 'ovs_tag', + value: '' + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('OVS options'), + name: 'ovs_options' + }); + } + + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('Comment'), + allowBlank: true, + nodename: me.nodename, + name: 'comments' + }); + + var url; + var method; + + if (me.isCreate) { + url = "/api2/extjs/nodes/" + me.nodename + "/network"; + method = 'POST'; + } else { + url = "/api2/extjs/nodes/" + me.nodename + "/network/" + me.iface; + method = 'PUT'; + } + + var column1 = [ + { + xtype: 'hiddenfield', + name: 'type', + value: me.iftype + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + fieldLabel: gettext('Name'), + name: 'iface', + value: me.iface, + vtype: iface_vtype, + allowBlank: false + } + ]; + + if (me.iftype === 'OVSBond') { + column1.push( + { + xtype: 'bondModeSelector', + fieldLabel: gettext('Mode'), + name: 'bond_mode', + openvswitch: true, + value: me.isCreate ? 'active-backup' : undefined, + allowBlank: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Slaves'), + name: 'ovs_bonds' + } + ); + } else { + + column1.push( + { + xtype: 'proxmoxtextfield', + deleteEmpty: !me.isCreate, + fieldLabel: 'IPv4/CIDR', + vtype: 'IPCIDRAddress', + name: 'cidr' + }, + { + xtype: 'proxmoxtextfield', + deleteEmpty: !me.isCreate, + fieldLabel: gettext('Gateway') + ' (IPv4)', + vtype: 'IPAddress', + name: 'gateway' + }, + { + xtype: 'proxmoxtextfield', + deleteEmpty: !me.isCreate, + fieldLabel: 'IPv6/CIDR', + vtype: 'IP6CIDRAddress', + name: 'cidr6' + }, + { + xtype: 'proxmoxtextfield', + deleteEmpty: !me.isCreate, + fieldLabel: gettext('Gateway') + ' (IPv6)', + vtype: 'IP6Address', + name: 'gateway6' + } + ); + } + + Ext.applyIf(me, { + url: url, + method: method, + items: { + xtype: 'inputpanel', + column1: column1, + column2: column2 + } + }); + + me.callParent(); + + if (me.isCreate) { + me.down('field[name=iface]').setValue(me.iface_default); + } else { + me.load({ + success: function(response, options) { + var data = response.result.data; + if (data.type !== me.iftype) { + var msg = "Got unexpected device type"; + Ext.Msg.alert(gettext('Error'), msg, function() { + me.close(); + }); + return; + } + me.setValues(data); + me.isValid(); // trigger validation + } + }); + } + } +}); +Ext.define('proxmox-networks', { + extend: 'Ext.data.Model', + fields: [ + 'iface', 'type', 'active', 'autostart', + 'bridge_ports', 'slaves', + 'address', 'netmask', 'gateway', + 'address6', 'netmask6', 'gateway6', + 'cidr', 'cidr6', + 'comments' + ], + idProperty: 'iface' +}); + +Ext.define('Proxmox.node.NetworkView', { + extend: 'Ext.panel.Panel', + + alias: ['widget.proxmoxNodeNetworkView'], + + // defines what types of network devices we want to create + // order is always the same + types: ['bridge', 'bond', 'ovs'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var baseUrl = '/nodes/' + me.nodename + '/network'; + + var store = Ext.create('Ext.data.Store', { + model: 'proxmox-networks', + proxy: { + type: 'proxmox', + url: '/api2/json' + baseUrl + }, + sorters: [ + { + property : 'iface', + direction: 'ASC' + } + ] + }); + + var reload = function() { + var changeitem = me.down('#changes'); + Proxmox.Utils.API2Request({ + url: baseUrl, + failure: function(response, opts) { + store.loadData({}); + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + changeitem.update(''); + changeitem.setHidden(true); + }, + success: function(response, opts) { + var result = Ext.decode(response.responseText); + store.loadData(result.data); + var changes = result.changes; + if (changes === undefined || changes === '') { + changes = gettext("No changes"); + changeitem.setHidden(true); + } else { + changeitem.update("
" + Ext.htmlEncode(changes) + "
"); + changeitem.setHidden(false); + } + } + }); + }; + + var run_editor = function() { + var grid = me.down('gridpanel'); + var sm = grid.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iface: rec.data.iface, + iftype: rec.data.type + }); + win.show(); + win.on('destroy', reload); + }; + + var edit_btn = new Ext.Button({ + text: gettext('Edit'), + disabled: true, + handler: run_editor + }); + + var del_btn = new Ext.Button({ + text: gettext('Remove'), + disabled: true, + handler: function(){ + var grid = me.down('gridpanel'); + var sm = grid.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var iface = rec.data.iface; + + Proxmox.Utils.API2Request({ + url: baseUrl + '/' + iface, + method: 'DELETE', + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + var set_button_status = function() { + var grid = me.down('gridpanel'); + var sm = grid.getSelectionModel(); + var rec = sm.getSelection()[0]; + + edit_btn.setDisabled(!rec); + del_btn.setDisabled(!rec); + }; + + var render_ports = function(value, metaData, record) { + if (value === 'bridge') { + return record.data.bridge_ports; + } else if (value === 'bond') { + return record.data.slaves; + } else if (value === 'OVSBridge') { + return record.data.ovs_ports; + } else if (value === 'OVSBond') { + return record.data.ovs_bonds; + } + }; + + var find_next_iface_id = function(prefix) { + var next; + for (next = 0; next <= 9999; next++) { + if (!store.getById(prefix + next.toString())) { + break; + } + } + return prefix + next.toString(); + }; + + var menu_items = []; + + if (me.types.indexOf('bridge') !== -1) { + menu_items.push({ + text: Proxmox.Utils.render_network_iface_type('bridge'), + handler: function() { + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iftype: 'bridge', + iface_default: find_next_iface_id('vmbr') + }); + win.on('destroy', reload); + win.show(); + } + }); + } + + if (me.types.indexOf('bond') !== -1) { + menu_items.push({ + text: Proxmox.Utils.render_network_iface_type('bond'), + handler: function() { + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iftype: 'bond', + iface_default: find_next_iface_id('bond') + }); + win.on('destroy', reload); + win.show(); + } + }); + } + + if (me.types.indexOf('ovs') !== -1) { + if (menu_items.length > 0) { + menu_items.push({ xtype: 'menuseparator' }); + } + + menu_items.push( + { + text: Proxmox.Utils.render_network_iface_type('OVSBridge'), + handler: function() { + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iftype: 'OVSBridge', + iface_default: find_next_iface_id('vmbr') + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: Proxmox.Utils.render_network_iface_type('OVSBond'), + handler: function() { + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iftype: 'OVSBond', + iface_default: find_next_iface_id('bond') + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: Proxmox.Utils.render_network_iface_type('OVSIntPort'), + handler: function() { + var win = Ext.create('Proxmox.node.NetworkEdit', { + nodename: me.nodename, + iftype: 'OVSIntPort' + }); + win.on('destroy', reload); + win.show(); + } + } + ); + } + + var renderer_generator = function(fieldname) { + return function(val, metaData, rec) { + var tmp = []; + if (rec.data[fieldname]) { + tmp.push(rec.data[fieldname]); + } + if (rec.data[fieldname + '6']) { + tmp.push(rec.data[fieldname + '6']); + } + return tmp.join('
') || ''; + }; + }; + + Ext.apply(me, { + layout: 'border', + tbar: [ + { + text: gettext('Create'), + menu: { + plain: true, + items: menu_items + } + }, ' ', + { + text: gettext('Revert'), + handler: function() { + Proxmox.Utils.API2Request({ + url: baseUrl, + method: 'DELETE', + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }, + edit_btn, + del_btn + ], + items: [ + { + xtype: 'gridpanel', + stateful: true, + stateId: 'grid-node-network', + store: store, + region: 'center', + border: false, + columns: [ + { + header: gettext('Name'), + sortable: true, + dataIndex: 'iface' + }, + { + header: gettext('Type'), + sortable: true, + width: 120, + renderer: Proxmox.Utils.render_network_iface_type, + dataIndex: 'type' + }, + { + xtype: 'booleancolumn', + header: gettext('Active'), + width: 80, + sortable: true, + dataIndex: 'active', + trueText: Proxmox.Utils.yesText, + falseText: Proxmox.Utils.noText, + undefinedText: Proxmox.Utils.noText, + }, + { + xtype: 'booleancolumn', + header: gettext('Autostart'), + width: 80, + sortable: true, + dataIndex: 'autostart', + trueText: Proxmox.Utils.yesText, + falseText: Proxmox.Utils.noText, + undefinedText: Proxmox.Utils.noText + }, + { + xtype: 'booleancolumn', + header: gettext('VLAN aware'), + width: 80, + sortable: true, + dataIndex: 'bridge_vlan_aware', + trueText: Proxmox.Utils.yesText, + falseText: Proxmox.Utils.noText, + undefinedText: Proxmox.Utils.noText + }, + { + header: gettext('Ports/Slaves'), + dataIndex: 'type', + renderer: render_ports + }, + { + header: gettext('Bond Mode'), + dataIndex: 'bond_mode', + renderer: Proxmox.Utils.render_bond_mode, + }, + { + header: gettext('Hash Policy'), + hidden: true, + dataIndex: 'bond_xmit_hash_policy', + }, + { + header: gettext('IP address'), + sortable: true, + width: 120, + hidden: true, + dataIndex: 'address', + renderer: renderer_generator('address'), + }, + { + header: gettext('Subnet mask'), + width: 120, + sortable: true, + hidden: true, + dataIndex: 'netmask', + renderer: renderer_generator('netmask'), + }, + { + header: gettext('CIDR'), + width: 120, + sortable: true, + dataIndex: 'cidr', + renderer: renderer_generator('cidr'), + }, + { + header: gettext('Gateway'), + width: 120, + sortable: true, + dataIndex: 'gateway', + renderer: renderer_generator('gateway'), + }, + { + header: gettext('Comment'), + dataIndex: 'comments', + flex: 1, + renderer: Ext.String.htmlEncode + } + ], + listeners: { + selectionchange: set_button_status, + itemdblclick: run_editor + } + }, + { + border: false, + region: 'south', + autoScroll: true, + hidden: true, + itemId: 'changes', + tbar: [ + gettext('Pending changes') + ' (' + + gettext('Please reboot to activate changes') + ')' + ], + split: true, + bodyPadding: 5, + flex: 0.6, + html: gettext("No changes") + } + ], + }); + + me.callParent(); + reload(); + } +}); +Ext.define('Proxmox.node.DNSEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.proxmoxNodeDNSEdit'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.items = [ + { + xtype: 'textfield', + fieldLabel: gettext('Search domain'), + name: 'search', + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('DNS server') + " 1", + vtype: 'IP64Address', + skipEmptyText: true, + name: 'dns1' + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('DNS server') + " 2", + vtype: 'IP64Address', + skipEmptyText: true, + name: 'dns2' + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('DNS server') + " 3", + vtype: 'IP64Address', + skipEmptyText: true, + name: 'dns3' + } + ]; + + Ext.applyIf(me, { + subject: gettext('DNS'), + url: "/api2/extjs/nodes/" + me.nodename + "/dns", + fieldDefaults: { + labelWidth: 120 + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('Proxmox.node.HostsView', { + extend: 'Ext.panel.Panel', + xtype: 'proxmoxNodeHostsView', + + reload: function() { + var me = this; + me.store.load(); + }, + + tbar: [ + { + text: gettext('Save'), + disabled: true, + itemId: 'savebtn', + handler: function() { + var me = this.up('panel'); + Proxmox.Utils.API2Request({ + params: { + digest: me.digest, + data: me.down('#hostsfield').getValue() + }, + method: 'POST', + url: '/nodes/' + me.nodename + '/hosts', + waitMsgTarget: me, + success: function(response, opts) { + me.reload(); + }, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + } + }, + { + text: gettext('Revert'), + disabled: true, + itemId: 'resetbtn', + handler: function() { + var me = this.up('panel'); + me.down('#hostsfield').reset(); + } + } + ], + + layout: 'fit', + + items: [ + { + xtype: 'textarea', + itemId: 'hostsfield', + fieldStyle: { + 'font-family': 'monospace', + 'white-space': 'pre' + }, + listeners: { + dirtychange: function(ta, dirty) { + var me = this.up('panel'); + me.down('#savebtn').setDisabled(!dirty); + me.down('#resetbtn').setDisabled(!dirty); + } + } + } + ], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.store = Ext.create('Ext.data.Store', { + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + "/hosts", + } + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.store); + + me.mon(me.store, 'load', function(store, records, success) { + if (!success || records.length < 1) { + return; + } + me.digest = records[0].data.digest; + var data = records[0].data.data; + me.down('#hostsfield').setValue(data); + me.down('#hostsfield').resetOriginalValue(); + }); + + me.reload(); + } +}); +Ext.define('Proxmox.node.DNSView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.proxmoxNodeDNSView'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var run_editor = function() { + var win = Ext.create('Proxmox.node.DNSEdit', { + nodename: me.nodename + }); + win.show(); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + me.nodename + "/dns", + cwidth1: 130, + interval: 1000, + run_editor: run_editor, + rows: { + search: { + header: 'Search domain', + required: true, + renderer: Ext.htmlEncode + }, + dns1: { + header: gettext('DNS server') + " 1", + required: true, + renderer: Ext.htmlEncode + }, + dns2: { + header: gettext('DNS server') + " 2", + renderer: Ext.htmlEncode + }, + dns3: { + header: gettext('DNS server') + " 3", + renderer: Ext.htmlEncode + } + }, + tbar: [ + { + text: gettext("Edit"), + handler: run_editor + } + ], + listeners: { + itemdblclick: run_editor + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('deactivate', me.rstore.stopUpdate); + me.on('destroy', me.rstore.stopUpdate); + } +}); +Ext.define('Proxmox.node.Tasks', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.proxmoxNodeTasks'], + stateful: true, + stateId: 'grid-node-tasks', + loadMask: true, + sortableColumns: false, + vmidFilter: 0, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var store = Ext.create('Ext.data.BufferedStore', { + pageSize: 500, + autoLoad: true, + remoteFilter: true, + model: 'proxmox-tasks', + proxy: { + type: 'proxmox', + startParam: 'start', + limitParam: 'limit', + url: "/api2/json/nodes/" + me.nodename + "/tasks" + } + }); + + var userfilter = ''; + var filter_errors = 0; + + var updateProxyParams = function() { + var params = { + errors: filter_errors + }; + if (userfilter) { + params.userfilter = userfilter; + } + if (me.vmidFilter) { + params.vmid = me.vmidFilter; + } + store.proxy.extraParams = params; + }; + + updateProxyParams(); + + var reload_task = Ext.create('Ext.util.DelayedTask',function() { + updateProxyParams(); + store.reload(); + }); + + var run_task_viewer = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: rec.data.upid + }); + win.show(); + }; + + var view_btn = new Ext.Button({ + text: gettext('View'), + disabled: true, + handler: run_task_viewer + }); + + Proxmox.Utils.monStoreErrors(me, store, true); + + Ext.apply(me, { + store: store, + viewConfig: { + trackOver: false, + stripeRows: false, // does not work with getRowClass() + + getRowClass: function(record, index) { + var status = record.get('status'); + + if (status && status != 'OK') { + return "proxmox-invalid-row"; + } + } + }, + tbar: [ + view_btn, '->', gettext('User name') +':', ' ', + { + xtype: 'textfield', + width: 200, + value: userfilter, + enableKeyEvents: true, + listeners: { + keyup: function(field, e) { + userfilter = field.getValue(); + reload_task.delay(500); + } + } + }, ' ', gettext('Only Errors') + ':', ' ', + { + xtype: 'checkbox', + hideLabel: true, + checked: filter_errors, + listeners: { + change: function(field, checked) { + filter_errors = checked ? 1 : 0; + reload_task.delay(10); + } + } + }, ' ' + ], + columns: [ + { + header: gettext("Start Time"), + dataIndex: 'starttime', + width: 100, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("End Time"), + dataIndex: 'endtime', + width: 100, + renderer: function(value, metaData, record) { + return Ext.Date.format(value,"M d H:i:s"); + } + }, + { + header: gettext("Node"), + dataIndex: 'node', + width: 100 + }, + { + header: gettext("User name"), + dataIndex: 'user', + width: 150 + }, + { + header: gettext("Description"), + dataIndex: 'upid', + flex: 1, + renderer: Proxmox.Utils.render_upid + }, + { + header: gettext("Status"), + dataIndex: 'status', + width: 200, + renderer: function(value, metaData, record) { + if (value == 'OK') { + return 'OK'; + } + // metaData.attr = 'style="color:red;"'; + return "ERROR: " + value; + } + } + ], + listeners: { + itemdblclick: run_task_viewer, + selectionchange: function(v, selections) { + view_btn.setDisabled(!(selections && selections[0])); + }, + show: function() { reload_task.delay(10); }, + destroy: function() { reload_task.cancel(); } + } + }); + + me.callParent(); + + } +}); +Ext.define('proxmox-services', { + extend: 'Ext.data.Model', + fields: [ 'service', 'name', 'desc', 'state' ], + idProperty: 'service' +}); + +Ext.define('Proxmox.node.ServiceView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.proxmoxNodeServiceView'], + + startOnlyServices: {}, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var rstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 1000, + storeid: 'proxmox-services' + me.nodename, + model: 'proxmox-services', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + "/services" + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: rstore, + sortAfterUpdate: true, + sorters: [ + { + property : 'name', + direction: 'ASC' + } + ] + }); + + var view_service_log = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + var win = Ext.create('Ext.window.Window', { + title: gettext('Syslog') + ': ' + rec.data.service, + modal: true, + width: 800, + height: 400, + layout: 'fit', + items: { + xtype: 'proxmoxLogView', + url: "/api2/extjs/nodes/" + me.nodename + "/syslog?service=" + + rec.data.service, + log_select_timespan: 1 + } + }); + win.show(); + }; + + var service_cmd = function(cmd) { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + Proxmox.Utils.API2Request({ + url: "/nodes/" + me.nodename + "/services/" + rec.data.service + "/" + cmd, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.loading = true; + }, + success: function(response, opts) { + rstore.startUpdate(); + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid + }); + win.show(); + } + }); + }; + + var start_btn = new Ext.Button({ + text: gettext('Start'), + disabled: true, + handler: function(){ + service_cmd("start"); + } + }); + + var stop_btn = new Ext.Button({ + text: gettext('Stop'), + disabled: true, + handler: function(){ + service_cmd("stop"); + } + }); + + var restart_btn = new Ext.Button({ + text: gettext('Restart'), + disabled: true, + handler: function(){ + service_cmd("restart"); + } + }); + + var syslog_btn = new Ext.Button({ + text: gettext('Syslog'), + disabled: true, + handler: view_service_log + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + start_btn.disable(); + stop_btn.disable(); + restart_btn.disable(); + syslog_btn.disable(); + return; + } + var service = rec.data.service; + var state = rec.data.state; + + syslog_btn.enable(); + + if (me.startOnlyServices[service]) { + if (state == 'running') { + start_btn.disable(); + restart_btn.enable(); + } else { + start_btn.enable(); + restart_btn.disable(); + } + stop_btn.disable(); + } else { + if (state == 'running') { + start_btn.disable(); + restart_btn.enable(); + stop_btn.enable(); + } else { + start_btn.enable(); + restart_btn.disable(); + stop_btn.disable(); + } + } + }; + + me.mon(store, 'refresh', set_button_status); + + Proxmox.Utils.monStoreErrors(me, rstore); + + Ext.apply(me, { + store: store, + stateful: false, + tbar: [ start_btn, stop_btn, restart_btn, syslog_btn ], + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + dataIndex: 'name' + }, + { + header: gettext('Status'), + width: 100, + sortable: true, + dataIndex: 'state' + }, + { + header: gettext('Description'), + renderer: Ext.String.htmlEncode, + dataIndex: 'desc', + flex: 2 + } + ], + listeners: { + selectionchange: set_button_status, + itemdblclick: view_service_log, + activate: rstore.startUpdate, + destroy: rstore.stopUpdate + } + }); + + me.callParent(); + } +}); +Ext.define('Proxmox.node.TimeEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.proxmoxNodeTimeEdit'], + + subject: gettext('Time zone'), + + width: 400, + + autoLoad: true, + + fieldDefaults: { + labelWidth: 70 + }, + + items: { + xtype: 'combo', + fieldLabel: gettext('Time zone'), + name: 'timezone', + queryMode: 'local', + store: Ext.create('Proxmox.data.TimezoneStore'), + displayField: 'zone', + editable: true, + anyMatch: true, + forceSelection: true, + allowBlank: false + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + me.url = "/api2/extjs/nodes/" + me.nodename + "/time"; + + me.callParent(); + } +}); +Ext.define('Proxmox.node.TimeView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.proxmoxNodeTimeView'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var tzoffset = (new Date()).getTimezoneOffset()*60000; + var renderlocaltime = function(value) { + var servertime = new Date((value * 1000) + tzoffset); + return Ext.Date.format(servertime, 'Y-m-d H:i:s'); + }; + + var run_editor = function() { + var win = Ext.create('Proxmox.node.TimeEdit', { + nodename: me.nodename + }); + win.show(); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + me.nodename + "/time", + cwidth1: 150, + interval: 1000, + run_editor: run_editor, + rows: { + timezone: { + header: gettext('Time zone'), + required: true + }, + localtime: { + header: gettext('Server time'), + required: true, + renderer: renderlocaltime + } + }, + tbar: [ + { + text: gettext("Edit"), + handler: run_editor + } + ], + listeners: { + itemdblclick: run_editor + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('deactivate', me.rstore.stopUpdate); + me.on('destroy', me.rstore.stopUpdate); + } +}); diff --git a/serverside/jsmod/6.0-4/pvemanagerlib.js b/serverside/jsmod/6.0-4/pvemanagerlib.js new file mode 100644 index 0000000..add3e49 --- /dev/null +++ b/serverside/jsmod/6.0-4/pvemanagerlib.js @@ -0,0 +1,39779 @@ +var pveOnlineHelpInfo = { + "ceph_rados_block_devices" : { + "link" : "/pve-docs/chapter-pvesm.html#ceph_rados_block_devices", + "title" : "Ceph RADOS Block Devices (RBD)" + }, + "chapter_ha_manager" : { + "link" : "/pve-docs/chapter-ha-manager.html#chapter_ha_manager", + "title" : "High Availability" + }, + "chapter_lvm" : { + "link" : "/pve-docs/chapter-sysadmin.html#chapter_lvm", + "title" : "Logical Volume Manager (LVM)" + }, + "chapter_pct" : { + "link" : "/pve-docs/chapter-pct.html#chapter_pct", + "title" : "Proxmox Container Toolkit" + }, + "chapter_pve_firewall" : { + "link" : "/pve-docs/chapter-pve-firewall.html#chapter_pve_firewall", + "title" : "Proxmox VE Firewall" + }, + "chapter_pveceph" : { + "link" : "/pve-docs/chapter-pveceph.html#chapter_pveceph", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "chapter_pvecm" : { + "link" : "/pve-docs/chapter-pvecm.html#chapter_pvecm", + "title" : "Cluster Manager" + }, + "chapter_pvesr" : { + "link" : "/pve-docs/chapter-pvesr.html#chapter_pvesr", + "title" : "Storage Replication" + }, + "chapter_storage" : { + "link" : "/pve-docs/chapter-pvesm.html#chapter_storage", + "title" : "Proxmox VE Storage" + }, + "chapter_system_administration" : { + "link" : "/pve-docs/chapter-sysadmin.html#chapter_system_administration", + "title" : "Host System Administration" + }, + "chapter_user_management" : { + "link" : "/pve-docs/chapter-pveum.html#chapter_user_management", + "title" : "User Management" + }, + "chapter_virtual_machines" : { + "link" : "/pve-docs/chapter-qm.html#chapter_virtual_machines", + "title" : "Qemu/KVM Virtual Machines" + }, + "chapter_vzdump" : { + "link" : "/pve-docs/chapter-vzdump.html#chapter_vzdump", + "title" : "Backup and Restore" + }, + "chapter_zfs" : { + "link" : "/pve-docs/chapter-sysadmin.html#chapter_zfs", + "title" : "ZFS on Linux" + }, + "datacenter_configuration_file" : { + "link" : "/pve-docs/pve-admin-guide.html#datacenter_configuration_file", + "title" : "Datacenter Configuration" + }, + "getting_help" : { + "link" : "/pve-docs/pve-admin-guide.html#getting_help", + "title" : "Getting Help" + }, + "gui_my_settings" : { + "link" : "/pve-docs/chapter-pve-gui.html#gui_my_settings", + "subtitle" : "My Settings", + "title" : "Graphical User Interface" + }, + "ha_manager_fencing" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_fencing", + "subtitle" : "Fencing", + "title" : "High Availability" + }, + "ha_manager_groups" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_groups", + "subtitle" : "Groups", + "title" : "High Availability" + }, + "ha_manager_resource_config" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resource_config", + "subtitle" : "Resources", + "title" : "High Availability" + }, + "ha_manager_resources" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resources", + "subtitle" : "Resources", + "title" : "High Availability" + }, + "pct_configuration" : { + "link" : "/pve-docs/chapter-pct.html#pct_configuration", + "subtitle" : "Configuration", + "title" : "Proxmox Container Toolkit" + }, + "pct_container_images" : { + "link" : "/pve-docs/chapter-pct.html#pct_container_images", + "subtitle" : "Container Images", + "title" : "Proxmox Container Toolkit" + }, + "pct_container_network" : { + "link" : "/pve-docs/chapter-pct.html#pct_container_network", + "subtitle" : "Network", + "title" : "Proxmox Container Toolkit" + }, + "pct_container_storage" : { + "link" : "/pve-docs/chapter-pct.html#pct_container_storage", + "subtitle" : "Container Storage", + "title" : "Proxmox Container Toolkit" + }, + "pct_cpu" : { + "link" : "/pve-docs/chapter-pct.html#pct_cpu", + "subtitle" : "CPU", + "title" : "Proxmox Container Toolkit" + }, + "pct_general" : { + "link" : "/pve-docs/chapter-pct.html#pct_general", + "subtitle" : "General Settings", + "title" : "Proxmox Container Toolkit" + }, + "pct_memory" : { + "link" : "/pve-docs/chapter-pct.html#pct_memory", + "subtitle" : "Memory", + "title" : "Proxmox Container Toolkit" + }, + "pct_migration" : { + "link" : "/pve-docs/chapter-pct.html#pct_migration", + "subtitle" : "Migration", + "title" : "Proxmox Container Toolkit" + }, + "pct_options" : { + "link" : "/pve-docs/chapter-pct.html#pct_options", + "subtitle" : "Options", + "title" : "Proxmox Container Toolkit" + }, + "pct_snapshots" : { + "link" : "/pve-docs/chapter-pct.html#pct_snapshots", + "subtitle" : "Snapshots", + "title" : "Proxmox Container Toolkit" + }, + "pct_startup_and_shutdown" : { + "link" : "/pve-docs/chapter-pct.html#pct_startup_and_shutdown", + "subtitle" : "Automatic Start and Shutdown of Containers", + "title" : "Proxmox Container Toolkit" + }, + "pve_admin_guide" : { + "link" : "/pve-docs/pve-admin-guide.html", + "title" : "Proxmox VE Administration Guide" + }, + "pve_ceph_install" : { + "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_install", + "subtitle" : "Installation of Ceph Packages", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "pve_ceph_osds" : { + "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_osds", + "subtitle" : "Creating Ceph OSDs", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "pve_ceph_pools" : { + "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_pools", + "subtitle" : "Creating Ceph Pools", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "pve_documentation_index" : { + "link" : "/pve-docs/index.html", + "title" : "Proxmox VE Documentation Index" + }, + "pve_firewall_cluster_wide_setup" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_cluster_wide_setup", + "subtitle" : "Cluster Wide Setup", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_host_specific_configuration" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_host_specific_configuration", + "subtitle" : "Host Specific Configuration", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_ip_aliases" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_aliases", + "subtitle" : "IP Aliases", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_ip_sets" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_sets", + "subtitle" : "IP Sets", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_vm_container_configuration" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_vm_container_configuration", + "subtitle" : "VM/Container Configuration", + "title" : "Proxmox VE Firewall" + }, + "pve_service_daemons" : { + "link" : "/pve-docs/index.html#_service_daemons", + "title" : "Service Daemons" + }, + "pveceph_fs" : { + "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs", + "subtitle" : "CephFS", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "pveceph_fs_create" : { + "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_create", + "subtitle" : "Create a CephFS", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "pvecm_create_cluster" : { + "link" : "/pve-docs/chapter-pvecm.html#pvecm_create_cluster", + "subtitle" : "Create the Cluster", + "title" : "Cluster Manager" + }, + "pvesr_schedule_time_format" : { + "link" : "/pve-docs/chapter-pvesr.html#pvesr_schedule_time_format", + "subtitle" : "Schedule Format", + "title" : "Storage Replication" + }, + "pveum_authentication_realms" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_authentication_realms", + "subtitle" : "Authentication Realms", + "title" : "User Management" + }, + "pveum_groups" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_groups", + "subtitle" : "Groups", + "title" : "User Management" + }, + "pveum_permission_management" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_permission_management", + "subtitle" : "Permission Management", + "title" : "User Management" + }, + "pveum_pools" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_pools", + "subtitle" : "Pools", + "title" : "User Management" + }, + "pveum_roles" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_roles", + "subtitle" : "Roles", + "title" : "User Management" + }, + "pveum_tfa_auth" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_tfa_auth", + "subtitle" : "Two factor authentication", + "title" : "User Management" + }, + "pveum_users" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_users", + "subtitle" : "Users", + "title" : "User Management" + }, + "qm_bios_and_uefi" : { + "link" : "/pve-docs/chapter-qm.html#qm_bios_and_uefi", + "subtitle" : "BIOS and UEFI", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_cloud_init" : { + "link" : "/pve-docs/chapter-qm.html#qm_cloud_init", + "title" : "Cloud-Init Support" + }, + "qm_copy_and_clone" : { + "link" : "/pve-docs/chapter-qm.html#qm_copy_and_clone", + "subtitle" : "Copies and Clones", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_cpu" : { + "link" : "/pve-docs/chapter-qm.html#qm_cpu", + "subtitle" : "CPU", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_general_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_general_settings", + "subtitle" : "General Settings", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_hard_disk" : { + "link" : "/pve-docs/chapter-qm.html#qm_hard_disk", + "subtitle" : "Hard Disk", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_memory" : { + "link" : "/pve-docs/chapter-qm.html#qm_memory", + "subtitle" : "Memory", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_migration" : { + "link" : "/pve-docs/chapter-qm.html#qm_migration", + "subtitle" : "Migration", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_network_device" : { + "link" : "/pve-docs/chapter-qm.html#qm_network_device", + "subtitle" : "Network Device", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_options" : { + "link" : "/pve-docs/chapter-qm.html#qm_options", + "subtitle" : "Options", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_os_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_os_settings", + "subtitle" : "OS Settings", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_pci_passthrough" : { + "link" : "/pve-docs/chapter-qm.html#qm_pci_passthrough", + "title" : "PCI(e) Passthrough" + }, + "qm_startup_and_shutdown" : { + "link" : "/pve-docs/chapter-qm.html#qm_startup_and_shutdown", + "subtitle" : "Automatic Start and Shutdown of Virtual Machines", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_system_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_system_settings", + "subtitle" : "System Settings", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_usb_passthrough" : { + "link" : "/pve-docs/chapter-qm.html#qm_usb_passthrough", + "subtitle" : "USB Passthrough", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_virtual_machines_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_virtual_machines_settings", + "subtitle" : "Virtual Machines Settings", + "title" : "Qemu/KVM Virtual Machines" + }, + "storage_cephfs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_cephfs", + "title" : "Ceph Filesystem (CephFS)" + }, + "storage_cifs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_cifs", + "title" : "CIFS Backend" + }, + "storage_directory" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_directory", + "title" : "Directory Backend" + }, + "storage_glusterfs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_glusterfs", + "title" : "GlusterFS Backend" + }, + "storage_lvm" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_lvm", + "title" : "LVM Backend" + }, + "storage_lvmthin" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_lvmthin", + "title" : "LVM thin Backend" + }, + "storage_nfs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_nfs", + "title" : "NFS Backend" + }, + "storage_open_iscsi" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_open_iscsi", + "title" : "Open-iSCSI initiator" + }, + "storage_zfspool" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_zfspool", + "title" : "Local ZFS Pool Backend" + }, + "sysadmin_certificate_management" : { + "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certificate_management", + "title" : "Certificate Management" + }, + "sysadmin_network_configuration" : { + "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_network_configuration", + "title" : "Network Configuration" + } +}; +Ext.ns('PVE'); + +// avoid errors related to Accessible Rich Internet Applications +// (access for people with disabilities) +// TODO reenable after all components are upgraded +Ext.enableAria = false; +Ext.enableAriaButtons = false; +Ext.enableAriaPanels = false; + +// avoid errors when running without development tools +if (!Ext.isDefined(Ext.global.console)) { + var console = { + log: function() {} + }; +} +console.log("Starting PVE Manager"); + +Ext.Ajax.defaultHeaders = { + 'Accept': 'application/json' +}; + +/*jslint confusion: true */ +Ext.define('PVE.Utils', { utilities: { + + // this singleton contains miscellaneous utilities + + toolkit: undefined, // (extjs|touch), set inside Toolkit.js + + bus_match: /^(ide|sata|virtio|scsi)\d+$/, + + log_severity_hash: { + 0: "panic", + 1: "alert", + 2: "critical", + 3: "error", + 4: "warning", + 5: "notice", + 6: "info", + 7: "debug" + }, + + support_level_hash: { + 'c': gettext('Community'), + 'b': gettext('Basic'), + 's': gettext('Standard'), + 'p': gettext('Premium') + }, + + noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.', + + kvm_ostypes: { + 'Linux': [ + { desc: '5.x - 2.6 Kernel', val: 'l26' }, + { desc: '2.4 Kernel', val: 'l24' } + ], + 'Microsoft Windows': [ + { desc: '10/2016', val: 'win10' }, + { desc: '8.x/2012/2012r2', val: 'win8' }, + { desc: '7/2008r2', val: 'win7' }, + { desc: 'Vista/2008', val: 'w2k8' }, + { desc: 'XP/2003', val: 'wxp' }, + { desc: '2000', val: 'w2k' } + ], + 'Solaris Kernel': [ + { desc: '-', val: 'solaris'} + ], + 'Other': [ + { desc: '-', val: 'other'} + ] + }, + + get_health_icon: function(state, circle) { + if (circle === undefined) { + circle = false; + } + + if (state === undefined) { + state = 'uknown'; + } + + var icon = 'faded fa-question'; + switch(state) { + case 'good': + icon = 'good fa-check'; + break; + case 'old': + icon = 'warning fa-refresh'; + break; + case 'warning': + icon = 'warning fa-exclamation'; + break; + case 'critical': + icon = 'critical fa-times'; + break; + default: break; + } + + if (circle) { + icon += '-circle'; + } + + return icon; + }, + + parse_ceph_version: function(service) { + if (service.ceph_version_short) { + return service.ceph_version_short; + } + + if (service.ceph_version) { + var match = service.ceph_version.match(/version (\d+(\.\d+)*)/); + if (match) { + return match[1]; + } + } + + return undefined; + }, + + compare_ceph_versions: function(a, b) { + if (a === b) { + return 0; + } + let avers = a.toString().split('.'); + let bvers = b.toString().split('.'); + + while (true) { + let av = avers.shift(); + let bv = bvers.shift(); + + if (av === undefined && bv === undefined) { + return 0; + } else if (av === undefined) { + return -1; + } else if (bv === undefined) { + return 1; + } else { + let diff = parseInt(av, 10) - parseInt(bv, 10); + if (diff != 0) return diff; + // else we need to look at the next parts + } + } + + }, + + get_ceph_icon_html: function(health, fw) { + var state = PVE.Utils.map_ceph_health[health]; + var cls = PVE.Utils.get_health_icon(state); + if (fw) { + cls += ' fa-fw'; + } + return " "; + }, + + map_ceph_health: { + 'HEALTH_OK':'good', + 'HEALTH_OLD':'old', + 'HEALTH_WARN':'warning', + 'HEALTH_ERR':'critical' + }, + + render_ceph_health: function(healthObj) { + var state = { + iconCls: PVE.Utils.get_health_icon(), + text: '' + }; + + if (!healthObj || !healthObj.status) { + return state; + } + + var health = PVE.Utils.map_ceph_health[healthObj.status]; + + state.iconCls = PVE.Utils.get_health_icon(health, true); + state.text = healthObj.status; + + return state; + }, + + render_zfs_health: function(value) { + if (typeof value == 'undefined'){ + return ""; + } + var iconCls = 'question-circle'; + switch (value) { + case 'AVAIL': + case 'ONLINE': + iconCls = 'check-circle good'; + break; + case 'REMOVED': + case 'DEGRADED': + iconCls = 'exclamation-circle warning'; + break; + case 'UNAVAIL': + case 'FAULTED': + case 'OFFLINE': + iconCls = 'times-circle critical'; + break; + default: //unknown + } + + return ' ' + value; + + }, + + get_kvm_osinfo: function(value) { + var info = { base: 'Other' }; // default + if (value) { + Ext.each(Object.keys(PVE.Utils.kvm_ostypes), function(k) { + Ext.each(PVE.Utils.kvm_ostypes[k], function(e) { + if (e.val === value) { + info = { desc: e.desc, base: k }; + } + }); + }); + } + return info; + }, + + render_kvm_ostype: function (value) { + var osinfo = PVE.Utils.get_kvm_osinfo(value); + if (osinfo.desc && osinfo.desc !== '-') { + return osinfo.base + ' ' + osinfo.desc; + } else { + return osinfo.base; + } + }, + + render_hotplug_features: function (value) { + var fa = []; + + if (!value || (value === '0')) { + return gettext('Disabled'); + } + + if (value === '1') { + value = 'disk,network,usb'; + } + + Ext.each(value.split(','), function(el) { + if (el === 'disk') { + fa.push(gettext('Disk')); + } else if (el === 'network') { + fa.push(gettext('Network')); + } else if (el === 'usb') { + fa.push('USB'); + } else if (el === 'memory') { + fa.push(gettext('Memory')); + } else if (el === 'cpu') { + fa.push(gettext('CPU')); + } else { + fa.push(el); + } + }); + + return fa.join(', '); + }, + + render_qga_features: function(value) { + if (!value) { + return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')'; + } + var props = PVE.Parser.parsePropertyString(value, 'enabled'); + if (!PVE.Parser.parseBoolean(props.enabled)) { + return Proxmox.Utils.disabledText; + } + + delete props.enabled; + var agentstring = Proxmox.Utils.enabledText; + + Ext.Object.each(props, function(key, value) { + var keystring = '' ; + agentstring += ', ' + key + ': '; + + if (PVE.Parser.parseBoolean(value)) { + agentstring += Proxmox.Utils.enabledText; + } else { + agentstring += Proxmox.Utils.disabledText; + } + }); + + return agentstring; + }, + + render_qemu_machine: function(value) { + return value || (Proxmox.Utils.defaultText + ' (i440fx)'); + }, + + render_qemu_bios: function(value) { + if (!value) { + return Proxmox.Utils.defaultText + ' (SeaBIOS)'; + } else if (value === 'seabios') { + return "SeaBIOS"; + } else if (value === 'ovmf') { + return "OVMF (UEFI)"; + } else { + return value; + } + }, + + render_dc_ha_opts: function(value) { + if (!value) { + return Proxmox.Utils.defaultText; + } else { + return PVE.Parser.printPropertyString(value); + } + }, + render_as_property_string: function(value) { + return (!value) ? Proxmox.Utils.defaultText + : PVE.Parser.printPropertyString(value); + }, + + render_scsihw: function(value) { + if (!value) { + return Proxmox.Utils.defaultText + ' (LSI 53C895A)'; + } else if (value === 'lsi') { + return 'LSI 53C895A'; + } else if (value === 'lsi53c810') { + return 'LSI 53C810'; + } else if (value === 'megasas') { + return 'MegaRAID SAS 8708EM2'; + } else if (value === 'virtio-scsi-pci') { + return 'VirtIO SCSI'; + } else if (value === 'virtio-scsi-single') { + return 'VirtIO SCSI single'; + } else if (value === 'pvscsi') { + return 'VMware PVSCSI'; + } else { + return value; + } + }, + + // fixme: auto-generate this + // for now, please keep in sync with PVE::Tools::kvmkeymaps + kvm_keymaps: { + //ar: 'Arabic', + da: 'Danish', + de: 'German', + 'de-ch': 'German (Swiss)', + 'en-gb': 'English (UK)', + 'en-us': 'English (USA)', + es: 'Spanish', + //et: 'Estonia', + fi: 'Finnish', + //fo: 'Faroe Islands', + fr: 'French', + 'fr-be': 'French (Belgium)', + 'fr-ca': 'French (Canada)', + 'fr-ch': 'French (Swiss)', + //hr: 'Croatia', + hu: 'Hungarian', + is: 'Icelandic', + it: 'Italian', + ja: 'Japanese', + lt: 'Lithuanian', + //lv: 'Latvian', + mk: 'Macedonian', + nl: 'Dutch', + //'nl-be': 'Dutch (Belgium)', + no: 'Norwegian', + pl: 'Polish', + pt: 'Portuguese', + 'pt-br': 'Portuguese (Brazil)', + //ru: 'Russian', + sl: 'Slovenian', + sv: 'Swedish', + //th: 'Thai', + tr: 'Turkish' + }, + + kvm_vga_drivers: { + std: gettext('Standard VGA'), + vmware: gettext('VMware compatible'), + qxl: 'SPICE', + qxl2: 'SPICE dual monitor', + qxl3: 'SPICE three monitors', + qxl4: 'SPICE four monitors', + serial0: gettext('Serial terminal') + ' 0', + serial1: gettext('Serial terminal') + ' 1', + serial2: gettext('Serial terminal') + ' 2', + serial3: gettext('Serial terminal') + ' 3', + virtio: 'VirtIO-GPU', + none: Proxmox.Utils.noneText + }, + + render_kvm_language: function (value) { + if (!value || value === '__default__') { + return Proxmox.Utils.defaultText; + } + var text = PVE.Utils.kvm_keymaps[value]; + if (text) { + return text + ' (' + value + ')'; + } + return value; + }, + + kvm_keymap_array: function() { + var data = [['__default__', PVE.Utils.render_kvm_language('')]]; + Ext.Object.each(PVE.Utils.kvm_keymaps, function(key, value) { + data.push([key, PVE.Utils.render_kvm_language(value)]); + }); + + return data; + }, + + console_map: { + '__default__': Proxmox.Utils.defaultText + ' (xterm.js)', + 'vv': 'SPICE (remote-viewer)', + 'html5': 'HTML5 (noVNC)', + 'xtermjs': 'xterm.js' + }, + + render_console_viewer: function(value) { + value = value || '__default__'; + if (PVE.Utils.console_map[value]) { + return PVE.Utils.console_map[value]; + } + return value; + }, + + console_viewer_array: function() { + return Ext.Array.map(Object.keys(PVE.Utils.console_map), function(v) { + return [v, PVE.Utils.render_console_viewer(v)]; + }); + }, + + render_kvm_vga_driver: function (value) { + if (!value) { + return Proxmox.Utils.defaultText; + } + var vga = PVE.Parser.parsePropertyString(value, 'type'); + var text = PVE.Utils.kvm_vga_drivers[vga.type]; + if (!vga.type) { + text = Proxmox.Utils.defaultText; + } + if (text) { + return text + ' (' + value + ')'; + } + return value; + }, + + kvm_vga_driver_array: function() { + var data = [['__default__', PVE.Utils.render_kvm_vga_driver('')]]; + Ext.Object.each(PVE.Utils.kvm_vga_drivers, function(key, value) { + data.push([key, PVE.Utils.render_kvm_vga_driver(value)]); + }); + + return data; + }, + + render_kvm_startup: function(value) { + var startup = PVE.Parser.parseStartup(value); + + var res = 'order='; + if (startup.order === undefined) { + res += 'any'; + } else { + res += startup.order; + } + if (startup.up !== undefined) { + res += ',up=' + startup.up; + } + if (startup.down !== undefined) { + res += ',down=' + startup.down; + } + + return res; + }, + + extractFormActionError: function(action) { + var msg; + switch (action.failureType) { + case Ext.form.action.Action.CLIENT_INVALID: + msg = gettext('Form fields may not be submitted with invalid values'); + break; + case Ext.form.action.Action.CONNECT_FAILURE: + msg = gettext('Connection error'); + var resp = action.response; + if (resp.status && resp.statusText) { + msg += " " + resp.status + ": " + resp.statusText; + } + break; + case Ext.form.action.Action.LOAD_FAILURE: + case Ext.form.action.Action.SERVER_INVALID: + msg = Proxmox.Utils.extractRequestError(action.result, true); + break; + } + return msg; + }, + + format_duration_short: function(ut) { + + if (ut < 60) { + return ut.toFixed(1) + 's'; + } + + if (ut < 3600) { + var mins = ut / 60; + return mins.toFixed(1) + 'm'; + } + + if (ut < 86400) { + var hours = ut / 3600; + return hours.toFixed(1) + 'h'; + } + + var days = ut / 86400; + return days.toFixed(1) + 'd'; + }, + + contentTypes: { + 'images': gettext('Disk image'), + 'backup': gettext('VZDump backup file'), + 'vztmpl': gettext('Container template'), + 'iso': gettext('ISO image'), + 'rootdir': gettext('Container'), + 'snippets': gettext('Snippets') + }, + + storageSchema: { + dir: { + name: Proxmox.Utils.directoryText, + ipanel: 'DirInputPanel', + faIcon: 'folder' + }, + lvm: { + name: 'LVM', + ipanel: 'LVMInputPanel', + faIcon: 'folder' + }, + lvmthin: { + name: 'LVM-Thin', + ipanel: 'LvmThinInputPanel', + faIcon: 'folder' + }, + nfs: { + name: 'NFS', + ipanel: 'NFSInputPanel', + faIcon: 'building' + }, + cifs: { + name: 'CIFS', + ipanel: 'CIFSInputPanel', + faIcon: 'building' + }, + glusterfs: { + name: 'GlusterFS', + ipanel: 'GlusterFsInputPanel', + faIcon: 'building' + }, + iscsi: { + name: 'iSCSI', + ipanel: 'IScsiInputPanel', + faIcon: 'building' + }, + cephfs: { + name: 'CephFS', + ipanel: 'CephFSInputPanel', + faIcon: 'building' + }, + pvecephfs: { + name: 'CephFS (PVE)', + ipanel: 'CephFSInputPanel', + hideAdd: true, + faIcon: 'building' + }, + rbd: { + name: 'RBD', + ipanel: 'RBDInputPanel', + faIcon: 'building' + }, + pveceph: { + name: 'RBD (PVE)', + ipanel: 'RBDInputPanel', + hideAdd: true, + faIcon: 'building' + }, + zfs: { + name: 'ZFS over iSCSI', + ipanel: 'ZFSInputPanel', + faIcon: 'building' + }, + zfspool: { + name: 'ZFS', + ipanel: 'ZFSPoolInputPanel', + faIcon: 'folder' + }, + drbd: { + name: 'DRBD', + hideAdd: true + } + }, + + format_storage_type: function(value, md, record) { + if (value === 'rbd') { + value = (!record || record.get('monhost') ? 'rbd' : 'pveceph'); + } else if (value === 'cephfs') { + value = (!record || record.get('monhost') ? 'cephfs' : 'pvecephfs'); + } + + var schema = PVE.Utils.storageSchema[value]; + if (schema) { + return schema.name; + } + return Proxmox.Utils.unknownText; + }, + + format_ha: function(value) { + var text = Proxmox.Utils.noneText; + + if (value.managed) { + text = value.state || Proxmox.Utils.noneText; + + text += ', ' + Proxmox.Utils.groupText + ': '; + text += value.group || Proxmox.Utils.noneText; + } + + return text; + }, + + format_content_types: function(value) { + return value.split(',').sort().map(function(ct) { + return PVE.Utils.contentTypes[ct] || ct; + }).join(', '); + }, + + render_storage_content: function(value, metaData, record) { + var data = record.data; + if (Ext.isNumber(data.channel) && + Ext.isNumber(data.id) && + Ext.isNumber(data.lun)) { + return "CH " + + Ext.String.leftPad(data.channel,2, '0') + + " ID " + data.id + " LUN " + data.lun; + } + return data.volid.replace(/^.*:(.*\/)?/,''); + }, + + render_serverity: function (value) { + return PVE.Utils.log_severity_hash[value] || value; + }, + + render_cpu: function(value, metaData, record, rowIndex, colIndex, store) { + + if (!(record.data.uptime && Ext.isNumeric(value))) { + return ''; + } + + var maxcpu = record.data.maxcpu || 1; + + if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) { + return ''; + } + + var per = value * 100; + + return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU'); + }, + + render_size: function(value, metaData, record, rowIndex, colIndex, store) { + /*jslint confusion: true */ + + if (!Ext.isNumeric(value)) { + return ''; + } + + return Proxmox.Utils.format_size(value); + }, + + render_bandwidth: function(value) { + if (!Ext.isNumeric(value)) { + return ''; + } + + return Proxmox.Utils.format_size(value) + '/s'; + }, + + render_timestamp_human_readable: function(value) { + return Ext.Date.format(new Date(value * 1000), 'l d F Y H:i:s'); + }, + + render_duration: function(value) { + if (value === undefined) { + return '-'; + } + return PVE.Utils.format_duration_short(value); + }, + + calculate_mem_usage: function(data) { + if (!Ext.isNumeric(data.mem) || + data.maxmem === 0 || + data.uptime < 1) { + return -1; + } + + return (data.mem / data.maxmem); + }, + + render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) { + if (!Ext.isNumeric(value) || value === -1) { + return ''; + } + if (value > 1 ) { + // we got no percentage but bytes + var mem = value; + var maxmem = record.data.maxmem; + if (!record.data.uptime || + maxmem === 0 || + !Ext.isNumeric(mem)) { + return ''; + } + + return ((mem*100)/maxmem).toFixed(1) + " %"; + } + return (value*100).toFixed(1) + " %"; + }, + + render_mem_usage: function(value, metaData, record, rowIndex, colIndex, store) { + + var mem = value; + var maxmem = record.data.maxmem; + + if (!record.data.uptime) { + return ''; + } + + if (!(Ext.isNumeric(mem) && maxmem)) { + return ''; + } + + return PVE.Utils.render_size(value); + }, + + calculate_disk_usage: function(data) { + + if (!Ext.isNumeric(data.disk) || + data.type === 'qemu' || + (data.type === 'lxc' && data.uptime === 0) || + data.maxdisk === 0) { + return -1; + } + + return (data.disk / data.maxdisk); + }, + + render_disk_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) { + if (!Ext.isNumeric(value) || value === -1) { + return ''; + } + + return (value * 100).toFixed(1) + " %"; + }, + + render_disk_usage: function(value, metaData, record, rowIndex, colIndex, store) { + + var disk = value; + var maxdisk = record.data.maxdisk; + var type = record.data.type; + + if (!Ext.isNumeric(disk) || + type === 'qemu' || + maxdisk === 0 || + (type === 'lxc' && record.data.uptime === 0)) { + return ''; + } + + return PVE.Utils.render_size(value); + }, + + get_object_icon_class: function(type, record) { + var status = ''; + var objType = type; + + if (type === 'type') { + // for folder view + objType = record.groupbyid; + } else if (record.template) { + // templates + objType = 'template'; + status = type; + } else { + // everything else + status = record.status + ' ha-' + record.hastate; + } + + if (record.lock) { + status += ' locked lock-' + record.lock; + } + + var defaults = PVE.tree.ResourceTree.typeDefaults[objType]; + if (defaults && defaults.iconCls) { + var retVal = defaults.iconCls + ' ' + status; + return retVal; + } + + return ''; + }, + + render_resource_type: function(value, metaData, record, rowIndex, colIndex, store) { + + var cls = PVE.Utils.get_object_icon_class(value,record.data); + + var fa = ' '; + return fa + value; + }, + + render_support_level: function(value, metaData, record) { + return PVE.Utils.support_level_hash[value] || '-'; + }, + + render_upid: function(value, metaData, record) { + var type = record.data.type; + var id = record.data.id; + + return Proxmox.Utils.format_task_description(type, id); + }, + + /* render functions for new status panel */ + + render_usage: function(val) { + return (val*100).toFixed(2) + '%'; + }, + + render_cpu_usage: function(val, max) { + return Ext.String.format(gettext('{0}% of {1}') + + ' ' + gettext('CPU(s)'), (val*100).toFixed(2), max); + }, + + render_size_usage: function(val, max) { + if (max === 0) { + return gettext('N/A'); + } + return (val*100/max).toFixed(2) + '% '+ '(' + + Ext.String.format(gettext('{0} of {1}'), + PVE.Utils.render_size(val), PVE.Utils.render_size(max)) + ')'; + }, + + /* this is different for nodes */ + render_node_cpu_usage: function(value, record) { + return PVE.Utils.render_cpu_usage(value, record.cpus); + }, + + /* this is different for nodes */ + render_node_size_usage: function(record) { + return PVE.Utils.render_size_usage(record.used, record.total); + }, + + render_optional_url: function(value) { + var match; + if (value && (match = value.match(/^https?:\/\//)) !== null) { + return '' + value + ''; + } + return value; + }, + + render_san: function(value) { + var names = []; + if (Ext.isArray(value)) { + value.forEach(function(val) { + if (!Ext.isNumber(val)) { + names.push(val); + } + }); + return names.join('
'); + } + return value; + }, + + render_full_name: function(firstname, metaData, record) { + var first = firstname || ''; + var last = record.data.lastname || ''; + return Ext.htmlEncode(first + " " + last); + }, + + render_u2f_error: function(error) { + var ErrorNames = { + '1': gettext('Other Error'), + '2': gettext('Bad Request'), + '3': gettext('Configuration Unsupported'), + '4': gettext('Device Ineligible'), + '5': gettext('Timeout') + }; + return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText; + }, + + windowHostname: function() { + return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match, + function(m, addr, offset, original) { return addr; }); + }, + + openDefaultConsoleWindow: function(consoles, vmtype, vmid, nodename, vmname, cmd) { + var dv = PVE.Utils.defaultViewer(consoles); + PVE.Utils.openConsoleWindow(dv, vmtype, vmid, nodename, vmname, cmd); + }, + + openConsoleWindow: function(viewer, vmtype, vmid, nodename, vmname, cmd) { + // kvm, lxc, shell, upgrade + + if (vmid == undefined && (vmtype === 'kvm' || vmtype === 'lxc')) { + throw "missing vmid"; + } + + if (!nodename) { + throw "no nodename specified"; + } + + if (viewer === 'html5') { + PVE.Utils.openVNCViewer(vmtype, vmid, nodename, vmname, cmd); + } else if (viewer === 'xtermjs') { + Proxmox.Utils.openXtermJsViewer(vmtype, vmid, nodename, vmname, cmd); + } else if (viewer === 'vv') { + var url; + var params = { proxy: PVE.Utils.windowHostname() }; + if (vmtype === 'kvm') { + url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy'; + PVE.Utils.openSpiceViewer(url, params); + } else if (vmtype === 'lxc') { + url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy'; + PVE.Utils.openSpiceViewer(url, params); + } else if (vmtype === 'shell') { + url = '/nodes/' + nodename + '/spiceshell'; + PVE.Utils.openSpiceViewer(url, params); + } else if (vmtype === 'upgrade') { + url = '/nodes/' + nodename + '/spiceshell'; + params.upgrade = 1; + PVE.Utils.openSpiceViewer(url, params); + } else if (vmtype === 'cmd') { + url = '/nodes/' + nodename + '/spiceshell'; + params.cmd = cmd; + PVE.Utils.openSpiceViewer(url, params); + } + } else { + throw "unknown viewer type"; + } + }, + + defaultViewer: function(consoles) { + + var allowSpice, allowXtermjs; + + if (consoles === true) { + allowSpice = true; + allowXtermjs = true; + } else if (typeof consoles === 'object') { + allowSpice = consoles.spice; + allowXtermjs = !!consoles.xtermjs; + } + var dv = PVE.VersionInfo.console || 'xtermjs'; + if (dv === 'vv' && !allowSpice) { + dv = (allowXtermjs) ? 'xtermjs' : 'html5'; + } else if (dv === 'xtermjs' && !allowXtermjs) { + dv = (allowSpice) ? 'vv' : 'html5'; + } + + return dv; + }, + + openVNCViewer: function(vmtype, vmid, nodename, vmname, cmd) { + var url = Ext.Object.toQueryString({ + console: vmtype, // kvm, lxc, upgrade or shell + novnc: 1, + vmid: vmid, + vmname: vmname, + node: nodename, + resize: 'off', + cmd: cmd + }); + var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427"); + if (nw) { + nw.focus(); + } + }, + + openSpiceViewer: function(url, params){ + + var downloadWithName = function(uri, name) { + var link = Ext.DomHelper.append(document.body, { + tag: 'a', + href: uri, + css : 'display:none;visibility:hidden;height:0px;' + }); + + // Note: we need to tell android the correct file name extension + // but we do not set 'download' tag for other environments, because + // It can have strange side effects (additional user prompt on firefox) + var andriod = navigator.userAgent.match(/Android/i) ? true : false; + if (andriod) { + link.download = name; + } + + if (link.fireEvent) { + link.fireEvent('onclick'); + } else { + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); + link.dispatchEvent(evt); + } + }; + + Proxmox.Utils.API2Request({ + url: url, + params: params, + method: 'POST', + failure: function(response, opts){ + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts){ + var raw = "[virt-viewer]\n"; + Ext.Object.each(response.result.data, function(k, v) { + raw += k + "=" + v + "\n"; + }); + var url = 'data:application/x-virt-viewer;charset=UTF-8,' + + encodeURIComponent(raw); + + downloadWithName(url, "pve-spice.vv"); + } + }); + }, + + openTreeConsole: function(tree, record, item, index, e) { + e.stopEvent(); + var nodename = record.data.node; + var vmid = record.data.vmid; + var vmname = record.data.name; + if (record.data.type === 'qemu' && !record.data.template) { + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts) { + let conf = response.result.data; + var consoles = { + spice: !!conf.spice, + xtermjs: !!conf.serial, + }; + PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname); + } + }); + } else if (record.data.type === 'lxc' && !record.data.template) { + PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname); + } + }, + + // test automation helper + call_menu_handler: function(menu, text) { + + var list = menu.query('menuitem'); + + Ext.Array.each(list, function(item) { + if (item.text === text) { + if (item.handler) { + item.handler(); + return 1; + } else { + return undefined; + } + } + }); + }, + + createCmdMenu: function(v, record, item, index, event) { + event.stopEvent(); + if (!(v instanceof Ext.tree.View)) { + v.select(record); + } + var menu; + var template = !!record.data.template; + var type = record.data.type; + + if (template) { + if (type === 'qemu' || type == 'lxc') { + menu = Ext.create('PVE.menu.TemplateMenu', { + pveSelNode: record + }); + } + } else if (type === 'qemu' || + type === 'lxc' || + type === 'node') { + menu = Ext.create('PVE.' + type + '.CmdMenu', { + pveSelNode: record, + nodename: record.data.node + }); + } else { + return; + } + + menu.showAt(event.getXY()); + return menu; + }, + + // helper for deleting field which are set to there default values + delete_if_default: function(values, fieldname, default_val, create) { + if (values[fieldname] === '' || values[fieldname] === default_val) { + if (!create) { + if (values['delete']) { + values['delete'] += ',' + fieldname; + } else { + values['delete'] = fieldname; + } + } + + delete values[fieldname]; + } + }, + + loadSSHKeyFromFile: function(file, callback) { + // ssh-keygen produces 740 bytes for an average 4096 bit rsa key, with + // a user@host comment, 1420 for 8192 bits; current max is 16kbit + // assume: 740*8 for max. 32kbit (5920 byte file) + // round upwards to nearest nice number => 8192 bytes, leaves lots of comment space + if (file.size > 8192) { + Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size); + return; + } + /*global + FileReader + */ + var reader = new FileReader(); + reader.onload = function(evt) { + callback(evt.target.result); + }; + reader.readAsText(file); + }, + + bus_counts: { ide: 4, sata: 6, scsi: 16, virtio: 16 }, + + // types is either undefined (all busses), an array of busses, or a single bus + forEachBus: function(types, func) { + var busses = Object.keys(PVE.Utils.bus_counts); + var i, j, count, cont; + + if (Ext.isArray(types)) { + busses = types; + } else if (Ext.isDefined(types)) { + busses = [ types ]; + } + + // check if we only have valid busses + for (i = 0; i < busses.length; i++) { + if (!PVE.Utils.bus_counts[busses[i]]) { + throw "invalid bus: '" + busses[i] + "'"; + } + } + + for (i = 0; i < busses.length; i++) { + count = PVE.Utils.bus_counts[busses[i]]; + for (j = 0; j < count; j++) { + cont = func(busses[i], j); + if (!cont && cont !== undefined) { + return; + } + } + } + }, + + mp_counts: { mps: 256, unused: 256 }, + + forEachMP: function(func, includeUnused) { + var i, cont; + for (i = 0; i < PVE.Utils.mp_counts.mps; i++) { + cont = func('mp', i); + if (!cont && cont !== undefined) { + return; + } + } + + if (!includeUnused) { + return; + } + + for (i = 0; i < PVE.Utils.mp_counts.unused; i++) { + cont = func('unused', i); + if (!cont && cont !== undefined) { + return; + } + } + }, + + cleanEmptyObjectKeys: function (obj) { + var propName; + for (propName in obj) { + if (obj.hasOwnProperty(propName)) { + if (obj[propName] === null || obj[propName] === undefined) { + delete obj[propName]; + } + } + } + }, + + handleStoreErrorOrMask: function(me, store, regex, callback) { + + me.mon(store, 'load', function (proxy, response, success, operation) { + + if (success) { + Proxmox.Utils.setErrorMask(me, false); + return; + } + var msg; + + if (operation.error.statusText) { + if (operation.error.statusText.match(regex)) { + callback(me, operation.error); + return; + } else { + msg = operation.error.statusText + ' (' + operation.error.status + ')'; + } + } else { + msg = gettext('Connection error'); + } + Proxmox.Utils.setErrorMask(me, msg); + }); + }, + + showCephInstallOrMask: function(container, msg, nodename, callback){ + var regex = new RegExp("not (installed|initialized)", "i"); + if (msg.match(regex)) { + if (Proxmox.UserName === 'root@pam') { + container.el.mask(); + if (!container.down('pveCephInstallWindow')){ + var isInstalled = msg.match(/not initialized/i) ? true : false; + var win = Ext.create('PVE.ceph.Install', { + nodename: nodename + }); + win.getViewModel().set('isInstalled', isInstalled); + container.add(win); + win.show(); + callback(win); + } + } else { + container.mask(Ext.String.format(gettext('{0} not installed.') + + ' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']); + } + return true; + } else { + return false; + } + } +}, + + singleton: true, + constructor: function() { + var me = this; + Ext.apply(me, me.utilities); + } + +}); + +// ExtJS related things + +Proxmox.Utils.toolkit = 'extjs'; + +// custom PVE specific VTypes +Ext.apply(Ext.form.field.VTypes, { + + QemuStartDate: function(v) { + return (/^(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)$/).test(v); + }, + QemuStartDateText: gettext('Format') + ': "now" or "2006-06-17T16:01:21" or "2006-06-17"', + IP64AddressList: function(v) { + var list = v.split(/[\ \,\;]+/); + var i; + for (i = 0; i < list.length; i++) { + if (list[i] == '') { + continue; + } + + if (!Proxmox.Utils.IP64_match.test(list[i])) { + return false; + } + } + + return true; + }, + IP64AddressListText: gettext('Example') + ': 192.168.1.1,192.168.1.2', + IP64AddressListMask: /[A-Fa-f0-9\,\:\.\;\ ]/ +}); + +Ext.define('PVE.form.field.Display', { + override: 'Ext.form.field.Display', + + setSubmitValue: function(value) { + // do nothing, this is only to allow generalized bindings for the: + // `me.isCreate ? 'textfield' : 'displayfield'` cases we have. + } +}); +// Some configuration values are complex strings - +// so we need parsers/generators for them. + +Ext.define('PVE.Parser', { statics: { + + // this class only contains static functions + + parseACME: function(value) { + if (!value) { + return; + } + + var res = {}; + var errors = false; + + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; //continue + } + + var match_res; + if ((match_res = p.match(/^(?:domains=)?((?:[a-zA-Z0-9\-\.]+[;, ]?)+)$/)) !== null) { + res.domains = match_res[1].split(/[;, ]/); + } else { + errors = true; + return false; + } + }); + + if (errors || !res) { + return; + } + + return res; + }, + + parseBoolean: function(value, default_value) { + if (!Ext.isDefined(value)) { + return default_value; + } + value = value.toLowerCase(); + return value === '1' || + value === 'on' || + value === 'yes' || + value === 'true'; + }, + + parsePropertyString: function(value, defaultKey) { + var res = {}, + error; + + Ext.Array.each(value.split(','), function(p) { + var kv = p.split('=', 2); + if (Ext.isDefined(kv[1])) { + res[kv[0]] = kv[1]; + } else if (Ext.isDefined(defaultKey)) { + if (Ext.isDefined(res[defaultKey])) { + error = 'defaultKey may be only defined once in propertyString'; + return false; // break + } + res[defaultKey] = kv[0]; + } else { + error = 'invalid propertyString, not a key=value pair and no defaultKey defined'; + return false; // break + } + }); + + if (error !== undefined) { + console.error(error); + return; + } + + return res; + }, + + printPropertyString: function(data, defaultKey) { + var stringparts = [], + gotDefaultKeyVal = false, + defaultKeyVal; + + Ext.Object.each(data, function(key, value) { + if (defaultKey !== undefined && key === defaultKey) { + gotDefaultKeyVal = true; + defaultKeyVal = value; + } else { + stringparts.push(key + '=' + value); + } + }); + + stringparts = stringparts.sort(); + if (gotDefaultKeyVal) { + stringparts.unshift(defaultKeyVal); + } + + return stringparts.join(','); + }, + + parseQemuNetwork: function(key, value) { + if (!(key && value)) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + var match_res; + + if ((match_res = p.match(/^(ne2k_pci|e1000|e1000-82540em|e1000-82544gc|e1000-82545em|vmxnet3|rtl8139|pcnet|virtio|ne2k_isa|i82551|i82557b|i82559er)(=([0-9a-f]{2}(:[0-9a-f]{2}){5}))?$/i)) !== null) { + res.model = match_res[1].toLowerCase(); + if (match_res[3]) { + res.macaddr = match_res[3]; + } + } else if ((match_res = p.match(/^bridge=(\S+)$/)) !== null) { + res.bridge = match_res[1]; + } else if ((match_res = p.match(/^rate=(\d+(\.\d+)?)$/)) !== null) { + res.rate = match_res[1]; + } else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) { + res.tag = match_res[1]; + } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) { + res.firewall = match_res[1]; + } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) { + res.disconnect = match_res[1]; + } else if ((match_res = p.match(/^queues=(\d+)$/)) !== null) { + res.queues = match_res[1]; + } else if ((match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null) { + res.trunks = match_res[1]; + } else { + errors = true; + return false; // break + } + }); + + if (errors || !res.model) { + return; + } + + return res; + }, + + printQemuNetwork: function(net) { + + var netstr = net.model; + if (net.macaddr) { + netstr += "=" + net.macaddr; + } + if (net.bridge) { + netstr += ",bridge=" + net.bridge; + if (net.tag) { + netstr += ",tag=" + net.tag; + } + if (net.firewall) { + netstr += ",firewall=" + net.firewall; + } + } + if (net.rate) { + netstr += ",rate=" + net.rate; + } + if (net.queues) { + netstr += ",queues=" + net.queues; + } + if (net.disconnect) { + netstr += ",link_down=" + net.disconnect; + } + if (net.trunks) { + netstr += ",trunks=" + net.trunks; + } + return netstr; + }, + + parseQemuDrive: function(key, value) { + if (!(key && value)) { + return; + } + + var res = {}; + + var match_res = key.match(/^([a-z]+)(\d+)$/); + if (!match_res) { + return; + } + res['interface'] = match_res[1]; + res.index = match_res[2]; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + var match_res = p.match(/^([a-z_]+)=(\S+)$/); + if (!match_res) { + if (!p.match(/\=/)) { + res.file = p; + return; // continue + } + errors = true; + return false; // break + } + var k = match_res[1]; + if (k === 'volume') { + k = 'file'; + } + + if (Ext.isDefined(res[k])) { + errors = true; + return false; // break + } + + var v = match_res[2]; + + if (k === 'cache' && v === 'off') { + v = 'none'; + } + + res[k] = v; + }); + + if (errors || !res.file) { + return; + } + + return res; + }, + + printQemuDrive: function(drive) { + + var drivestr = drive.file; + + Ext.Object.each(drive, function(key, value) { + if (!Ext.isDefined(value) || key === 'file' || + key === 'index' || key === 'interface') { + return; // continue + } + drivestr += ',' + key + '=' + value; + }); + + return drivestr; + }, + + parseIPConfig: function(key, value) { + if (!(key && value)) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + var match_res; + if ((match_res = p.match(/^ip=(\S+)$/)) !== null) { + res.ip = match_res[1]; + } else if ((match_res = p.match(/^gw=(\S+)$/)) !== null) { + res.gw = match_res[1]; + } else if ((match_res = p.match(/^ip6=(\S+)$/)) !== null) { + res.ip6 = match_res[1]; + } else if ((match_res = p.match(/^gw6=(\S+)$/)) !== null) { + res.gw6 = match_res[1]; + } else { + errors = true; + return false; // break + } + }); + + if (errors) { + return; + } + + return res; + }, + + printIPConfig: function(cfg) { + var c = ""; + var str = ""; + if (cfg.ip) { + str += "ip=" + cfg.ip; + c = ","; + } + if (cfg.gw) { + str += c + "gw=" + cfg.gw; + c = ","; + } + if (cfg.ip6) { + str += c + "ip6=" + cfg.ip6; + c = ","; + } + if (cfg.gw6) { + str += c + "gw6=" + cfg.gw6; + c = ","; + } + return str; + }, + + parseOpenVZNetIf: function(value) { + if (!value) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(';'), function(item) { + if (!item || item.match(/^\s*$/)) { + return; // continue + } + + var data = {}; + Ext.Array.each(item.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + var match_res = p.match(/^(ifname|mac|bridge|host_ifname|host_mac|mac_filter)=(\S+)$/); + if (!match_res) { + errors = true; + return false; // break + } + if (match_res[1] === 'bridge'){ + var bridgevlanf = match_res[2]; + var bridge_res = bridgevlanf.match(/^(vmbr(\d+))(v(\d+))?(f)?$/); + if (!bridge_res) { + errors = true; + return false; // break + } + data.bridge = bridge_res[1]; + data.tag = bridge_res[4]; + /*jslint confusion: true*/ + data.firewall = bridge_res[5] ? 1 : 0; + /*jslint confusion: false*/ + } else { + data[match_res[1]] = match_res[2]; + } + }); + + if (errors || !data.ifname) { + errors = true; + return false; // break + } + + data.raw = item; + + res[data.ifname] = data; + }); + + return errors ? undefined: res; + }, + + printOpenVZNetIf: function(netif) { + var netarray = []; + + Ext.Object.each(netif, function(iface, data) { + var tmparray = []; + Ext.Array.each(['ifname', 'mac', 'bridge', 'host_ifname' , 'host_mac', 'mac_filter', 'tag', 'firewall'], function(key) { + var value = data[key]; + if (key === 'bridge'){ + if(data.tag){ + value = value + 'v' + data.tag; + } + if (data.firewall){ + value = value + 'f'; + } + } + if (value) { + tmparray.push(key + '=' + value); + } + + }); + netarray.push(tmparray.join(',')); + }); + + return netarray.join(';'); + }, + + parseLxcNetwork: function(value) { + if (!value) { + return; + } + + var data = {}; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + var match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|tag|rate)=(\S+)$/); + if (match_res) { + data[match_res[1]] = match_res[2]; + } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) { + data.firewall = PVE.Parser.parseBoolean(match_res[1]); + } else { + // todo: simply ignore errors ? + return; // continue + } + }); + + return data; + }, + + printLxcNetwork: function(data) { + var tmparray = []; + Ext.Array.each(['bridge', 'hwaddr', 'mtu', 'name', 'ip', + 'gw', 'ip6', 'gw6', 'firewall', 'tag'], function(key) { + var value = data[key]; + if (value) { + tmparray.push(key + '=' + value); + } + }); + + /*jslint confusion: true*/ + if (data.rate > 0) { + tmparray.push('rate=' + data.rate); + } + /*jslint confusion: false*/ + return tmparray.join(','); + }, + + parseLxcMountPoint: function(value) { + if (!value) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + var match_res = p.match(/^([a-z_]+)=(.+)$/); + if (!match_res) { + if (!p.match(/\=/)) { + res.file = p; + return; // continue + } + errors = true; + return false; // break + } + var k = match_res[1]; + if (k === 'volume') { + k = 'file'; + } + + if (Ext.isDefined(res[k])) { + errors = true; + return false; // break + } + + var v = match_res[2]; + + res[k] = v; + }); + + if (errors || !res.file) { + return; + } + + var m = res.file.match(/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):/i); + if (m) { + res.storage = m[1]; + res.type = 'volume'; + } else if (res.file.match(/^\/dev\//)) { + res.type = 'device'; + } else { + res.type = 'bind'; + } + + return res; + }, + + printLxcMountPoint: function(mp) { + var drivestr = mp.file; + + Ext.Object.each(mp, function(key, value) { + if (!Ext.isDefined(value) || key === 'file' || + key === 'type' || key === 'storage') { + return; // continue + } + drivestr += ',' + key + '=' + value; + }); + + return drivestr; + }, + + parseStartup: function(value) { + if (value === undefined) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + var match_res; + + if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) { + res.order = match_res[2]; + } else if ((match_res = p.match(/^up=(\d+)$/)) !== null) { + res.up = match_res[1]; + } else if ((match_res = p.match(/^down=(\d+)$/)) !== null) { + res.down = match_res[1]; + } else { + errors = true; + return false; // break + } + }); + + if (errors) { + return; + } + + return res; + }, + + printStartup: function(startup) { + var arr = []; + if (startup.order !== undefined && startup.order !== '') { + arr.push('order=' + startup.order); + } + if (startup.up !== undefined && startup.up !== '') { + arr.push('up=' + startup.up); + } + if (startup.down !== undefined && startup.down !== '') { + arr.push('down=' + startup.down); + } + + return arr.join(','); + }, + + parseQemuSmbios1: function(value) { + var res = value.split(',').reduce(function (accumulator, currentValue) { + var splitted = currentValue.split(new RegExp("=(.+)")); + accumulator[splitted[0]] = splitted[1]; + return accumulator; + }, {}); + + if (PVE.Parser.parseBoolean(res.base64, false)) { + Ext.Object.each(res, function(key, value) { + if (key === 'uuid') { return; } + res[key] = Ext.util.Base64.decode(value); + }); + } + + return res; + }, + + printQemuSmbios1: function(data) { + + var datastr = ''; + var base64 = false; + Ext.Object.each(data, function(key, value) { + if (value === '') { return; } + if (key === 'uuid') { + datastr += (datastr !== '' ? ',' : '') + key + '=' + value; + } else { + // values should be base64 encoded from now on, mark config strings correspondingly + if (!base64) { + base64 = true; + datastr += (datastr !== '' ? ',' : '') + 'base64=1'; + } + datastr += (datastr !== '' ? ',' : '') + key + '=' + Ext.util.Base64.encode(value); + } + }); + + return datastr; + }, + + parseTfaConfig: function(value) { + var res = {}; + + Ext.Array.each(value.split(','), function(p) { + var kva = p.split('=', 2); + res[kva[0]] = kva[1]; + }); + + return res; + }, + + parseTfaType: function(value) { + /*jslint confusion: true*/ + var match; + if (!value || !value.length) { + return undefined; + } else if (value === 'x!oath') { + return 'totp'; + } else if (!!(match = value.match(/^x!(.+)$/))) { + return match[1]; + } else { + return 1; + } + }, + + parseQemuCpu: function(value) { + if (!value) { + return {}; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + if (!p.match(/\=/)) { + if (Ext.isDefined(res.cpu)) { + errors = true; + return false; // break + } + res.cputype = p; + return; // continue + } + + var match_res = p.match(/^([a-z_]+)=(\S+)$/); + if (!match_res) { + errors = true; + return false; // break + } + + var k = match_res[1]; + if (Ext.isDefined(res[k])) { + errors = true; + return false; // break + } + + res[k] = match_res[2]; + }); + + if (errors || !res.cputype) { + return; + } + + return res; + }, + + printQemuCpu: function(cpu) { + var cpustr = cpu.cputype; + var optstr = ''; + + Ext.Object.each(cpu, function(key, value) { + if (!Ext.isDefined(value) || key === 'cputype') { + return; // continue + } + optstr += ',' + key + '=' + value; + }); + + if (!cpustr) { + if (optstr) { + return 'kvm64' + optstr; + } + return; + } + + return cpustr + optstr; + }, + + parseSSHKey: function(key) { + // |--- options can have quotes--| type key comment + var keyre = /^(?:((?:[^\s"]|\"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/; + var typere = /^(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)$/; + + var m = key.match(keyre); + if (!m) { + return null; + } + if (m.length < 3 || !m[2]) { // [2] is always either type or key + return null; + } + if (m[1] && m[1].match(typere)) { + return { + type: m[1], + key: m[2], + comment: m[3] + }; + } + if (m[2].match(typere)) { + return { + options: m[1], + type: m[2], + key: m[3], + comment: m[4] + }; + } + return null; + } +}}); +/* This state provider keeps part of the state inside + * the browser history. + * + * We compress (shorten) url using dictionary based compression + * i.e. use column separated list instead of url encoded hash: + * #v\d* version/format + * := indicates string values + * :\d+ lookup value in dictionary hash + * #v1:=value1:5:=value2:=value3:... +*/ + +Ext.define('PVE.StateProvider', { + extend: 'Ext.state.LocalStorageProvider', + + // private + setHV: function(name, newvalue, fireEvents) { + var me = this; + + var changes = false; + var oldtext = Ext.encode(me.UIState[name]); + var newtext = Ext.encode(newvalue); + if (newtext != oldtext) { + changes = true; + me.UIState[name] = newvalue; + //console.log("changed old " + name + " " + oldtext); + //console.log("changed new " + name + " " + newtext); + if (fireEvents) { + me.fireEvent("statechange", me, name, { value: newvalue }); + } + } + return changes; + }, + + // private + hslist: [ + // order is important for notifications + // [ name, default ] + ['view', 'server'], + ['rid', 'root'], + ['ltab', 'tasks'], + ['nodetab', ''], + ['storagetab', ''], + ['pooltab', ''], + ['kvmtab', ''], + ['lxctab', ''], + ['dctab', ''] + ], + + hprefix: 'v1', + + compDict: { + cloudinit: 52, + replication: 51, + system: 50, + monitor: 49, + 'ha-fencing': 48, + 'ha-groups': 47, + 'ha-resources': 46, + 'ceph-log': 45, + 'ceph-crushmap':44, + 'ceph-pools': 43, + 'ceph-osdtree': 42, + 'ceph-disklist': 41, + 'ceph-monlist': 40, + 'ceph-config': 39, + ceph: 38, + 'firewall-fwlog': 37, + 'firewall-options': 36, + 'firewall-ipset': 35, + 'firewall-aliases': 34, + 'firewall-sg': 33, + firewall: 32, + apt: 31, + members: 30, + snapshot: 29, + ha: 28, + support: 27, + pools: 26, + syslog: 25, + ubc: 24, + initlog: 23, + openvz: 22, + backup: 21, + resources: 20, + content: 19, + root: 18, + domains: 17, + roles: 16, + groups: 15, + users: 14, + time: 13, + dns: 12, + network: 11, + services: 10, + options: 9, + console: 8, + hardware: 7, + permissions: 6, + summary: 5, + tasks: 4, + clog: 3, + storage: 2, + folder: 1, + server: 0 + }, + + decodeHToken: function(token) { + var me = this; + + var state = {}; + if (!token) { + Ext.Array.each(me.hslist, function(rec) { + state[rec[0]] = rec[1]; + }); + return state; + } + + // return Ext.urlDecode(token); + + var items = token.split(':'); + var prefix = items.shift(); + + if (prefix != me.hprefix) { + return me.decodeHToken(); + } + + Ext.Array.each(me.hslist, function(rec) { + var value = items.shift(); + if (value) { + if (value[0] === '=') { + value = decodeURIComponent(value.slice(1)); + } else { + Ext.Object.each(me.compDict, function(key, cv) { + if (value == cv) { + value = key; + return false; + } + }); + } + } + state[rec[0]] = value; + }); + + return state; + }, + + encodeHToken: function(state) { + var me = this; + + // return Ext.urlEncode(state); + + var ctoken = me.hprefix; + Ext.Array.each(me.hslist, function(rec) { + var value = state[rec[0]]; + if (!Ext.isDefined(value)) { + value = rec[1]; + } + value = encodeURIComponent(value); + if (!value) { + ctoken += ':'; + } else { + var comp = me.compDict[value]; + if (Ext.isDefined(comp)) { + ctoken += ":" + comp; + } else { + ctoken += ":=" + value; + } + } + }); + + return ctoken; + }, + + constructor: function(config){ + var me = this; + + me.callParent([config]); + + me.UIState = me.decodeHToken(); // set default + + var history_change_cb = function(token) { + //console.log("HC " + token); + if (!token) { + var res = window.confirm(gettext('Are you sure you want to navigate away from this page?')); + if (res){ + // process text value and close... + Ext.History.back(); + } else { + Ext.History.forward(); + } + return; + } + + var newstate = me.decodeHToken(token); + Ext.Array.each(me.hslist, function(rec) { + if (typeof newstate[rec[0]] == "undefined") { + return; + } + me.setHV(rec[0], newstate[rec[0]], true); + }); + }; + + var start_token = Ext.History.getToken(); + if (start_token) { + history_change_cb(start_token); + } else { + var htext = me.encodeHToken(me.UIState); + Ext.History.add(htext); + } + + Ext.History.on('change', history_change_cb); + }, + + get: function(name, defaultValue){ + /*jslint confusion: true */ + var me = this; + var data; + + if (typeof me.UIState[name] != "undefined") { + data = { value: me.UIState[name] }; + } else { + data = me.callParent(arguments); + if (!data && name === 'GuiCap') { + data = { vms: {}, storage: {}, access: {}, nodes: {}, dc: {} }; + } + } + + //console.log("GET " + name + " " + Ext.encode(data)); + return data; + }, + + clear: function(name){ + var me = this; + + if (typeof me.UIState[name] != "undefined") { + me.UIState[name] = null; + } + + me.callParent(arguments); + }, + + set: function(name, value, fireevent){ + var me = this; + + //console.log("SET " + name + " " + Ext.encode(value)); + if (typeof me.UIState[name] != "undefined") { + var newvalue = value ? value.value : null; + if (me.setHV(name, newvalue, fireevent)) { + var htext = me.encodeHToken(me.UIState); + Ext.History.add(htext); + } + } else { + me.callParent(arguments); + } + } +}); +Ext.define('PVE.menu.Item', { + extend: 'Ext.menu.Item', + alias: 'widget.pveMenuItem', + + // set to wrap the handler callback in a confirm dialog showing this text + confirmMsg: false, + + // set to focus 'No' instead of 'Yes' button and show a warning symbol + dangerous: false, + + initComponent: function() { + var me = this; + + if (me.handler) { + me.setHandler(me.handler, me.scope); + } + + me.callParent(); + }, + + setHandler: function(fn, scope) { + var me = this; + me.scope = scope; + me.handler = function(button, e) { + var rec, msg; + if (me.confirmMsg) { + msg = me.confirmMsg; + Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: msg, + buttons: Ext.Msg.YESNO, + defaultFocus: me.dangerous ? 'no' : 'yes', + callback: function(btn) { + if (btn === 'yes') { + Ext.callback(fn, me.scope, [me, e], 0, me); + } + } + }); + } else { + Ext.callback(fn, me.scope, [me, e], 0, me); + } + }; + } +}); +Ext.define('PVE.menu.TemplateMenu', { + extend: 'Ext.menu.Menu', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var guestType = me.pveSelNode.data.type; + if (guestType !== 'qemu' && guestType != 'lxc') { + throw "invalid guest type"; + } + + var vmname = me.pveSelNode.data.name; + + var template = me.pveSelNode.data.template; + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + nodename + '/' + guestType + '/' + vmid + "/status/" + cmd, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + vmid; + + me.items = [ + { + text: gettext('Migrate'), + iconCls: 'fa fa-fw fa-send-o', + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: guestType, + nodename: nodename, + vmid: vmid + }); + win.show(); + } + }, + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + handler: function() { + var win = Ext.create('PVE.window.Clone', { + nodename: nodename, + guestType: guestType, + vmid: vmid, + isTemplate: template + }); + win.show(); + } + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.button.ConsoleButton', { + extend: 'Ext.button.Split', + alias: 'widget.pveConsoleButton', + + consoleType: 'shell', // one of 'shell', 'kvm', 'lxc', 'upgrade', 'cmd' + + cmd: undefined, + + consoleName: undefined, + + iconCls: 'fa fa-terminal', + + enableSpice: true, + enableXtermjs: true, + + nodename: undefined, + + vmid: 0, + + text: gettext('Console'), + + setEnableSpice: function(enable){ + var me = this; + + me.enableSpice = enable; + me.down('#spicemenu').setDisabled(!enable); + }, + + setEnableXtermJS: function(enable){ + var me = this; + + me.enableXtermjs = enable; + me.down('#xtermjs').setDisabled(!enable); + }, + + handler: function() { + var me = this; + var consoles = { + spice: me.enableSpice, + xtermjs: me.enableXtermjs + }; + PVE.Utils.openDefaultConsoleWindow(consoles, me.consoleType, me.vmid, + me.nodename, me.consoleName, me.cmd); + }, + + menu: [ + { + xtype:'menuitem', + text: 'noVNC', + iconCls: 'pve-itype-icon-novnc', + type: 'html5', + handler: function(button) { + var me = this.up('button'); + PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd); + } + }, + { + xterm: 'menuitem', + itemId: 'spicemenu', + text: 'SPICE', + type: 'vv', + iconCls: 'pve-itype-icon-virt-viewer', + handler: function(button) { + var me = this.up('button'); + PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd); + } + }, + { + text: 'xterm.js', + itemId: 'xtermjs', + iconCls: 'pve-itype-icon-xtermjs', + type: 'xtermjs', + handler: function(button) { + var me = this.up('button'); + PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd); + } + } + ], + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.callParent(); + } +}); +/* Button features: + * - observe selection changes to enable/disable the button using enableFn() + * - pop up confirmation dialog using confirmMsg() + * + * does this for the button and every menu item + */ +Ext.define('PVE.button.Split', { + extend: 'Ext.button.Split', + alias: 'widget.pveSplitButton', + + // the selection model to observe + selModel: undefined, + + // if 'false' handler will not be called (button disabled) + enableFn: function(record) { }, + + // function(record) or text + confirmMsg: false, + + // take special care in confirm box (select no as default). + dangerous: false, + + handlerWrapper: function(button, event) { + var me = this; + var rec, msg; + if (me.selModel) { + rec = me.selModel.getSelection()[0]; + if (!rec || (me.enableFn(rec) === false)) { + return; + } + } + + if (me.confirmMsg) { + msg = me.confirmMsg; + // confirMsg can be boolean or function + /*jslint confusion: true*/ + if (Ext.isFunction(me.confirmMsg)) { + msg = me.confirmMsg(rec); + } + /*jslint confusion: false*/ + Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: msg, + buttons: Ext.Msg.YESNO, + callback: function(btn) { + if (btn !== 'yes') { + return; + } + me.realHandler(button, event, rec); + } + }); + } else { + me.realHandler(button, event, rec); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + + var me = this; + + if (me.handler) { + me.realHandler = me.handler; + me.handler = me.handlerWrapper; + } + + if (me.menu && me.menu.items) { + me.menu.items.forEach(function(item) { + if (item.handler) { + item.realHandler = item.handler; + item.handler = me.handlerWrapper; + } + + if (item.selModel) { + me.mon(item.selModel, "selectionchange", function() { + var rec = item.selModel.getSelection()[0]; + if (!rec || (item.enableFn(rec) === false )) { + item.setDisabled(true); + } else { + item.setDisabled(false); + } + }); + } + }); + } + + me.callParent(); + + if (me.selModel) { + + me.mon(me.selModel, "selectionchange", function() { + var rec = me.selModel.getSelection()[0]; + if (!rec || (me.enableFn(rec) === false)) { + me.setDisabled(true); + } else { + me.setDisabled(false); + } + }); + } + } +}); +Ext.define('PVE.controller.StorageEdit', { + extend: 'Ext.app.ViewController', + alias: 'controller.storageEdit', + control: { + 'field[name=content]': { + change: function(field, value) { + var hasBackups = Ext.Array.contains(value, 'backup'); + var maxfiles = this.lookupReference('maxfiles'); + if (!maxfiles) { + return; + } + + if (!hasBackups) { + // clear values which will never be submitted + maxfiles.reset(); + } + maxfiles.setDisabled(!hasBackups); + } + } + } +}); +Ext.define('PVE.qemu.CmdMenu', { + extend: 'Ext.menu.Menu', + + showSeparator: false, + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var vmname = me.pveSelNode.data.name; + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + nodename + '/qemu/' + vmid + "/status/" + cmd, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + var caps = Ext.state.Manager.get('GuiCap'); + + var running = false; + var stopped = true; + var suspended = false; + var standalone = PVE.data.ResourceStore.getNodes().length < 2; + + switch (me.pveSelNode.data.status) { + case 'running': + running = true; + stopped = false; + break; + case 'suspended': + stopped = false; + suspended = true; + break; + case 'paused': + stopped = false; + suspended = true; + break; + default: break; + } + + me.title = "VM " + vmid; + + me.items = [ + { + text: gettext('Start'), + iconCls: 'fa fa-fw fa-play', + hidden: running || suspended, + disabled: running || suspended, + handler: function() { + vm_command('start'); + } + }, + { + text: gettext('Pause'), + iconCls: 'fa fa-fw fa-pause', + hidden: stopped || suspended, + disabled: stopped || suspended, + handler: function() { + var msg = Proxmox.Utils.format_task_description('qmpause', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + vm_command('suspend'); + }); + } + }, + { + text: gettext('Hibernate'), + iconCls: 'fa fa-fw fa-download', + hidden: stopped || suspended, + disabled: stopped || suspended, + tooltip: gettext('Suspend to disk'), + handler: function() { + var msg = Proxmox.Utils.format_task_description('qmsuspend', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + vm_command('suspend', { todisk: 1 }); + }); + } + }, + { + text: gettext('Resume'), + iconCls: 'fa fa-fw fa-play', + hidden: !suspended, + handler: function() { + vm_command('resume'); + } + }, + { + text: gettext('Shutdown'), + iconCls: 'fa fa-fw fa-power-off', + disabled: stopped || suspended, + handler: function() { + var msg = Proxmox.Utils.format_task_description('qmshutdown', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command('shutdown'); + }); + } + }, + { + text: gettext('Stop'), + iconCls: 'fa fa-fw fa-stop', + disabled: stopped, + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'), + handler: function() { + var msg = Proxmox.Utils.format_task_description('qmstop', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command("stop"); + }); + } + }, + { + xtype: 'menuseparator', + hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone'] + }, + { + text: gettext('Migrate'), + iconCls: 'fa fa-fw fa-send-o', + hidden: standalone || !caps.vms['VM.Migrate'], + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'qemu', + nodename: nodename, + vmid: vmid + }); + win.show(); + } + }, + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: !caps.vms['VM.Clone'], + handler: function() { + PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'qemu'); + } + }, + { + text: gettext('Convert to template'), + iconCls: 'fa fa-fw fa-file-o', + hidden: !caps.vms['VM.Allocate'], + handler: function() { + var msg = Proxmox.Utils.format_task_description('qmtemplate', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/qemu/' + vmid + '/template', + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }); + } + }, + { xtype: 'menuseparator' }, + { + text: gettext('Console'), + iconCls: 'fa fa-fw fa-terminal', + handler: function() { + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts) { + var allowSpice = response.result.data.spice; + var allowXtermjs = response.result.data.serial; + var consoles = { + spice: allowSpice, + xtermjs: allowXtermjs + }; + PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname); + } + }); + } + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.lxc.CmdMenu', { + extend: 'Ext.menu.Menu', + + showSeparator: false, + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no CT ID specified"; + } + var vmname = me.pveSelNode.data.name; + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + nodename + '/lxc/' + vmid + "/status/" + cmd, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + var caps = Ext.state.Manager.get('GuiCap'); + + var running = false; + var stopped = true; + var suspended = false; + var standalone = PVE.data.ResourceStore.getNodes().length < 2; + + switch (me.pveSelNode.data.status) { + case 'running': + running = true; + stopped = false; + break; + case 'paused': + stopped = false; + suspended = true; + break; + default: break; + } + + me.title = 'CT ' + vmid; + + me.items = [ + { + text: gettext('Start'), + iconCls: 'fa fa-fw fa-play', + disabled: running, + handler: function() { + vm_command('start'); + } + }, +// { +// text: gettext('Suspend'), +// iconCls: 'fa fa-fw fa-pause', +// hidde: suspended, +// disabled: stopped || suspended, +// handler: function() { +// var msg = Proxmox.Utils.format_task_description('vzsuspend', vmid); +// Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { +// if (btn !== 'yes') { +// return; +// } +// +// vm_command('suspend'); +// }); +// } +// }, +// { +// text: gettext('Resume'), +// iconCls: 'fa fa-fw fa-play', +// hidden: !suspended, +// handler: function() { +// vm_command('resume'); +// } +// }, + { + text: gettext('Shutdown'), + iconCls: 'fa fa-fw fa-power-off', + disabled: stopped || suspended, + handler: function() { + var msg = Proxmox.Utils.format_task_description('vzshutdown', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command('shutdown'); + }); + } + }, + { + text: gettext('Stop'), + iconCls: 'fa fa-fw fa-stop', + disabled: stopped, + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'), + handler: function() { + var msg = Proxmox.Utils.format_task_description('vzstop', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command("stop"); + }); + } + }, + { + xtype: 'menuseparator', + hidden: standalone || !caps.vms['VM.Migrate'] + }, + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: !caps.vms['VM.Clone'], + handler: function() { + PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'lxc'); + } + }, + { + text: gettext('Migrate'), + iconCls: 'fa fa-fw fa-send-o', + hidden: standalone || !caps.vms['VM.Migrate'], + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'lxc', + nodename: nodename, + vmid: vmid + }); + win.show(); + } + }, + { + text: gettext('Convert to template'), + iconCls: 'fa fa-fw fa-file-o', + handler: function() { + var msg = Proxmox.Utils.format_task_description('vztemplate', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/lxc/' + vmid + '/template', + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }); + } + }, + { xtype: 'menuseparator' }, + { + text: gettext('Console'), + iconCls: 'fa fa-fw fa-terminal', + handler: function() { + PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname); + } + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.node.CmdMenu', { + extend: 'Ext.menu.Menu', + xtype: 'nodeCmdMenu', + + showSeparator: false, + + items: [ + { + text: gettext('Create VM'), + itemId: 'createvm', + iconCls: 'fa fa-desktop', + handler: function() { + var me = this.up('menu'); + var wiz = Ext.create('PVE.qemu.CreateWizard', { + nodename: me.nodename + }); + wiz.show(); + } + }, + { + text: gettext('Create CT'), + itemId: 'createct', + iconCls: 'fa fa-cube', + handler: function() { + var me = this.up('menu'); + var wiz = Ext.create('PVE.lxc.CreateWizard', { + nodename: me.nodename + }); + wiz.show(); + } + }, + { xtype: 'menuseparator' }, + { + text: gettext('Bulk Start'), + itemId: 'bulkstart', + iconCls: 'fa fa-fw fa-play', + handler: function() { + var me = this.up('menu'); + var win = Ext.create('PVE.window.BulkAction', { + nodename: me.nodename, + title: gettext('Bulk Start'), + btnText: gettext('Start'), + action: 'startall' + }); + win.show(); + } + }, + { + text: gettext('Bulk Stop'), + itemId: 'bulkstop', + iconCls: 'fa fa-fw fa-stop', + handler: function() { + var me = this.up('menu'); + var win = Ext.create('PVE.window.BulkAction', { + nodename: me.nodename, + title: gettext('Bulk Stop'), + btnText: gettext('Stop'), + action: 'stopall' + }); + win.show(); + } + }, + { + text: gettext('Bulk Migrate'), + itemId: 'bulkmigrate', + iconCls: 'fa fa-fw fa-send-o', + handler: function() { + var me = this.up('menu'); + var win = Ext.create('PVE.window.BulkAction', { + nodename: me.nodename, + title: gettext('Bulk Migrate'), + btnText: gettext('Migrate'), + action: 'migrateall' + }); + win.show(); + } + }, + { xtype: 'menuseparator' }, + { + text: gettext('Shell'), + itemId: 'shell', + iconCls: 'fa fa-fw fa-terminal', + handler: function() { + var me = this.up('menu'); + PVE.Utils.openDefaultConsoleWindow(true, 'shell', undefined, me.nodename, undefined); + } + }, + { xtype: 'menuseparator' }, + { + text: gettext('Wake-on-LAN'), + itemId: 'wakeonlan', + iconCls: 'fa fa-fw fa-power-off', + handler: function() { + var me = this.up('menu'); + Proxmox.Utils.API2Request({ + param: {}, + url: '/nodes/' + me.nodename + '/wakeonlan', + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + Ext.Msg.show({ + title: 'Success', + icon: Ext.Msg.INFO, + msg: Ext.String.format(gettext("Wake on LAN packet send for '{0}': '{1}'"), me.nodename, response.result.data) + }); + } + }); + } + } + ], + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw 'no nodename specified'; + } + + me.title = gettext('Node') + " '" + me.nodename + "'"; + me.callParent(); + + var caps = Ext.state.Manager.get('GuiCap'); + // disable not allowed options + if (!caps.vms['VM.Allocate']) { + me.getComponent('createct').setDisabled(true); + me.getComponent('createvm').setDisabled(true); + } + + if (!caps.nodes['Sys.PowerMgmt']) { + me.getComponent('bulkstart').setDisabled(true); + me.getComponent('bulkstop').setDisabled(true); + me.getComponent('bulkmigrate').setDisabled(true); + me.getComponent('wakeonlan').setDisabled(true); + } + + if (!caps.nodes['Sys.Console']) { + me.getComponent('shell').setDisabled(true); + } + + if (me.pveSelNode.data.running) { + me.getComponent('wakeonlan').setDisabled(true); + } + } +}); +Ext.define('PVE.noVncConsole', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNoVncConsole', + + nodename: undefined, + + vmid: undefined, + + cmd: undefined, + + consoleType: undefined, // lxc, kvm, shell, cmd + + layout: 'fit', + + xtermjs: false, + + border: false, + + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.consoleType) { + throw "no console type specified"; + } + + if (!me.vmid && me.consoleType !== 'shell' && me.consoleType !== 'cmd') { + throw "no VM ID specified"; + } + + // always use same iframe, to avoid running several noVnc clients + // at same time (to avoid performance problems) + var box = Ext.create('Ext.ux.IFrame', { itemid : "vncconsole" }); + + var type = me.xtermjs ? 'xtermjs' : 'novnc'; + Ext.apply(me, { + items: box, + listeners: { + activate: function() { + var queryDict = { + console: me.consoleType, // kvm, lxc, upgrade or shell + vmid: me.vmid, + node: me.nodename, + cmd: me.cmd, + resize: 'scale' + }; + queryDict[type] = 1; + PVE.Utils.cleanEmptyObjectKeys(queryDict); + var url = '/?' + Ext.Object.toQueryString(queryDict); + box.load(url); + } + } + }); + + me.callParent(); + + me.on('afterrender', function() { + me.focus(); + }); + } +}); + +Ext.define('PVE.data.PermPathStore', { + extend: 'Ext.data.Store', + alias: 'store.pvePermPath', + fields: [ 'value' ], + autoLoad: false, + data: [ + {'value': '/'}, + {'value': '/access'}, + {'value': '/nodes'}, + {'value': '/pool'}, + {'value': '/storage'}, + {'value': '/vms'} + ], + + constructor: function(config) { + var me = this; + + config = config || {}; + + me.callParent([config]); + + me.suspendEvents(); + PVE.data.ResourceStore.each(function(record) { + switch (record.get('type')) { + case 'node': + me.add({value: '/nodes/' + record.get('text')}); + break; + + case 'qemu': + me.add({value: '/vms/' + record.get('vmid')}); + break; + + case 'lxc': + me.add({value: '/vms/' + record.get('vmid')}); + break; + + case 'storage': + me.add({value: '/storage/' + record.get('storage')}); + break; + case 'pool': + me.add({value: '/pool/' + record.get('pool')}); + break; + } + }); + me.resumeEvents(); + + me.fireEvent('refresh', me); + me.fireEvent('datachanged', me); + + me.sort({ + property: 'value', + direction: 'ASC' + }); + } +}); +Ext.define('PVE.data.ResourceStore', { + extend: 'Proxmox.data.UpdateStore', + singleton: true, + + findVMID: function(vmid) { + var me = this, i; + + return (me.findExact('vmid', parseInt(vmid, 10)) >= 0); + }, + + // returns the cached data from all nodes + getNodes: function() { + var me = this; + + var nodes = []; + me.each(function(record) { + if (record.get('type') == "node") { + nodes.push( record.getData() ); + } + }); + + return nodes; + }, + + storageIsShared: function(storage_path) { + var me = this; + + var index = me.findExact('id', storage_path); + + return me.getAt(index).data.shared; + }, + + guestNode: function(vmid) { + var me = this; + + var index = me.findExact('vmid', parseInt(vmid, 10)); + + return me.getAt(index).data.node; + }, + + constructor: function(config) { + // fixme: how to avoid those warnings + /*jslint confusion: true */ + + var me = this; + + config = config || {}; + + var field_defaults = { + type: { + header: gettext('Type'), + type: 'string', + renderer: PVE.Utils.render_resource_type, + sortable: true, + hideable: false, + width: 100 + }, + id: { + header: 'ID', + type: 'string', + hidden: true, + sortable: true, + width: 80 + }, + running: { + header: gettext('Online'), + type: 'boolean', + renderer: Proxmox.Utils.format_boolean, + hidden: true, + convert: function(value, record) { + var info = record.data; + return (Ext.isNumeric(info.uptime) && (info.uptime > 0)); + } + }, + text: { + header: gettext('Description'), + type: 'string', + sortable: true, + width: 200, + convert: function(value, record) { + var info = record.data; + var text; + + if (value) { + return value; + } + + if (Ext.isNumeric(info.vmid) && info.vmid > 0) { + text = String(info.vmid); + if (info.name) { + text += " (" + info.name + ')'; + } + } else { // node, pool, storage + text = info[info.type] || info.id; + if (info.node && info.type !== 'node') { + text += " (" + info.node + ")"; + } + } + + return text; + } + }, + vmid: { + header: 'VMID', + type: 'integer', + hidden: true, + sortable: true, + width: 80 + }, + name: { + header: gettext('Name'), + hidden: true, + sortable: true, + type: 'string' + }, + disk: { + header: gettext('Disk usage'), + type: 'integer', + renderer: PVE.Utils.render_disk_usage, + sortable: true, + width: 100, + hidden: true + }, + diskuse: { + header: gettext('Disk usage') + " %", + type: 'number', + sortable: true, + renderer: PVE.Utils.render_disk_usage_percent, + width: 100, + calculate: PVE.Utils.calculate_disk_usage, + sortType: 'asFloat' + }, + maxdisk: { + header: gettext('Disk size'), + type: 'integer', + renderer: PVE.Utils.render_size, + sortable: true, + hidden: true, + width: 100 + }, + mem: { + header: gettext('Memory usage'), + type: 'integer', + renderer: PVE.Utils.render_mem_usage, + sortable: true, + hidden: true, + width: 100 + }, + memuse: { + header: gettext('Memory usage') + " %", + type: 'number', + renderer: PVE.Utils.render_mem_usage_percent, + calculate: PVE.Utils.calculate_mem_usage, + sortType: 'asFloat', + sortable: true, + width: 100 + }, + maxmem: { + header: gettext('Memory size'), + type: 'integer', + renderer: PVE.Utils.render_size, + hidden: true, + sortable: true, + width: 100 + }, + cpu: { + header: gettext('CPU usage'), + type: 'float', + renderer: PVE.Utils.render_cpu, + sortable: true, + width: 100 + }, + maxcpu: { + header: gettext('maxcpu'), + type: 'integer', + hidden: true, + sortable: true, + width: 60 + }, + diskread: { + header: gettext('Total Disk Read'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100 + }, + diskwrite: { + header: gettext('Total Disk Write'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100 + }, + netin: { + header: gettext('Total NetIn'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100 + }, + netout: { + header: gettext('Total NetOut'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100 + }, + template: { + header: gettext('Template'), + type: 'integer', + hidden: true, + sortable: true, + width: 60 + }, + uptime: { + header: gettext('Uptime'), + type: 'integer', + renderer: Proxmox.Utils.render_uptime, + sortable: true, + width: 110 + }, + node: { + header: gettext('Node'), + type: 'string', + hidden: true, + sortable: true, + width: 110 + }, + storage: { + header: gettext('Storage'), + type: 'string', + hidden: true, + sortable: true, + width: 110 + }, + pool: { + header: gettext('Pool'), + type: 'string', + hidden: true, + sortable: true, + width: 110 + }, + hastate: { + header: gettext('HA State'), + type: 'string', + defaultValue: 'unmanaged', + hidden: true, + sortable: true + }, + status: { + header: gettext('Status'), + type: 'string', + hidden: true, + sortable: true, + width: 110 + }, + lock: { + header: gettext('Lock'), + type: 'string', + hidden: true, + sortable: true, + width: 110 + } + }; + + var fields = []; + var fieldNames = []; + Ext.Object.each(field_defaults, function(key, value) { + var field = {name: key, type: value.type}; + if (Ext.isDefined(value.convert)) { + field.convert = value.convert; + } + + if (Ext.isDefined(value.calculate)) { + field.calculate = value.calculate; + } + + if (Ext.isDefined(value.defaultValue)) { + field.defaultValue = value.defaultValue; + } + + fields.push(field); + fieldNames.push(key); + }); + + Ext.define('PVEResources', { + extend: "Ext.data.Model", + fields: fields, + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/resources' + } + }); + + Ext.define('PVETree', { + extend: "Ext.data.Model", + fields: fields, + proxy: { type: 'memory' } + }); + + Ext.apply(config, { + storeid: 'PVEResources', + model: 'PVEResources', + defaultColumns: function() { + var res = []; + Ext.Object.each(field_defaults, function(field, info) { + var fi = Ext.apply({ dataIndex: field }, info); + res.push(fi); + }); + return res; + }, + fieldNames: fieldNames + }); + + me.callParent([config]); + } +}); +Ext.define('pve-domains', { + extend: "Ext.data.Model", + fields: [ + 'realm', 'type', 'comment', 'default', 'tfa', + { + name: 'descr', + // Note: We use this in the RealmComboBox.js (see Bug #125) + convert: function(value, record) { + if (value) { + return value; + } + + var info = record.data; + // return realm if there is no comment + var text = info.comment || info.realm; + + if (info.tfa) { + text += " (+ " + info.tfa + ")"; + } + + return Ext.String.htmlEncode(text); + } + } + ], + idProperty: 'realm', + proxy: { + type: 'proxmox', + url: "/api2/json/access/domains" + } +}); +Ext.define('pve-rrd-node', { + extend: 'Ext.data.Model', + fields: [ + { + name:'cpu', + // percentage + convert: function(value) { + return value*100; + } + }, + { + name:'iowait', + // percentage + convert: function(value) { + return value*100; + } + }, + 'loadavg', + 'maxcpu', + 'memtotal', + 'memused', + 'netin', + 'netout', + 'roottotal', + 'rootused', + 'swaptotal', + 'swapused', + { type: 'date', dateFormat: 'timestamp', name: 'time' } + ] +}); + +Ext.define('pve-rrd-guest', { + extend: 'Ext.data.Model', + fields: [ + { + name:'cpu', + // percentage + convert: function(value) { + return value*100; + } + }, + 'maxcpu', + 'netin', + 'netout', + 'mem', + 'maxmem', + 'disk', + 'maxdisk', + 'diskread', + 'diskwrite', + { type: 'date', dateFormat: 'timestamp', name: 'time' } + ] +}); + +Ext.define('pve-rrd-storage', { + extend: 'Ext.data.Model', + fields: [ + 'used', + 'total', + { type: 'date', dateFormat: 'timestamp', name: 'time' } + ] +}); +Ext.define('PVE.form.VlanField', { + extend: 'Ext.form.field.Number', + alias: ['widget.pveVlanField'], + + deleteEmpty: false, + + emptyText: 'no VLAN', + + fieldLabel: gettext('VLAN Tag'), + + allowBlank: true, + + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue) { + val = me.getSubmitValue(); + if (val) { + data = {}; + data[me.getName()] = val; + } else if (me.deleteEmpty) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + }, + + initComponent: function() { + var me = this; + + Ext.apply(me, { + minValue: 1, + maxValue: 4094 + }); + + me.callParent(); + } +}); +// boolean type including 'Default' (delete property from file) +Ext.define('PVE.form.Boolean', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.booleanfield'], + comboItems: [ + ['__default__', gettext('Default')], + [1, gettext('Yes')], + [0, gettext('No')] + ] +}); +Ext.define('PVE.form.CompressionSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveCompressionSelector'], + comboItems: [ + ['0', Proxmox.Utils.noneText], + ['lzo', 'LZO (' + gettext('fast') + ')'], + ['gzip', 'GZIP (' + gettext('good') + ')'] + ] +}); +Ext.define('PVE.form.PoolSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pvePoolSelector'], + + allowBlank: false, + valueField: 'poolid', + displayField: 'poolid', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-pools', + sorters: 'poolid' + }); + + Ext.apply(me, { + store: store, + autoSelect: false, + listConfig: { + columns: [ + { + header: gettext('Pool'), + sortable: true, + dataIndex: 'poolid', + flex: 1 + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ] + } + }); + + me.callParent(); + + store.load(); + } + +}, function() { + + Ext.define('pve-pools', { + extend: 'Ext.data.Model', + fields: [ 'poolid', 'comment' ], + proxy: { + type: 'proxmox', + url: "/api2/json/pools" + }, + idProperty: 'poolid' + }); + +}); +Ext.define('PVE.form.PrivilegesSelector', { + extend: 'Proxmox.form.KVComboBox', + xtype: 'pvePrivilegesSelector', + + multiSelect: true, + + initComponent: function() { + var me = this; + + // So me.store is available. + me.callParent(); + + Proxmox.Utils.API2Request({ + url: '/access/roles/Administrator', + method: 'GET', + success: function(response, options) { + var data = [], key; + /*jslint forin: true */ + for (key in response.result.data) { + data.push([key, key]); + } + /*jslint forin: false */ + + me.store.setData(data); + + me.store.sort({ + property: 'key', + direction: 'ASC' + }); + }, + + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } +}); +Ext.define('pve-groups', { + extend: 'Ext.data.Model', + fields: [ 'groupid', 'comment' ], + proxy: { + type: 'proxmox', + url: "/api2/json/access/groups" + }, + idProperty: 'groupid' +}); + +Ext.define('PVE.form.GroupSelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pveGroupSelector', + + allowBlank: false, + autoSelect: false, + valueField: 'groupid', + displayField: 'groupid', + listConfig: { + columns: [ + { + header: gettext('Group'), + sortable: true, + dataIndex: 'groupid', + flex: 1 + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ] + }, + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-groups', + sorters: [{ + property: 'groupid' + }] + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + store.load(); + } +}); +Ext.define('PVE.form.UserSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveUserSelector'], + + allowBlank: false, + autoSelect: false, + valueField: 'userid', + displayField: 'userid', + + editable: true, + anyMatch: true, + forceSelection: true, + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-users', + sorters: [{ + property: 'userid' + }] + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: gettext('User'), + sortable: true, + dataIndex: 'userid', + flex: 1 + }, + { + header: gettext('Name'), + sortable: true, + renderer: PVE.Utils.render_full_name, + dataIndex: 'firstname', + flex: 1 + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ] + } + }); + + me.callParent(); + + store.load({ params: { enabled: 1 }}); + } + +}, function() { + + Ext.define('pve-users', { + extend: 'Ext.data.Model', + fields: [ + 'userid', 'firstname', 'lastname' , 'email', 'comment', + { type: 'boolean', name: 'enable' }, + { type: 'date', dateFormat: 'timestamp', name: 'expire' } + ], + proxy: { + type: 'proxmox', + url: "/api2/json/access/users" + }, + idProperty: 'userid' + }); + +}); + + +Ext.define('PVE.form.RoleSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveRoleSelector'], + + allowBlank: false, + autoSelect: false, + valueField: 'roleid', + displayField: 'roleid', + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-roles', + sorters: [{ + property: 'roleid' + }] + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: gettext('Role'), + sortable: true, + dataIndex: 'roleid', + flex: 1 + } + ] + } + }); + + me.callParent(); + + store.load(); + } + +}, function() { + + Ext.define('pve-roles', { + extend: 'Ext.data.Model', + fields: [ 'roleid', 'privs' ], + proxy: { + type: 'proxmox', + url: "/api2/json/access/roles" + }, + idProperty: 'roleid' + }); + +}); +Ext.define('PVE.form.GuestIDSelector', { + extend: 'Ext.form.field.Number', + alias: 'widget.pveGuestIDSelector', + + allowBlank: false, + + minValue: 100, + + maxValue: 999999999, + + validateExists: undefined, + + loadNextFreeID: false, + + guestType: undefined, + + validator: function(value) { + var me = this; + + if (!Ext.isNumeric(value) || + value < me.minValue || + value > me.maxValue) { + // check is done by ExtJS + return true; + } + + if (me.validateExists === true && !me.exists) { + return me.unknownID; + } + + if (me.validateExists === false && me.exists) { + return me.inUseID; + } + + return true; + }, + + initComponent: function() { + var me = this; + var label = '{0} ID'; + var unknownID = gettext('This {0} ID does not exists'); + var inUseID = gettext('This {0} ID is already in use'); + var type = 'CT/VM'; + + if (me.guestType === 'lxc') { + type = 'CT'; + } else if (me.guestType === 'qemu') { + type = 'VM'; + } + + me.label = Ext.String.format(label, type); + me.unknownID = Ext.String.format(unknownID, type); + me.inUseID = Ext.String.format(inUseID, type); + + Ext.apply(me, { + fieldLabel: me.label, + listeners: { + 'change': function(field, newValue, oldValue) { + if (!Ext.isDefined(me.validateExists)) { + return; + } + Proxmox.Utils.API2Request({ + params: { vmid: newValue }, + url: '/cluster/nextid', + method: 'GET', + success: function(response, opts) { + me.exists = false; + me.validate(); + }, + failure: function(response, opts) { + me.exists = true; + me.validate(); + } + }); + } + } + }); + + me.callParent(); + + if (me.loadNextFreeID) { + Proxmox.Utils.API2Request({ + url: '/cluster/nextid', + method: 'GET', + success: function(response, opts) { + me.setRawValue(response.result.data); + } + }); + } + } +}); +Ext.define('PVE.form.MemoryField', { + extend: 'Ext.form.field.Number', + alias: 'widget.pveMemoryField', + + allowBlank: false, + + hotplug: false, + + minValue: 32, + + maxValue: 4178944, + + step: 32, + + value: '512', // qm default + + allowDecimals: false, + + allowExponential: false, + + computeUpDown: function(value) { + var me = this; + + if (!me.hotplug) { + return { up: value + me.step, down: value - me.step }; + } + + var dimm_size = 512; + var prev_dimm_size = 0; + var min_size = 1024; + var current_size = min_size; + var value_up = min_size; + var value_down = min_size; + var value_start = min_size; + + var i, j; + for (j = 0; j < 9; j++) { + for (i = 0; i < 32; i++) { + if ((value >= current_size) && (value < (current_size + dimm_size))) { + value_start = current_size; + value_up = current_size + dimm_size; + value_down = current_size - ((i === 0) ? prev_dimm_size : dimm_size); + } + current_size += dimm_size; + } + prev_dimm_size = dimm_size; + dimm_size = dimm_size*2; + } + + return { up: value_up, down: value_down, start: value_start }; + }, + + onSpinUp: function() { + var me = this; + if (!me.readOnly) { + var res = me.computeUpDown(me.getValue()); + me.setValue(Ext.Number.constrain(res.up, me.minValue, me.maxValue)); + } + }, + + onSpinDown: function() { + var me = this; + if (!me.readOnly) { + var res = me.computeUpDown(me.getValue()); + me.setValue(Ext.Number.constrain(res.down, me.minValue, me.maxValue)); + } + }, + + initComponent: function() { + var me = this; + + if (me.hotplug) { + me.minValue = 1024; + + me.on('blur', function(field) { + var value = me.getValue(); + var res = me.computeUpDown(value); + if (value === res.start || value === res.up || value === res.down) { + return; + } + field.setValue(res.up); + }); + } + + me.callParent(); + } +}); +Ext.define('PVE.form.NetworkCardSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: 'widget.pveNetworkCardSelector', + comboItems: [ + ['e1000', 'Intel E1000'], + ['virtio', 'VirtIO (' + gettext('paravirtualized') + ')'], + ['rtl8139', 'Realtek RTL8139'], + ['vmxnet3', 'VMware vmxnet3'] + ] +}); +Ext.define('PVE.form.DiskFormatSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: 'widget.pveDiskFormatSelector', + comboItems: [ + ['raw', gettext('Raw disk image') + ' (raw)'], + ['qcow2', gettext('QEMU image format') + ' (qcow2)'], + ['vmdk', gettext('VMware image format') + ' (vmdk)'] + ] +}); +Ext.define('PVE.form.DiskSelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pveDiskSelector', + + // can be + // undefined: all + // unused: only unused + // journal_disk: all disks with gpt + diskType: undefined, + + valueField: 'devpath', + displayField: 'devpath', + emptyText: gettext('No Disks unused'), + listConfig: { + width: 600, + columns: [ + { + header: gettext('Device'), + flex: 3, + sortable: true, + dataIndex: 'devpath' + }, + { + header: gettext('Size'), + flex: 2, + sortable: false, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + }, + { + header: gettext('Serial'), + flex: 5, + sortable: true, + dataIndex: 'serial' + } + ] + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + if (!nodename) { + throw "no node name specified"; + } + + var store = Ext.create('Ext.data.Store', { + filterOnLoad: true, + model: 'pve-disk-list', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/disks/list", + extraParams: { type: me.diskType } + }, + sorters: [ + { + property : 'devpath', + direction: 'ASC' + } + ] + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + store.load(); + } +}, function() { + + Ext.define('pve-disk-list', { + extend: 'Ext.data.Model', + fields: [ 'devpath', 'used', { name: 'size', type: 'number'}, + {name: 'osdid', type: 'number'}, + 'vendor', 'model', 'serial'], + idProperty: 'devpath' + }); +}); +Ext.define('PVE.form.BusTypeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: 'widget.pveBusSelector', + + noVirtIO: false, + + initComponent: function() { + var me = this; + + me.comboItems = [['ide', 'IDE'], ['sata', 'SATA']]; + + if (!me.noVirtIO) { + me.comboItems.push(['virtio', 'VirtIO Block']); + } + + me.comboItems.push(['scsi', 'SCSI']); + + me.callParent(); + } +}); +Ext.define('PVE.form.ControllerSelector', { + extend: 'Ext.form.FieldContainer', + alias: 'widget.pveControllerSelector', + + statics: { + maxIds: { + ide: 3, + sata: 5, + virtio: 15, + scsi: 13 + } + }, + + noVirtIO: false, + + vmconfig: {}, // used to check for existing devices + + sortByPreviousUsage: function(vmconfig, controllerList) { + + var usedControllers = Ext.clone(PVE.form.ControllerSelector.maxIds); + + var type; + for (type in usedControllers) { + if(usedControllers.hasOwnProperty(type)) { + usedControllers[type] = 0; + } + } + + var property; + for (property in vmconfig) { + if (vmconfig.hasOwnProperty(property)) { + if (property.match(PVE.Utils.bus_match) && !vmconfig[property].match(/media=cdrom/)) { + var foundController = property.match(PVE.Utils.bus_match)[1]; + usedControllers[foundController]++; + } + } + } + + var vmDefaults = PVE.qemu.OSDefaults[vmconfig.ostype]; + + var sortPriority = vmDefaults && vmDefaults.busPriority + ? vmDefaults.busPriority : PVE.qemu.OSDefaults.generic; + + var sortedList = Ext.clone(controllerList); + sortedList.sort(function(a,b) { + if (usedControllers[b] == usedControllers[a]) { + return sortPriority[b] - sortPriority[a]; + } + return usedControllers[b] - usedControllers[a]; + }); + + return sortedList; + }, + + setVMConfig: function(vmconfig, autoSelect) { + var me = this; + + me.vmconfig = Ext.apply({}, vmconfig); + + var clist = ['ide', 'virtio', 'scsi', 'sata']; + var bussel = me.down('field[name=controller]'); + var deviceid = me.down('field[name=deviceid]'); + + if (autoSelect === 'cdrom') { + clist = ['ide', 'scsi', 'sata']; + if (!Ext.isDefined(me.vmconfig.ide2)) { + bussel.setValue('ide'); + deviceid.setValue(2); + return; + } + } else { + // in most cases we want to add a disk to the same controller + // we previously used + clist = me.sortByPreviousUsage(me.vmconfig, clist); + } + + Ext.Array.each(clist, function(controller) { + var confid, i; + bussel.setValue(controller); + for (i = 0; i <= PVE.form.ControllerSelector.maxIds[controller]; i++) { + confid = controller + i.toString(); + if (!Ext.isDefined(me.vmconfig[confid])) { + deviceid.setValue(i); + return false; // break + } + } + }); + deviceid.validate(); + }, + + initComponent: function() { + var me = this; + + Ext.apply(me, { + fieldLabel: gettext('Bus/Device'), + layout: 'hbox', + defaults: { + hideLabel: true + }, + items: [ + { + xtype: 'pveBusSelector', + name: 'controller', + value: PVE.qemu.OSDefaults.generic.busType, + noVirtIO: me.noVirtIO, + allowBlank: false, + flex: 2, + listeners: { + change: function(t, value) { + if (!value) { + return; + } + var field = me.down('field[name=deviceid]'); + field.setMaxValue(PVE.form.ControllerSelector.maxIds[value]); + field.validate(); + } + } + }, + { + xtype: 'proxmoxintegerfield', + name: 'deviceid', + minValue: 0, + maxValue: PVE.form.ControllerSelector.maxIds.ide, + value: '0', + flex: 1, + allowBlank: false, + validator: function(value) { + /*jslint confusion: true */ + if (!me.rendered) { + return; + } + var field = me.down('field[name=controller]'); + var controller = field.getValue(); + var confid = controller + value; + if (Ext.isDefined(me.vmconfig[confid])) { + return "This device is already in use."; + } + return true; + } + } + ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.form.EmailNotificationSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveEmailNotificationSelector'], + comboItems: [ + ['always', gettext('Always')], + ['failure', gettext('On failure only')] + ] +}); +/*global Proxmox*/ +Ext.define('PVE.form.RealmComboBox', { + extend: 'Ext.form.field.ComboBox', + alias: ['widget.pveRealmComboBox'], + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.store.on('load', this.onLoad, view); + }, + + onLoad: function(store, records, success) { + if (!success) { + return; + } + var me = this; + var val = me.getValue(); + if (!val || !me.store.findRecord('realm', val)) { + var def = 'pam'; + Ext.each(records, function(rec) { + if (rec.data && rec.data['default']) { + def = rec.data.realm; + } + }); + me.setValue(def); + } + } + }, + + fieldLabel: gettext('Realm'), + name: 'realm', + queryMode: 'local', + allowBlank: false, + editable: false, + forceSelection: true, + autoSelect: false, + triggerAction: 'all', + valueField: 'realm', + displayField: 'descr', + getState: function() { + return { value: this.getValue() }; + }, + applyState : function(state) { + if (state && state.value) { + this.setValue(state.value); + } + }, + stateEvents: [ 'select' ], + stateful: true, // last chosen auth realm is saved between page reloads + id: 'pveloginrealm', // We need stable ids when using stateful, not autogenerated + stateID: 'pveloginrealm', + + needOTP: function(realm) { + var me = this; + // use exact match + var rec = me.store.findRecord('realm', realm, 0, false, false, true); + return rec && rec.data && rec.data.tfa ? rec.data.tfa : undefined; + }, + + store: { + model: 'pve-domains', + autoLoad: true + } +}); +/* + * Top left combobox, used to select a view of the underneath RessourceTree + */ +Ext.define('PVE.form.ViewSelector', { + extend: 'Ext.form.field.ComboBox', + alias: ['widget.pveViewSelector'], + + editable: false, + allowBlank: false, + forceSelection: true, + autoSelect: false, + valueField: 'key', + displayField: 'value', + hideLabel: true, + queryMode: 'local', + + initComponent: function() { + var me = this; + + var default_views = { + server: { + text: gettext('Server View'), + groups: ['node'] + }, + folder: { + text: gettext('Folder View'), + groups: ['type'] + }, + storage: { + text: gettext('Storage View'), + groups: ['node'], + filterfn: function(node) { + return node.data.type === 'storage' || node.data.type === 'node'; + } + }, + pool: { + text: gettext('Pool View'), + groups: ['pool'], + // Pool View only lists VMs and Containers + filterfn: function(node) { + return node.data.type === 'qemu' || node.data.type === 'lxc' || node.data.type === 'openvz' || + node.data.type === 'pool'; + } + } + }; + + var groupdef = []; + Ext.Object.each(default_views, function(viewname, value) { + groupdef.push([viewname, value.text]); + }); + + var store = Ext.create('Ext.data.Store', { + model: 'KeyValue', + proxy: { + type: 'memory', + reader: 'array' + }, + data: groupdef, + autoload: true + }); + + Ext.apply(me, { + store: store, + value: groupdef[0][0], + getViewFilter: function() { + var view = me.getValue(); + return Ext.apply({ id: view }, default_views[view] || default_views.server); + }, + + getState: function() { + return { value: me.getValue() }; + }, + + applyState : function(state, doSelect) { + var view = me.getValue(); + if (state && state.value && (view != state.value)) { + var record = store.findRecord('key', state.value); + if (record) { + me.setValue(state.value, true); + if (doSelect) { + me.fireEvent('select', me, [record]); + } + } + } + }, + stateEvents: [ 'select' ], + stateful: true, + stateId: 'pveview', + id: 'view' + }); + + me.callParent(); + + var statechange = function(sp, key, value) { + if (key === me.id) { + me.applyState(value, true); + } + }; + + var sp = Ext.state.Manager.getProvider(); + me.mon(sp, 'statechange', statechange, me); + } +}); +Ext.define('PVE.form.NodeSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveNodeSelector'], + + // invalidate nodes which are offline + onlineValidator: false, + + selectCurNode: false, + + // do not allow those nodes (array) + disallowedNodes: undefined, + + // only allow those nodes (array) + allowedNodes: undefined, + // set default value to empty array, else it inits it with + // null and after the store load it is an empty array, + // triggering dirtychange + value: [], + valueField: 'node', + displayField: 'node', + store: { + fields: [ 'node', 'cpu', 'maxcpu', 'mem', 'maxmem', 'uptime' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes' + }, + sorters: [ + { + property : 'node', + direction: 'ASC' + }, + { + property : 'mem', + direction: 'DESC' + } + ] + }, + + listConfig: { + columns: [ + { + header: gettext('Node'), + dataIndex: 'node', + sortable: true, + hideable: false, + flex: 1 + }, + { + header: gettext('Memory usage') + " %", + renderer: PVE.Utils.render_mem_usage_percent, + sortable: true, + width: 100, + dataIndex: 'mem' + }, + { + header: gettext('CPU usage'), + renderer: PVE.Utils.render_cpu, + sortable: true, + width: 100, + dataIndex: 'cpu' + } + ] + }, + + validator: function(value) { + /*jslint confusion: true */ + var me = this; + if (!me.onlineValidator || (me.allowBlank && !value)) { + return true; + } + + var offline = []; + var notAllowed = []; + + Ext.Array.each(value.split(/\s*,\s*/), function(node) { + var rec = me.store.findRecord(me.valueField, node); + if (!(rec && rec.data) || rec.data.status !== 'online') { + offline.push(node); + } else if (me.allowedNodes && !Ext.Array.contains(me.allowedNodes, node)) { + notAllowed.push(node); + } + }); + + if (value && notAllowed.length !== 0) { + return "Node " + notAllowed.join(', ') + " is not allowed for this action!"; + } + + if (value && offline.length !== 0) { + return "Node " + offline.join(', ') + " seems to be offline!"; + } + return true; + }, + + initComponent: function() { + var me = this; + + if (me.selectCurNode && PVE.curSelectedNode && PVE.curSelectedNode.data.node) { + me.preferredValue = PVE.curSelectedNode.data.node; + } + + me.callParent(); + me.getStore().load(); + + // filter out disallowed nodes + me.getStore().addFilter(new Ext.util.Filter({ + filterFn: function(item) { + if (Ext.isArray(me.disallowedNodes)) { + return !Ext.Array.contains(me.disallowedNodes, item.data.node); + } else { + return true; + } + } + })); + + me.mon(me.getStore(), 'load', function(){ + me.isValid(); + }); + } +}); +Ext.define('PVE.form.FileSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveFileSelector', + + editable: true, + anyMatch: true, + forceSelection: true, + + listeners: { + afterrender: function() { + var me = this; + if (!me.disabled) { + me.setStorage(me.storage, me.nodename); + } + } + }, + + setStorage: function(storage, nodename) { + var me = this; + + var change = false; + if (storage && (me.storage !== storage)) { + me.storage = storage; + change = true; + } + + if (nodename && (me.nodename !== nodename)) { + me.nodename = nodename; + change = true; + } + + if (!(me.storage && me.nodename && change)) { + return; + } + + var url = '/api2/json/nodes/' + me.nodename + '/storage/' + me.storage + '/content'; + if (me.storageContent) { + url += '?content=' + me.storageContent; + } + + me.store.setProxy({ + type: 'proxmox', + url: url + }); + + me.store.removeAll(); + me.store.load(); + }, + + setNodename: function(nodename) { + this.setStorage(undefined, nodename); + }, + + store: { + model: 'pve-storage-content' + }, + + allowBlank: false, + autoSelect: false, + valueField: 'volid', + displayField: 'text', + + listConfig: { + width: 600, + columns: [ + { + header: gettext('Name'), + dataIndex: 'text', + hideable: false, + flex: 1 + }, + { + header: gettext('Format'), + width: 60, + dataIndex: 'format' + }, + { + header: gettext('Size'), + width: 100, + dataIndex: 'size', + renderer: Proxmox.Utils.format_size + } + ] + } +}); +Ext.define('PVE.form.StorageSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveStorageSelector', + + allowBlank: false, + valueField: 'storage', + displayField: 'storage', + listConfig: { + width: 450, + columns: [ + { + header: gettext('Name'), + dataIndex: 'storage', + hideable: false, + flex: 1 + }, + { + header: gettext('Type'), + width: 75, + dataIndex: 'type' + }, + { + header: gettext('Avail'), + width: 90, + dataIndex: 'avail', + renderer: Proxmox.Utils.format_size + }, + { + header: gettext('Capacity'), + width: 90, + dataIndex: 'total', + renderer: Proxmox.Utils.format_size + } + ] + }, + + reloadStorageList: function() { + var me = this; + if (!me.nodename) { + return; + } + + var params = { + format: 1 + }; + var url = '/api2/json/nodes/' + me.nodename + '/storage'; + if (me.storageContent) { + params.content = me.storageContent; + } + if (me.targetNode) { + params.target = me.targetNode; + params.enabled = 1; // skip disabled storages + } + me.store.setProxy({ + type: 'proxmox', + url: url, + extraParams: params + }); + + me.store.load(); + + }, + + setTargetNode: function(targetNode) { + var me = this; + + if (!targetNode || (me.targetNode === targetNode)) { + return; + } + + me.targetNode = targetNode; + + me.reloadStorageList(); + }, + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + + me.reloadStorageList(); + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + me.nodename = undefined; + + var store = Ext.create('Ext.data.Store', { + model: 'pve-storage-status', + sorters: { + property: 'storage', + order: 'DESC' + } + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + if (nodename) { + me.setNodename(nodename); + } + } +}, function() { + + Ext.define('pve-storage-status', { + extend: 'Ext.data.Model', + fields: [ 'storage', 'active', 'type', 'avail', 'total' ], + idProperty: 'storage' + }); + +}); +Ext.define('PVE.form.DiskStorageSelector', { + extend: 'Ext.container.Container', + alias: 'widget.pveDiskStorageSelector', + + layout: 'fit', + defaults: { + margin: '0 0 5 0' + }, + + // the fieldLabel for the storageselector + storageLabel: gettext('Storage'), + + // the content to show (e.g., images or rootdir) + storageContent: undefined, + + // if true, selects the first available storage + autoSelect: false, + + allowBlank: false, + emptyText: '', + + // hides the selection field + // this is always hidden on creation, + // and only shown when the storage needs a selection and + // hideSelection is not true + hideSelection: undefined, + + // hides the size field (e.g, for the efi disk dialog) + hideSize: false, + + // sets the initial size value + // string because else we get a type confusion + defaultSize: '32', + + changeStorage: function(f, value) { + var me = this; + var formatsel = me.getComponent('diskformat'); + var hdfilesel = me.getComponent('hdimage'); + var hdsizesel = me.getComponent('disksize'); + + // initial store load, and reset/deletion of the storage + if (!value) { + hdfilesel.setDisabled(true); + hdfilesel.setVisible(false); + + formatsel.setDisabled(true); + return; + } + + var rec = f.store.getById(value); + // if the storage is not defined, or valid, + // we cannot know what to enable/disable + if (!rec) { + return; + } + + var selectformat = false; + if (rec.data.format) { + var format = rec.data.format[0]; // 0 is the formats, 1 the default in the backend + delete format.subvol; // we never need subvol in the gui + selectformat = (Ext.Object.getSize(format) > 1); + } + + var select = !!rec.data.select_existing && !me.hideSelection; + + formatsel.setDisabled(!selectformat); + formatsel.setValue(selectformat ? 'qcow2' : 'raw'); + + hdfilesel.setDisabled(!select); + hdfilesel.setVisible(select); + if (select) { + hdfilesel.setStorage(value); + } + + hdsizesel.setDisabled(select || me.hideSize); + hdsizesel.setVisible(!select && !me.hideSize); + }, + + setNodename: function(nodename) { + var me = this; + var hdstorage = me.getComponent('hdstorage'); + var hdfilesel = me.getComponent('hdimage'); + + hdstorage.setNodename(nodename); + hdfilesel.setNodename(nodename); + }, + + setDisabled: function(value) { + var me = this; + var hdstorage = me.getComponent('hdstorage'); + + // reset on disable + if (value) { + hdstorage.setValue(); + } + hdstorage.setDisabled(value); + + // disabling does not always fire this event and we do not need + // the value of the validity + hdstorage.fireEvent('validitychange'); + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: 'pveStorageSelector', + itemId: 'hdstorage', + name: 'hdstorage', + reference: 'hdstorage', + fieldLabel: me.storageLabel, + nodename: me.nodename, + storageContent: me.storageContent, + disabled: me.disabled, + autoSelect: me.autoSelect, + allowBlank: me.allowBlank, + emptyText: me.emptyText, + listeners: { + change: { + fn: me.changeStorage, + scope: me + } + } + }, + { + xtype: 'pveFileSelector', + name: 'hdimage', + reference: 'hdimage', + itemId: 'hdimage', + fieldLabel: gettext('Disk image'), + nodename: me.nodename, + disabled: true, + hidden: true + }, + { + xtype: 'numberfield', + itemId: 'disksize', + reference: 'disksize', + name: 'disksize', + fieldLabel: gettext('Disk size') + ' (GiB)', + hidden: me.hideSize, + disabled: me.hideSize, + minValue: 0.001, + maxValue: 128*1024, + decimalPrecision: 3, + value: me.defaultSize, + allowBlank: false + }, + { + xtype: 'pveDiskFormatSelector', + itemId: 'diskformat', + reference: 'diskformat', + name: 'diskformat', + fieldLabel: gettext('Format'), + nodename: me.nodename, + disabled: true, + hidden: me.storageContent === 'rootdir', + value: 'qcow2', + allowBlank: false + } + ]; + + // use it to disable the children but not ourself + me.disabled = false; + + me.callParent(); + } +}); +Ext.define('PVE.form.BridgeSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.PVE.form.BridgeSelector'], + + bridgeType: 'any_bridge', // bridge, OVSBridge or any_bridge + + store: { + fields: [ 'iface', 'active', 'type' ], + filterOnLoad: true, + sorters: [ + { + property : 'iface', + direction: 'ASC' + } + ] + }, + valueField: 'iface', + displayField: 'iface', + listConfig: { + columns: [ + { + header: gettext('Bridge'), + dataIndex: 'iface', + hideable: false, + width: 100 + }, + { + header: gettext('Active'), + width: 60, + dataIndex: 'active', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('Comment'), + dataIndex: 'comments', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ] + }, + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/network?type=' + + me.bridgeType + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + me.nodename = undefined; + + me.callParent(); + + me.setNodename(nodename); + } +}); + +Ext.define('PVE.form.PCISelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pvePCISelector', + + store: { + fields: [ 'id','vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev' ], + filterOnLoad: true, + sorters: [ + { + property : 'id', + direction: 'ASC' + } + ] + }, + + autoSelect: false, + valueField: 'id', + displayField: 'id', + + // can contain a load callback for the store + // useful to determine the state of the IOMMU + onLoadCallBack: undefined, + + listConfig: { + width: 800, + columns: [ + { + header: 'ID', + dataIndex: 'id', + width: 80 + }, + { + header: gettext('IOMMU Group'), + dataIndex: 'iommugroup', + width: 50 + }, + { + header: gettext('Vendor'), + dataIndex: 'vendor_name', + flex: 2 + }, + { + header: gettext('Device'), + dataIndex: 'device_name', + flex: 6 + }, + { + header: gettext('Mediated Devices'), + dataIndex: 'mdev', + flex: 1, + renderer: function(val) { + return Proxmox.Utils.format_boolean(!!val); + } + } + ] + }, + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/hardware/pci' + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + me.nodename = undefined; + + me.callParent(); + + if (me.onLoadCallBack !== undefined) { + me.mon(me.getStore(), 'load', me.onLoadCallBack); + } + + me.setNodename(nodename); + } +}); + +Ext.define('PVE.form.MDevSelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pveMDevSelector', + + store: { + fields: [ 'type','available', 'description' ], + filterOnLoad: true, + sorters: [ + { + property : 'type', + direction: 'ASC' + } + ] + }, + autoSelect: false, + valueField: 'type', + displayField: 'type', + listConfig: { + columns: [ + { + header: gettext('Type'), + dataIndex: 'type', + flex: 1 + }, + { + header: gettext('Available'), + dataIndex: 'available', + width: 80 + }, + { + header: gettext('Description'), + dataIndex: 'description', + flex: 1, + renderer: function(value) { + if (!value) { + return ''; + } + + return value.split('\n').join('
'); + } + } + ] + }, + + setPciID: function(pciid, force) { + var me = this; + + if (!force && (!pciid || (me.pciid === pciid))) { + return; + } + + me.pciid = pciid; + me.updateProxy(); + }, + + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + me.updateProxy(); + }, + + updateProxy: function() { + var me = this; + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/hardware/pci/' + me.pciid + '/mdev' + }); + me.store.load(); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw 'no node name specified'; + } + + me.callParent(); + + if (me.pciid) { + me.setPciID(me.pciid, true); + } + } +}); + +Ext.define('PVE.form.SecurityGroupsSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveSecurityGroupsSelector'], + + valueField: 'group', + displayField: 'group', + initComponent: function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: [ 'group', 'comment' ], + idProperty: 'group', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/firewall/groups" + }, + sorters: { + property: 'group', + order: 'DESC' + } + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: gettext('Security Group'), + dataIndex: 'group', + hideable: false, + width: 100 + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ] + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.form.IPRefSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveIPRefSelector'], + + base_url: undefined, + + preferredValue: '', // hack: else Form sets dirty flag? + + ref_type: undefined, // undefined = any [undefined, 'ipset' or 'alias'] + + valueField: 'ref', + displayField: 'ref', + + initComponent: function() { + var me = this; + + if (!me.base_url) { + throw "no base_url specified"; + } + + var url = "/api2/json" + me.base_url; + if (me.ref_type) { + url += "?type=" + me.ref_type; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: [ 'type', 'name', 'ref', 'comment' ], + idProperty: 'ref', + proxy: { + type: 'proxmox', + url: url + }, + sorters: { + property: 'ref', + order: 'DESC' + } + }); + + var disable_query_for_ips = function(f, value) { + if (value === null || + value.match(/^\d/)) { // IP address starts with \d + f.queryDelay = 9999999999; // hack: disable with long delay + } else { + f.queryDelay = 10; + } + }; + + var columns = []; + + if (!me.ref_type) { + columns.push({ + header: gettext('Type'), + dataIndex: 'type', + hideable: false, + width: 60 + }); + } + + columns.push( + { + header: gettext('Name'), + dataIndex: 'ref', + hideable: false, + width: 140 + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ); + + Ext.apply(me, { + store: store, + listConfig: { columns: columns } + }); + + me.on('change', disable_query_for_ips); + + me.callParent(); + } +}); + +Ext.define('PVE.form.IPProtocolSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveIPProtocolSelector'], + valueField: 'p', + displayField: 'p', + listConfig: { + columns: [ + { + header: gettext('Protocol'), + dataIndex: 'p', + hideable: false, + sortable: false, + width: 100 + }, + { + header: gettext('Number'), + dataIndex: 'n', + hideable: false, + sortable: false, + width: 50 + }, + { + header: gettext('Description'), + dataIndex: 'd', + hideable: false, + sortable: false, + flex: 1 + } + ] + }, + store: { + fields: [ 'p', 'd', 'n'], + data: [ + { p: 'tcp', n: 6, d: 'Transmission Control Protocol' }, + { p: 'udp', n: 17, d: 'User Datagram Protocol' }, + { p: 'icmp', n: 1, d: 'Internet Control Message Protocol' }, + { p: 'igmp', n: 2, d: 'Internet Group Management' }, + { p: 'ggp', n: 3, d: 'gateway-gateway protocol' }, + { p: 'ipencap', n: 4, d: 'IP encapsulated in IP' }, + { p: 'st', n: 5, d: 'ST datagram mode' }, + { p: 'egp', n: 8, d: 'exterior gateway protocol' }, + { p: 'igp', n: 9, d: 'any private interior gateway (Cisco)' }, + { p: 'pup', n: 12, d: 'PARC universal packet protocol' }, + { p: 'hmp', n: 20, d: 'host monitoring protocol' }, + { p: 'xns-idp', n: 22, d: 'Xerox NS IDP' }, + { p: 'rdp', n: 27, d: '"reliable datagram" protocol' }, + { p: 'iso-tp4', n: 29, d: 'ISO Transport Protocol class 4 [RFC905]' }, + { p: 'dccp', n: 33, d: 'Datagram Congestion Control Prot. [RFC4340]' }, + { p: 'xtp', n: 36, d: 'Xpress Transfer Protocol' }, + { p: 'ddp', n: 37, d: 'Datagram Delivery Protocol' }, + { p: 'idpr-cmtp', n: 38, d: 'IDPR Control Message Transport' }, + { p: 'ipv6', n: 41, d: 'Internet Protocol, version 6' }, + { p: 'ipv6-route', n: 43, d: 'Routing Header for IPv6' }, + { p: 'ipv6-frag', n: 44, d: 'Fragment Header for IPv6' }, + { p: 'idrp', n: 45, d: 'Inter-Domain Routing Protocol' }, + { p: 'rsvp', n: 46, d: 'Reservation Protocol' }, + { p: 'gre', n: 47, d: 'General Routing Encapsulation' }, + { p: 'esp', n: 50, d: 'Encap Security Payload [RFC2406]' }, + { p: 'ah', n: 51, d: 'Authentication Header [RFC2402]' }, + { p: 'skip', n: 57, d: 'SKIP' }, + { p: 'ipv6-icmp', n: 58, d: 'ICMP for IPv6' }, + { p: 'ipv6-nonxt', n: 59, d: 'No Next Header for IPv6' }, + { p: 'ipv6-opts', n: 60, d: 'Destination Options for IPv6' }, + { p: 'vmtp', n: 81, d: 'Versatile Message Transport' }, + { p: 'eigrp', n: 88, d: 'Enhanced Interior Routing Protocol (Cisco)' }, + { p: 'ospf', n: 89, d: 'Open Shortest Path First IGP' }, + { p: 'ax.25', n: 93, d: 'AX.25 frames' }, + { p: 'ipip', n: 94, d: 'IP-within-IP Encapsulation Protocol' }, + { p: 'etherip', n: 97, d: 'Ethernet-within-IP Encapsulation [RFC3378]' }, + { p: 'encap', n: 98, d: 'Yet Another IP encapsulation [RFC1241]' }, + { p: 'pim', n: 103, d: 'Protocol Independent Multicast' }, + { p: 'ipcomp', n: 108, d: 'IP Payload Compression Protocol' }, + { p: 'vrrp', n: 112, d: 'Virtual Router Redundancy Protocol [RFC5798]' }, + { p: 'l2tp', n: 115, d: 'Layer Two Tunneling Protocol [RFC2661]' }, + { p: 'isis', n: 124, d: 'IS-IS over IPv4' }, + { p: 'sctp', n: 132, d: 'Stream Control Transmission Protocol' }, + { p: 'fc', n: 133, d: 'Fibre Channel' }, + { p: 'mobility-header', n: 135, d: 'Mobility Support for IPv6 [RFC3775]' }, + { p: 'udplite', n: 136, d: 'UDP-Lite [RFC3828]' }, + { p: 'mpls-in-ip', n: 137, d: 'MPLS-in-IP [RFC4023]' }, + { p: 'hip', n: 139, d: 'Host Identity Protocol' }, + { p: 'shim6', n: 140, d: 'Shim6 Protocol [RFC5533]' }, + { p: 'wesp', n: 141, d: 'Wrapped Encapsulating Security Payload' }, + { p: 'rohc', n: 142, d: 'Robust Header Compression' } + ] + } +}); +Ext.define('PVE.form.CPUModelSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.CPUModelSelector'], + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (kvm64)'], + ['486', '486'], + ['athlon', 'athlon'], + ['core2duo', 'core2duo'], + ['coreduo', 'coreduo'], + ['kvm32', 'kvm32'], + ['kvm64', 'kvm64'], + ['pentium', 'pentium'], + ['pentium2', 'pentium2'], + ['pentium3', 'pentium3'], + ['phenom', 'phenom'], + ['qemu32', 'qemu32'], + ['qemu64', 'qemu64'], + ['Conroe', 'Conroe'], + ['Penryn', 'Penryn'], + ['Nehalem', 'Nehalem'], + ['Westmere', 'Westmere'], + ['SandyBridge', 'SandyBridge'], + ['IvyBridge', 'IvyBridge'], + ['Haswell', 'Haswell'], + ['Haswell-noTSX','Haswell-noTSX'], + ['Broadwell', 'Broadwell'], + ['Broadwell-noTSX','Broadwell-noTSX'], + ['Skylake-Client','Skylake-Client'], + ['Opteron_G1', 'Opteron_G1'], + ['Opteron_G2', 'Opteron_G2'], + ['Opteron_G3', 'Opteron_G3'], + ['Opteron_G4', 'Opteron_G4'], + ['Opteron_G5', 'Opteron_G5'], + ['EPYC', 'EPYC'], + ['host', 'host'] + + ] +}); +Ext.define('PVE.form.VNCKeyboardSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.VNCKeyboardSelector'], + comboItems: PVE.Utils.kvm_keymap_array() +}); +Ext.define('PVE.form.CacheTypeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.CacheTypeSelector'], + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + " (" + gettext('No cache') + ")"], + ['directsync', 'Direct sync'], + ['writethrough', 'Write through'], + ['writeback', 'Write back'], + ['unsafe', 'Write back (' + gettext('unsafe') + ')'], + ['none', gettext('No cache')] + ] +}); +Ext.define('PVE.form.SnapshotSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.PVE.form.SnapshotSelector'], + + valueField: 'name', + displayField: 'name', + + loadStore: function(nodename, vmid) { + var me = this; + + if (!nodename) { + return; + } + + me.nodename = nodename; + + if (!vmid) { + return; + } + + me.vmid = vmid; + + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid +'/snapshot' + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.guestType) { + throw "no guest type specified"; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'name'], + filterOnLoad: true + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: gettext('Snapshot'), + dataIndex: 'name', + hideable: false, + flex: 1 + } + ] + } + }); + + me.callParent(); + + me.loadStore(me.nodename, me.vmid); + } +}); +Ext.define('PVE.form.ContentTypeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveContentTypeSelector'], + + cts: undefined, + + initComponent: function() { + var me = this; + + me.comboItems = []; + + if (me.cts === undefined) { + me.cts = ['images', 'iso', 'vztmpl', 'backup', 'rootdir', 'snippets']; + } + + Ext.Array.each(me.cts, function(ct) { + me.comboItems.push([ct, PVE.Utils.format_content_types(ct)]); + }); + + me.callParent(); + } +}); +Ext.define('PVE.form.HotplugFeatureSelector', { + extend: 'Ext.form.CheckboxGroup', + alias: 'widget.pveHotplugFeatureSelector', + + columns: 1, + vertical: true, + + defaults: { + name: 'hotplug', + submitValue: false + }, + items: [ + { + boxLabel: gettext('Disk'), + inputValue: 'disk', + checked: true + }, + { + boxLabel: gettext('Network'), + inputValue: 'network', + checked: true + }, + { + boxLabel: 'USB', + inputValue: 'usb', + checked: true + }, + { + boxLabel: gettext('Memory'), + inputValue: 'memory' + }, + { + boxLabel: gettext('CPU'), + inputValue: 'cpu' + } + ], + + setValue: function(value) { + var me = this; + var newVal = []; + if (value === '1') { + newVal = ['disk', 'network', 'usb']; + } else if (value !== '0') { + newVal = value.split(','); + } + me.callParent([{ hotplug: newVal }]); + }, + + // override framework function to + // assemble the hotplug value + getSubmitData: function() { + var me = this, + boxes = me.getBoxes(), + data = []; + Ext.Array.forEach(boxes, function(box){ + if (box.getValue()) { + data.push(box.inputValue); + } + }); + + /* because above is hotplug an array */ + /*jslint confusion: true*/ + if (data.length === 0) { + return { 'hotplug':'0' }; + } else { + return { 'hotplug': data.join(',') }; + } + } + +}); +Ext.define('PVE.form.AgentFeatureSelector', { + extend: 'Proxmox.panel.InputPanel', + alias: ['widget.pveAgentFeatureSelector'], + + initComponent: function() { + var me = this; + me.items= [ + { + xtype: 'proxmoxcheckbox', + boxLabel: gettext('Qemu Agent'), + name: 'enabled', + uncheckedValue: 0, + listeners: { + change: function(f, value, old) { + var gtcb = me.down('proxmoxcheckbox[name=fstrim_cloned_disks]'); + if (value) { + gtcb.setDisabled(false); + } else { + gtcb.setDisabled(true); + } + } + } + }, + { + xtype: 'proxmoxcheckbox', + boxLabel: gettext('Run guest-trim after clone disk'), + name: 'fstrim_cloned_disks', + disabled: true + } + ]; + me.callParent(); + }, + + onGetValues: function(values) { + var agentstr = PVE.Parser.printPropertyString(values, 'enabled'); + return { agent: agentstr }; + }, + + setValues: function(values) { + var agent = values.agent || ''; + var res = PVE.Parser.parsePropertyString(agent, 'enabled'); + this.callParent([res]); + } +}); +Ext.define('PVE.form.iScsiProviderSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveiScsiProviderSelector'], + comboItems: [ + ['comstar', 'Comstar'], + [ 'istgt', 'istgt'], + [ 'iet', 'IET'], + [ 'LIO', 'LIO'] + ] +}); +Ext.define('PVE.form.DayOfWeekSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveDayOfWeekSelector'], + comboItems:[], + initComponent: function(){ + var me = this; + me.comboItems = [ + ['mon', Ext.util.Format.htmlDecode(Ext.Date.dayNames[1])], + ['tue', Ext.util.Format.htmlDecode(Ext.Date.dayNames[2])], + ['wed', Ext.util.Format.htmlDecode(Ext.Date.dayNames[3])], + ['thu', Ext.util.Format.htmlDecode(Ext.Date.dayNames[4])], + ['fri', Ext.util.Format.htmlDecode(Ext.Date.dayNames[5])], + ['sat', Ext.util.Format.htmlDecode(Ext.Date.dayNames[6])], + ['sun', Ext.util.Format.htmlDecode(Ext.Date.dayNames[0])] + ]; + this.callParent(); + } +}); +Ext.define('PVE.form.BackupModeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveBackupModeSelector'], + comboItems: [ + ['snapshot', gettext('Snapshot')], + ['suspend', gettext('Suspend')], + ['stop', gettext('Stop')] + ] +}); +Ext.define('PVE.form.ScsiHwSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveScsiHwSelector'], + comboItems: [ + ['__default__', PVE.Utils.render_scsihw('')], + ['lsi', PVE.Utils.render_scsihw('lsi')], + ['lsi53c810', PVE.Utils.render_scsihw('lsi53c810')], + ['megasas', PVE.Utils.render_scsihw('megasas')], + ['virtio-scsi-pci', PVE.Utils.render_scsihw('virtio-scsi-pci')], + ['virtio-scsi-single', PVE.Utils.render_scsihw('virtio-scsi-single')], + ['pvscsi', PVE.Utils.render_scsihw('pvscsi')] + ] +}); +Ext.define('PVE.form.FirewallPolicySelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveFirewallPolicySelector'], + comboItems: [ + ['ACCEPT', 'ACCEPT'], + ['REJECT', 'REJECT'], + [ 'DROP', 'DROP'] + ] +}); +/* + * This is a global search field + * it loads the /cluster/resources on focus + * and displays the result in a floating grid + * + * it filters and sorts the objects by the algorithm in + * the customFilter function + * + * also it does accept key up/down and enter for input + * and it opens to ctrl+shift+f and ctrl+space + */ +Ext.define('PVE.form.GlobalSearchField', { + extend: 'Ext.form.field.Text', + alias: 'widget.pveGlobalSearchField', + + emptyText: gettext('Search'), + enableKeyEvents: true, + selectOnFocus: true, + padding: '0 5 0 5', + + grid: { + xtype: 'gridpanel', + focusOnToFront: false, + floating: true, + emptyText: Proxmox.Utils.noneText, + width: 600, + height: 400, + scrollable: { + xtype: 'scroller', + y: true, + x:false + }, + store: { + model: 'PVEResources', + proxy:{ + type: 'proxmox', + url: '/api2/extjs/cluster/resources' + } + }, + plugins: { + ptype: 'bufferedrenderer', + trailingBufferZone: 20, + leadingBufferZone: 20 + }, + + hideMe: function() { + var me = this; + if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) { + return; + } + me.hasFocus = false; + if (!me.textfield.hasFocus) { + me.hide(); + } + }, + + setFocus: function() { + var me = this; + me.hasFocus = true; + }, + + listeners: { + rowclick: function(grid, record) { + var me = this; + me.textfield.selectAndHide(record.id); + }, + itemcontextmenu: function(v, record, item, index, event) { + var me = this; + me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event); + }, + /* because of lint */ + focusleave: { + fn: 'hideMe' + }, + focusenter: 'setFocus' + }, + + columns: [ + { + text: gettext('Type'), + dataIndex: 'type', + width: 100, + renderer: PVE.Utils.render_resource_type + }, + { + text: gettext('Description'), + flex: 1, + dataIndex: 'text' + }, + { + text: gettext('Node'), + dataIndex: 'node' + }, + { + text: gettext('Pool'), + dataIndex: 'pool' + } + ] + }, + + customFilter: function(item) { + var me = this; + var match = 0; + var fieldArr = []; + var i,j, fields; + + // different types of objects have different fields to search + // for example, a node will never have a pool and vice versa + switch (item.data.type) { + case 'pool': fieldArr = ['type', 'pool', 'text']; break; + case 'node': fieldArr = ['type', 'node', 'text']; break; + case 'storage': fieldArr = ['type', 'pool', 'node', 'storage']; break; + default: fieldArr = ['name', 'type', 'node', 'pool', 'vmid']; + } + if (me.filterVal === '') { + item.data.relevance = 0; + return true; + } + + // all text is case insensitive and each word is + // searched alone + // for every partial match, the row gets + // 1 match point, for every exact match + // it gets 2 points + // + // results gets sorted by points (descending) + fields = me.filterVal.split(/\s+/); + for(i = 0; i < fieldArr.length; i++) { + var v = item.data[fieldArr[i]]; + if (v !== undefined) { + v = v.toString().toLowerCase(); + for(j = 0; j < fields.length; j++) { + if (v.indexOf(fields[j]) !== -1) { + match++; + if(v === fields[j]) { + match++; + } + } + } + } + } + // give the row the 'relevance' value + item.data.relevance = match; + return (match > 0); + }, + + updateFilter: function(field, newValue, oldValue) { + var me = this; + // parse input and filter store, + // show grid + me.grid.store.filterVal = newValue.toLowerCase().trim(); + me.grid.store.clearFilter(true); + me.grid.store.filterBy(me.customFilter); + me.grid.getSelectionModel().select(0); + }, + + selectAndHide: function(id) { + var me = this; + me.tree.selectById(id); + me.grid.hide(); + me.setValue(''); + me.blur(); + }, + + onKey: function(field, e) { + var me = this; + var key = e.getKey(); + + switch(key) { + case Ext.event.Event.ENTER: + // go to first entry if there is one + if (me.grid.store.getCount() > 0) { + me.selectAndHide(me.grid.getSelection()[0].data.id); + } + break; + case Ext.event.Event.UP: + me.grid.getSelectionModel().selectPrevious(); + break; + case Ext.event.Event.DOWN: + me.grid.getSelectionModel().selectNext(); + break; + case Ext.event.Event.ESC: + me.grid.hide(); + me.blur(); + break; + } + }, + + loadValues: function(field) { + var me = this; + var records = []; + + me.hasFocus = true; + me.grid.textfield = me; + me.grid.store.load(); + me.grid.showBy(me, 'tl-bl'); + }, + + hideGrid: function() { + var me = this; + + me.hasFocus = false; + if (!me.grid.hasFocus) { + me.grid.hide(); + } + }, + + listeners: { + change: { + fn: 'updateFilter', + buffer: 250 + }, + specialkey: 'onKey', + focusenter: 'loadValues', + focusleave: { + fn: 'hideGrid', + delay: 100 + } + }, + + toggleFocus: function() { + var me = this; + if (!me.hasFocus) { + me.focus(); + } else { + me.blur(); + } + }, + + initComponent: function() { + var me = this; + + if (!me.tree) { + throw "no tree given"; + } + + me.grid = Ext.create(me.grid); + + me.callParent(); + + /*jslint confusion: true*/ + /*because shift is also a function*/ + // bind ctrl+shift+f and ctrl+space + // to open/close the search + me.keymap = new Ext.KeyMap({ + target: Ext.get(document), + binding: [{ + key:'F', + ctrl: true, + shift: true, + fn: me.toggleFocus, + scope: me + },{ + key:' ', + ctrl: true, + fn: me.toggleFocus, + scope: me + }] + }); + + // always select first item and + // sort by relevance after load + me.mon(me.grid.store, 'load', function() { + me.grid.getSelectionModel().select(0); + me.grid.store.sort({ + property: 'relevance', + direction: 'DESC' + }); + }); + } + +}); +Ext.define('PVE.form.QemuBiosSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveQemuBiosSelector'], + + initComponent: function() { + var me = this; + + me.comboItems = [ + ['__default__', PVE.Utils.render_qemu_bios('')], + ['seabios', PVE.Utils.render_qemu_bios('seabios')], + ['ovmf', PVE.Utils.render_qemu_bios('ovmf')] + ]; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +/* filter is a javascript builtin, but extjs calls it also filter */ +Ext.define('PVE.form.VMSelector', { + extend: 'Ext.grid.Panel', + alias: 'widget.vmselector', + + mixins: { + field: 'Ext.form.field.Field' + }, + + allowBlank: true, + selectAll: false, + isFormField: true, + + plugins: 'gridfilters', + + store: { + model: 'PVEResources', + autoLoad: true, + sorters: 'vmid', + filters: [{ + property: 'type', + value: /lxc|qemu/ + }] + }, + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 80, + filter: { + type: 'number' + } + }, + { + header: gettext('Node'), + dataIndex: 'node' + }, + { + header: gettext('Status'), + dataIndex: 'status', + filter: { + type: 'list' + } + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1, + filter: { + type: 'string' + } + }, + { + header: gettext('Pool'), + dataIndex: 'pool', + filter: { + type: 'list' + } + }, + { + header: gettext('Type'), + dataIndex: 'type', + width: 120, + renderer: function(value) { + if (value === 'qemu') { + return gettext('Virtual Machine'); + } else if (value === 'lxc') { + return gettext('LXC Container'); + } + + return ''; + }, + filter: { + type: 'list', + store: { + data: [ + {id: 'qemu', text: gettext('Virtual Machine')}, + {id: 'lxc', text: gettext('LXC Container')} + ], + // due to EXTJS-18711 + // we have to do a static list via a store + // but to avoid creating an object, + // we have to have a pseudo un function + un: function(){} + } + } + }, + { + header: 'HA ' + gettext('Status'), + dataIndex: 'hastate', + flex: 1, + filter: { + type: 'list' + } + } + ], + + selModel: { + selType: 'checkboxmodel', + mode: 'SIMPLE' + }, + + checkChangeEvents: [ + 'selectionchange', + 'change' + ], + + listeners: { + selectionchange: function() { + // to trigger validity and error checks + this.checkChange(); + } + }, + + getValue: function() { + var me = this; + var sm = me.getSelectionModel(); + var selection = sm.getSelection(); + var values = []; + var store = me.getStore(); + selection.forEach(function(item) { + // only add if not filtered + if (store.findExact('vmid', item.data.vmid) !== -1) { + values.push(item.data.vmid); + } + }); + return values; + }, + + setValue: function(value) { + console.log(value); + var me = this; + var sm = me.getSelectionModel(); + if (!Ext.isArray(value)) { + value = value.split(','); + } + var selection = []; + var store = me.getStore(); + + value.forEach(function(item) { + var rec = store.findRecord('vmid',item, 0, false, true, true); + console.log(store); + + if (rec) { + console.log(rec); + selection.push(rec); + } + }); + + sm.select(selection); + + return me.mixins.field.setValue.call(me, value); + }, + + getErrors: function(value) { + var me = this; + if (me.allowBlank === false && + me.getSelectionModel().getCount() === 0) { + me.addBodyCls(['x-form-trigger-wrap-default','x-form-trigger-wrap-invalid']); + return [gettext('No VM selected')]; + } + + me.removeBodyCls(['x-form-trigger-wrap-default','x-form-trigger-wrap-invalid']); + return []; + }, + + initComponent: function() { + var me = this; + + me.callParent(); + + if (me.nodename) { + me.store.filters.add({ + property: 'node', + exactMatch: true, + value: me.nodename + }); + } + + // only show the relevant guests by default + if (me.action) { + var statusfilter = ''; + switch (me.action) { + case 'startall': + statusfilter = 'stopped'; + break; + case 'stopall': + statusfilter = 'running'; + break; + } + if (statusfilter !== '') { + me.store.filters.add({ + property: 'template', + value: 0 + },{ + id: 'x-gridfilter-status', + operator: 'in', + property: 'status', + value: [statusfilter] + }); + } + } + + var store = me.getStore(); + var sm = me.getSelectionModel(); + + if (me.selectAll) { + me.mon(store,'load', function(){ + me.getSelectionModel().selectAll(false); + }); + } + } +}); + + +Ext.define('PVE.form.VMComboSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.vmComboSelector', + + valueField: 'vmid', + displayField: 'vmid', + + autoSelect: false, + editable: true, + anyMatch: true, + forceSelection: true, + + store: { + model: 'PVEResources', + autoLoad: true, + sorters: 'vmid', + filters: [{ + property: 'type', + value: /lxc|qemu/ + }] + }, + + listConfig: { + width: 600, + plugins: 'gridfilters', + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 80, + filter: { + type: 'number' + } + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1, + filter: { + type: 'string' + } + }, + { + header: gettext('Node'), + dataIndex: 'node' + }, + { + header: gettext('Status'), + dataIndex: 'status', + filter: { + type: 'list' + } + }, + { + header: gettext('Pool'), + dataIndex: 'pool', + hidden: true, + filter: { + type: 'list' + } + }, + { + header: gettext('Type'), + dataIndex: 'type', + width: 120, + renderer: function(value) { + if (value === 'qemu') { + return gettext('Virtual Machine'); + } else if (value === 'lxc') { + return gettext('LXC Container'); + } + + return ''; + }, + filter: { + type: 'list', + store: { + data: [ + {id: 'qemu', text: gettext('Virtual Machine')}, + {id: 'lxc', text: gettext('LXC Container')} + ], + un: function(){} // due to EXTJS-18711 + } + } + }, + { + header: 'HA ' + gettext('Status'), + dataIndex: 'hastate', + hidden: true, + flex: 1, + filter: { + type: 'list' + } + } + ] + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.form.VMCPUFlagSelector', { + extend: 'Ext.grid.Panel', + alias: 'widget.vmcpuflagselector', + + mixins: { + field: 'Ext.form.field.Field' + }, + + disableSelection: true, + columnLines: false, + selectable: false, + hideHeaders: true, + + scrollable: 'y', + height: 200, + + unkownFlags: [], + + store: { + type: 'store', + fields: ['flag', { name: 'state', defaultValue: '=' }, 'desc'], + data: [ + // FIXME: let qemu-server host this and autogenerate or get from API call?? + { flag: 'md-clear', desc: 'Required to let the guest OS know if MDS is mitigated correctly' }, + { flag: 'pcid', desc: 'Meltdown fix cost reduction on Westmere, Sandy-, and IvyBridge Intel CPUs' }, + { flag: 'spec-ctrl', desc: 'Allows improved Spectre mitigation with Intel CPUs' }, + { flag: 'ssbd', desc: 'Protection for "Speculative Store Bypass" for Intel models' }, + { flag: 'ibpb', desc: 'Allows improved Spectre mitigation with AMD CPUs' }, + { flag: 'virt-ssbd', desc: 'Basis for "Speculative Store Bypass" protection for AMD models' }, + { flag: 'amd-ssbd', desc: 'Improves Spectre mitigation performance with AMD CPUs, best used with "virt-ssbd"' }, + { flag: 'amd-no-ssb', desc: 'Notifies guest OS that host is not vulnerable for Spectre on AMD CPUs' }, + { flag: 'pdpe1gb', desc: 'Allow guest OS to use 1GB size pages, if host HW supports it' } + ], + listeners: { + update: function() { + this.commitChanges(); + } + } + }, + + getValue: function() { + var me = this; + var store = me.getStore(); + var flags = ''; + + // ExtJS does not has a nice getAllRecords interface for stores :/ + store.queryBy(Ext.returnTrue).each(function(rec) { + var s = rec.get('state'); + if (s && s !== '=') { + var f = rec.get('flag'); + if (flags === '') { + flags = s + f; + } else { + flags += ';' + s + f; + } + } + }); + + flags += me.unkownFlags.join(';'); + + return flags; + }, + + setValue: function(value) { + var me = this; + var store = me.getStore(); + + me.value = value || ''; + + me.unkownFlags = []; + + me.getStore().queryBy(Ext.returnTrue).each(function(rec) { + rec.set('state', '='); + }); + + var flags = value ? value.split(';') : []; + flags.forEach(function(flag) { + var sign = flag.substr(0, 1); + flag = flag.substr(1); + + var rec = store.findRecord('flag', flag); + if (rec !== null) { + rec.set('state', sign); + } else { + me.unkownFlags.push(flag); + } + }); + store.reload(); + + var res = me.mixins.field.setValue.call(me, value); + + return res; + }, + columns: [ + { + dataIndex: 'state', + renderer: function(v) { + switch(v) { + case '=': return 'Default'; + case '-': return 'Off'; + case '+': return 'On'; + default: return 'Unknown'; + } + }, + width: 65 + }, + { + xtype: 'widgetcolumn', + dataIndex: 'state', + width: 95, + onWidgetAttach: function (column, widget, record) { + var val = record.get('state') || '='; + widget.down('[inputValue=' + val + ']').setValue(true); + // TODO: disable if selected CPU model and flag are incompatible + }, + widget: { + xtype: 'radiogroup', + hideLabel: true, + layout: 'hbox', + validateOnChange: false, + value: '=', + listeners: { + change: function(f, value) { + var v = Object.values(value)[0]; + f.getWidgetRecord().set('state', v); + + var view = this.up('grid'); + view.dirty = view.getValue() !== view.originalValue; + view.checkDirty(); + //view.checkChange(); + } + }, + items: [ + { + boxLabel: '-', + boxLabelAlign: 'before', + inputValue: '-' + }, + { + checked: true, + inputValue: '=' + }, + { + boxLabel: '+', + inputValue: '+' + } + ] + } + }, + { + dataIndex: 'flag', + width: 100 + }, + { + dataIndex: 'desc', + cellWrap: true, + flex: 1 + } + ], + + initComponent: function() { + var me = this; + + // static class store, thus gets not recreated, so ensure defaults are set! + me.getStore().data.forEach(function(v) { + v.state = '='; + }); + + me.value = me.originalValue = ''; + + me.callParent(arguments); + } +}); +Ext.define('PVE.form.USBSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveUSBSelector'], + allowBlank: false, + autoSelect: false, + displayField: 'usbid', + valueField: 'usbid', + editable: true, + + getUSBValue: function() { + var me = this; + var rec = me.store.findRecord('usbid', me.value); + var val = 'host='+ me.value; + if (rec && rec.data.speed === "5000") { + val = 'host=' + me.value + ",usb3=1"; + } + return val; + }, + + validator: function(value) { + var me = this; + if (me.type === 'device') { + return (/^[a-f0-9]{4}\:[a-f0-9]{4}$/i).test(value); + } else if (me.type === 'port') { + return (/^[0-9]+\-[0-9]+(\.[0-9]+)*$/).test(value); + } + return false; + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + + if (!nodename) { + throw "no nodename specified"; + } + + if (me.type !== 'device' && me.type !== 'port') { + throw "no valid type specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-usb-' + me.type, + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/scan/usb" + }, + filters: [ + function (item) { + return !!item.data.usbpath && !!item.data.prodid && item.data['class'] != 9; + } + ] + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: (me.type === 'device')?gettext('Device'):gettext('Port'), + sortable: true, + dataIndex: 'usbid', + width: 80 + }, + { + header: gettext('Manufacturer'), + sortable: true, + dataIndex: 'manufacturer', + width: 100 + }, + { + header: gettext('Product'), + sortable: true, + dataIndex: 'product', + flex: 1 + }, + { + header: gettext('Speed'), + width: 70, + sortable: true, + dataIndex: 'speed', + renderer: function(value) { + if (value === "5000") { + return "USB 3.0"; + } else if (value === "480") { + return "USB 2.0"; + } else { + return "USB 1.x"; + } + } + } + ] + } + }); + + me.callParent(); + + store.load(); + } + +}, function() { + + Ext.define('pve-usb-device', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'usbid', + convert: function(val, data) { + if (val) { + return val; + } + return data.get('vendid') + ':' + data.get('prodid'); + } + }, + 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath', + { name: 'port' , type: 'number' }, + { name: 'level' , type: 'number' }, + { name: 'class' , type: 'number' }, + { name: 'devnum' , type: 'number' }, + { name: 'busnum' , type: 'number' } + ] + }); + + Ext.define('pve-usb-port', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'usbid', + convert: function(val,data) { + if (val) { + return val; + } + return data.get('busnum') + '-' + data.get('usbpath'); + } + }, + 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath', + { name: 'port' , type: 'number' }, + { name: 'level' , type: 'number' }, + { name: 'class' , type: 'number' }, + { name: 'devnum' , type: 'number' }, + { name: 'busnum' , type: 'number' } + ] + }); +}); +Ext.define('PVE.form.CalendarEvent', { + extend: 'Ext.form.field.ComboBox', + xtype: 'pveCalendarEvent', + + editable: true, + + valueField: 'value', + displayField: 'text', + queryMode: 'local', + + store: { + field: [ 'value', 'text'], + data: [ + { value: '*/30', text: Ext.String.format(gettext("Every {0} minutes"), 30) }, + { value: '*/2:00', text: gettext("Every two hours")}, + { value: '2,22:30', text: gettext("Every day") + " 02:30, 22:30"}, + { value: 'mon..fri', text: gettext("Monday to Friday") + " 00:00"}, + { value: 'mon..fri */1:00', text: gettext("Monday to Friday") + ': ' + gettext("hourly")}, + { value: 'sun 01:00', text: gettext("Sunday") + " 01:00"} + ] + }, + + tpl: [ + '
    ', + '
  • {text}
  • ', + '
' + ], + + displayTpl: [ + '', + '{value}', + '' + ] + +}); +Ext.define('PVE.form.CephPoolSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCephPoolSelector', + + allowBlank: false, + valueField: 'pool_name', + displayField: 'pool_name', + editable: false, + queryMode: 'local', + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['name'], + sorters: 'name', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/ceph/pools' + } + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + store.load({ + callback: function(rec, op, success){ + if (success && rec.length > 0) { + me.select(rec[0]); + } + } + }); + } + +}); +Ext.define('PVE.form.PermPathSelector', { + extend: 'Ext.form.field.ComboBox', + xtype: 'pvePermPathSelector', + + valueField: 'value', + displayField: 'value', + typeAhead: true, + queryMode: 'local', + store: { + type: 'pvePermPath' + } +}); +/* This class defines the "Tasks" tab of the bottom status panel + * Tasks are jobs with a start, end and log output + */ + +Ext.define('PVE.dc.Tasks', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveClusterTasks'], + + initComponent : function() { + var me = this; + + var taskstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-cluster-tasks', + model: 'proxmox-tasks', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/tasks' + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: taskstore, + sortAfterUpdate: true, + appendAtStart: true, + sorters: [ + { + property : 'pid', + direction: 'DESC' + }, + { + property : 'starttime', + direction: 'DESC' + } + ] + + }); + + var run_task_viewer = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: rec.data.upid + }); + win.show(); + }; + + Ext.apply(me, { + store: store, + stateful: false, + + viewConfig: { + trackOver: false, + stripeRows: true, // does not work with getRowClass() + + getRowClass: function(record, index) { + var status = record.get('status'); + + if (status && status != 'OK') { + return "proxmox-invalid-row"; + } + } + }, + sortableColumns: false, + columns: [ + { + header: gettext("Start Time"), + dataIndex: 'starttime', + width: 150, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("End Time"), + dataIndex: 'endtime', + width: 150, + renderer: function(value, metaData, record) { + if (record.data.pid) { + if (record.data.type == "vncproxy" || + record.data.type == "vncshell" || + record.data.type == "spiceproxy") { + metaData.tdCls = "x-grid-row-console"; + } else { + metaData.tdCls = "x-grid-row-loading"; + } + return ""; + } + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("Node"), + dataIndex: 'node', + width: 100 + }, + { + header: gettext("User name"), + dataIndex: 'user', + width: 150 + }, + { + header: gettext("Description"), + dataIndex: 'upid', + flex: 1, + renderer: Proxmox.Utils.render_upid + }, + { + header: gettext("Status"), + dataIndex: 'status', + width: 200, + renderer: function(value, metaData, record) { + if (record.data.pid) { + if (record.data.type != "vncproxy") { + metaData.tdCls = "x-grid-row-loading"; + } + return ""; + } + if (value == 'OK') { + return 'OK'; + } + // metaData.attr = 'style="color:red;"'; + return Proxmox.Utils.errorText + ': ' + value; + } + } + ], + listeners: { + itemdblclick: run_task_viewer, + show: taskstore.startUpdate, + destroy: taskstore.stopUpdate + } + }); + + me.callParent(); + } +}); +/* This class defines the "Cluster log" tab of the bottom status panel + * A log entry is a timestamp associated with an action on a cluster + */ + +Ext.define('PVE.dc.Log', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveClusterLog'], + + initComponent : function() { + var me = this; + + var logstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-cluster-log', + model: 'proxmox-cluster-log', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/log' + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: logstore, + appendAtStart: true + }); + + Ext.apply(me, { + store: store, + stateful: false, + + viewConfig: { + trackOver: false, + stripeRows: true, + + getRowClass: function(record, index) { + var pri = record.get('pri'); + + if (pri && pri <= 3) { + return "proxmox-invalid-row"; + } + } + }, + sortableColumns: false, + columns: [ + { + header: gettext("Time"), + dataIndex: 'time', + width: 150, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("Node"), + dataIndex: 'node', + width: 150 + }, + { + header: gettext("Service"), + dataIndex: 'tag', + width: 100 + }, + { + header: "PID", + dataIndex: 'pid', + width: 100 + }, + { + header: gettext("User name"), + dataIndex: 'user', + width: 150 + }, + { + header: gettext("Severity"), + dataIndex: 'pri', + renderer: PVE.Utils.render_serverity, + width: 100 + }, + { + header: gettext("Message"), + dataIndex: 'msg', + flex: 1 + } + ], + listeners: { + activate: logstore.startUpdate, + deactivate: logstore.stopUpdate, + destroy: logstore.stopUpdate + } + }); + + me.callParent(); + } +}); +/* + * This class describes the bottom panel + */ +Ext.define('PVE.panel.StatusPanel', { + extend: 'Ext.tab.Panel', + alias: 'widget.pveStatusPanel', + + + //title: "Logs", + //tabPosition: 'bottom', + + initComponent: function() { + var me = this; + + var stateid = 'ltab'; + var sp = Ext.state.Manager.getProvider(); + + var state = sp.get(stateid); + if (state && state.value) { + me.activeTab = state.value; + } + + Ext.apply(me, { + listeners: { + tabchange: function() { + var atab = me.getActiveTab().itemId; + var state = { value: atab }; + sp.set(stateid, state); + } + }, + items: [ + { + itemId: 'tasks', + title: gettext('Tasks'), + xtype: 'pveClusterTasks' + }, + { + itemId: 'clog', + title: gettext('Cluster log'), + xtype: 'pveClusterLog' + } + ] + }); + + me.callParent(); + + me.items.get(0).fireEvent('show', me.items.get(0)); + + var statechange = function(sp, key, state) { + if (key === stateid) { + var atab = me.getActiveTab().itemId; + var ntab = state.value; + if (state && ntab && (atab != ntab)) { + me.setActiveTab(ntab); + } + } + }; + + sp.on('statechange', statechange); + me.on('destroy', function() { + sp.un('statechange', statechange); + }); + + } +}); +Ext.define('PVE.panel.StatusView', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveStatusView', + + layout: { + type: 'column' + }, + + title: gettext('Status'), + + getRecordValue: function(key, store) { + if (!key) { + throw "no key given"; + } + var me = this; + + if (store === undefined) { + store = me.getStore(); + } + + var rec = store.getById(key); + if (rec) { + return rec.data.value; + } + + return ''; + }, + + fieldRenderer: function(val,max) { + if (max === undefined) { + return val; + } + + if (!Ext.isNumeric(max) || max === 1) { + return PVE.Utils.render_usage(val); + } + return PVE.Utils.render_size_usage(val,max); + }, + + fieldCalculator: function(used, max) { + if (!Ext.isNumeric(max) && Ext.isNumeric(used)) { + return used; + } else if(!Ext.isNumeric(used)) { + /* we come here if the field is from a node + * where the records are not mem and maxmem + * but mem.used and mem.total + */ + if (used.used !== undefined && + used.total !== undefined) { + return used.used/used.total; + } + } + + return used/max; + }, + + updateField: function(field) { + var me = this; + var text = ''; + var renderer = me.fieldRenderer; + if (Ext.isFunction(field.renderer)) { + renderer = field.renderer; + } + if (field.multiField === true) { + field.updateValue(renderer.call(field, me.getStore().getRecord())); + } else if (field.textField !== undefined) { + field.updateValue(renderer.call(field, me.getRecordValue(field.textField))); + } else if(field.valueField !== undefined) { + var used = me.getRecordValue(field.valueField); + /*jslint confusion: true*/ + /* string and int */ + var max = field.maxField !== undefined ? me.getRecordValue(field.maxField) : 1; + + var calculate = me.fieldCalculator; + + if (Ext.isFunction(field.calculate)) { + calculate = field.calculate; + } + field.updateValue(renderer.call(field, used,max), calculate(used,max)); + } + }, + + getStore: function() { + var me = this; + if (!me.rstore) { + throw "there is no rstore"; + } + + return me.rstore; + }, + + updateTitle: function() { + var me = this; + me.setTitle(me.getRecordValue('name')); + }, + + updateValues: function(store, records, success) { + if (!success) { + return; // do not update if store load was not successful + } + var me = this; + var itemsToUpdate = me.query('pveInfoWidget'); + + itemsToUpdate.forEach(me.updateField, me); + + me.updateTitle(store); + }, + + initComponent: function() { + var me = this; + + if (!me.rstore) { + throw "no rstore given"; + } + + if (!me.title) { + throw "no title given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + me.callParent(); + + me.mon(me.rstore, 'load', 'updateValues'); + } + +}); +Ext.define('PVE.panel.GuestStatusView', { + extend: 'PVE.panel.StatusView', + alias: 'widget.pveGuestStatusView', + mixins: ['Proxmox.Mixin.CBind'], + + height: 300, + + cbindData: function (initialConfig) { + var me = this; + return { + isQemu: me.pveSelNode.data.type === 'qemu', + isLxc: me.pveSelNode.data.type === 'lxc' + }; + }, + + layout: { + type: 'vbox', + align: 'stretch' + }, + + defaults: { + xtype: 'pveInfoWidget', + padding: '2 25' + }, + items: [ + { + xtype: 'box', + height: 20 + }, + { + itemId: 'status', + title: gettext('Status'), + iconCls: 'fa fa-info fa-fw', + printBar: false, + multiField: true, + renderer: function(record) { + var me = this; + var text = record.data.status; + var qmpstatus = record.data.qmpstatus; + if (qmpstatus && qmpstatus !== record.data.status) { + text += ' (' + qmpstatus + ')'; + } + return text; + } + }, + { + itemId: 'hamanaged', + iconCls: 'fa fa-heartbeat fa-fw', + title: gettext('HA State'), + printBar: false, + textField: 'ha', + renderer: PVE.Utils.format_ha + }, + { + xtype: 'pveInfoWidget', + itemId: 'node', + iconCls: 'fa fa-building fa-fw', + title: gettext('Node'), + cbind: { + text: '{pveSelNode.data.node}' + }, + printBar: false + }, + { + xtype: 'box', + height: 15 + }, + { + itemId: 'cpu', + iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon', + title: gettext('CPU usage'), + valueField: 'cpu', + maxField: 'cpus', + renderer: PVE.Utils.render_cpu_usage, + // in this specific api call + // we already have the correct value for the usage + calculate: Ext.identityFn + }, + { + itemId: 'memory', + iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon', + title: gettext('Memory usage'), + valueField: 'mem', + maxField: 'maxmem' + }, + { + itemId: 'swap', + xtype: 'pveInfoWidget', + iconCls: 'fa fa-refresh fa-fw', + title: gettext('SWAP usage'), + valueField: 'swap', + maxField: 'maxswap', + cbind: { + hidden: '{isQemu}', + disabled: '{isQemu}' + } + }, + { + itemId: 'rootfs', + iconCls: 'fa fa-hdd-o fa-fw', + title: gettext('Bootdisk size'), + valueField: 'disk', + maxField: 'maxdisk', + printBar: false, + renderer: function(used, max) { + var me = this; + me.setPrintBar(used > 0); + if (used === 0) { + return PVE.Utils.render_size(max); + } else { + return PVE.Utils.render_size_usage(used,max); + } + } + }, + { + xtype: 'box', + height: 15 + }, + { + itemId: 'ips', + xtype: 'pveAgentIPView', + cbind: { + rstore: '{rstore}', + pveSelNode: '{pveSelNode}', + hidden: '{isLxc}', + disabled: '{isLxc}' + } + } + ], + + updateTitle: function() { + var me = this; + var uptime = me.getRecordValue('uptime'); + + var text = ""; + if (Number(uptime) > 0) { + text = " (" + gettext('Uptime') + ': ' + Proxmox.Utils.format_duration_long(uptime) + + ')'; + } + + me.setTitle(me.getRecordValue('name') + text); + } +}); +/* + * This is a running chart widget + * you add time datapoints to it, + * and we only show the last x of it + * used for ceph performance charts + */ +Ext.define('PVE.widget.RunningChart', { + extend: 'Ext.container.Container', + alias: 'widget.pveRunningChart', + + layout: { + type: 'hbox', + align: 'center' + }, + items: [ + { + width: 80, + xtype: 'box', + itemId: 'title', + data: { + title: '' + }, + tpl: '

{title}:

' + }, + { + flex: 1, + xtype: 'cartesian', + height: '100%', + itemId: 'chart', + border: false, + axes: [ + { + type: 'numeric', + position: 'left', + hidden: true, + minimum: 0 + }, + { + type: 'numeric', + position: 'bottom', + hidden: true + } + ], + + store: { + data: {} + }, + + sprites: [{ + id: 'valueSprite', + type: 'text', + text: '0 B/s', + textAlign: 'end', + textBaseline: 'middle', + fontSize: 14 + }], + + series: [{ + type: 'line', + xField: 'time', + yField: 'val', + fill: 'true', + colors: ['#cfcfcf'], + tooltip: { + trackMouse: true, + renderer: function( tooltip, record, ctx) { + var me = this.getChart(); + var date = new Date(record.data.time); + var value = me.up().renderer(record.data.val); + tooltip.setHtml( + me.up().title + ': ' + value + '
' + + Ext.Date.format(date, 'H:i:s') + ); + } + }, + style: { + lineWidth: 1.5, + opacity: 0.60 + }, + marker: { + opacity: 0, + scaling: 0.01, + fx: { + duration: 200, + easing: 'easeOut' + } + }, + highlightCfg: { + opacity: 1, + scaling: 1.5 + } + }] + } + ], + + // the renderer for the tooltip and last value, + // default just the value + renderer: Ext.identityFn, + + // show the last x seconds + // default is 5 minutes + timeFrame: 5*60, + + addDataPoint: function(value, time) { + var me = this.chart; + var panel = me.up(); + var now = new Date(); + var begin = new Date(now.getTime() - (1000*panel.timeFrame)); + + me.store.add({ + time: time || now.getTime(), + val: value || 0 + }); + + // delete all old records when we have 20 times more datapoints + // than seconds in our timeframe (so even a subsecond graph does + // not trigger this often) + // + // records in the store do not take much space, but like this, + // we prevent a memory leak when someone has the site open for a long time + // with minimal graphical glitches + if (me.store.count() > panel.timeFrame * 20) { + var oldData = me.store.getData().createFiltered(function(item) { + return item.data.time < begin.getTime(); + }); + + me.store.remove(oldData.getRange()); + } + + me.timeaxis.setMinimum(begin.getTime()); + me.timeaxis.setMaximum(now.getTime()); + me.valuesprite.setText(panel.renderer(value || 0).toString()); + me.valuesprite.setAttributes({ + x: me.getWidth() - 15, + y: me.getHeight()/2 + }, true); + me.redraw(); + }, + + setTitle: function(title) { + this.title = title; + var me = this.getComponent('title'); + me.update({title: title}); + }, + + initComponent: function(){ + var me = this; + me.callParent(); + + if (me.title) { + me.getComponent('title').update({title: me.title}); + } + me.chart = me.getComponent('chart'); + me.chart.timeaxis = me.chart.getAxes()[1]; + me.chart.valuesprite = me.chart.getSurface('chart').get('valueSprite'); + if (me.color) { + me.chart.series[0].setStyle({ + fill: me.color, + stroke: me.color + }); + } + } +}); +Ext.define('PVE.widget.Info',{ + extend: 'Ext.container.Container', + alias: 'widget.pveInfoWidget', + + layout: { + type: 'vbox', + align: 'stretch' + }, + + value: 0, + maximum: 1, + printBar: true, + items: [ + { + xtype: 'component', + itemId: 'label', + data: { + title: '', + usage: '', + iconCls: undefined + }, + tpl: [ + '
', + '', + ' ', + '', + '{title}
 
{usage}
' + ] + }, + { + height: 2, + border: 0 + }, + { + xtype: 'progressbar', + itemId: 'progress', + height: 5, + value: 0, + animate: true + } + ], + + warningThreshold: 0.6, + criticalThreshold: 0.9, + + setPrintBar: function(enable) { + var me = this; + me.printBar = enable; + me.getComponent('progress').setVisible(enable); + }, + + setIconCls: function(iconCls) { + var me = this; + me.getComponent('label').data.iconCls = iconCls; + }, + + updateValue: function(text, usage) { + var me = this; + var label = me.getComponent('label'); + label.update(Ext.apply(label.data, {title: me.title, usage:text})); + + if (usage !== undefined && + me.printBar && + Ext.isNumeric(usage) && + usage >= 0) { + var progressBar = me.getComponent('progress'); + progressBar.updateProgress(usage, ''); + if (usage > me.criticalThreshold) { + progressBar.removeCls('warning'); + progressBar.addCls('critical'); + } else if (usage > me.warningThreshold) { + progressBar.removeCls('critical'); + progressBar.addCls('warning'); + } else { + progressBar.removeCls('warning'); + progressBar.removeCls('critical'); + } + } + }, + + initComponent: function() { + var me = this; + + if (!me.title) { + throw "no title defined"; + } + + me.callParent(); + + me.getComponent('progress').setVisible(me.printBar); + + me.updateValue(me.text, me.value); + me.setIconCls(me.iconCls); + } + +}); +Ext.define('PVE.panel.TemplateStatusView',{ + extend: 'PVE.panel.StatusView', + alias: 'widget.pveTemplateStatusView', + + layout: { + type: 'vbox', + align: 'stretch' + }, + + defaults: { + xtype: 'pveInfoWidget', + printBar: false, + padding: '2 25' + }, + items: [ + { + xtype: 'box', + height: 20 + }, + { + itemId: 'hamanaged', + iconCls: 'fa fa-heartbeat fa-fw', + title: gettext('HA State'), + printBar: false, + textField: 'ha', + renderer: PVE.Utils.format_ha + }, + { + itemId: 'node', + iconCls: 'fa fa-fw fa-building', + title: gettext('Node') + }, + { + xtype: 'box', + height: 20 + }, + { + itemId: 'cpus', + iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon', + title: gettext('Processors'), + textField: 'cpus' + }, + { + itemId: 'memory', + iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon', + title: gettext('Memory'), + textField: 'maxmem', + renderer: PVE.Utils.render_size + }, + { + itemId: 'swap', + iconCls: 'fa fa-refresh fa-fw', + title: gettext('Swap'), + textField: 'maxswap', + renderer: PVE.Utils.render_size + }, + { + itemId: 'disk', + iconCls: 'fa fa-hdd-o fa-fw', + title: gettext('Bootdisk size'), + textField: 'maxdisk', + renderer: PVE.Utils.render_size + }, + { + xtype: 'box', + height: 20 + } + ], + + initComponent: function() { + var me = this; + + var name = me.pveSelNode.data.name; + if (!name) { + throw "no name specified"; + } + + me.title = name; + + me.callParent(); + if (me.pveSelNode.data.type !== 'lxc') { + me.remove(me.getComponent('swap')); + } + me.getComponent('node').updateValue(me.pveSelNode.data.node); + } +}); +Ext.define('PVE.widget.HealthWidget', { + extend: 'Ext.Component', + alias: 'widget.pveHealthWidget', + + data: { + iconCls: PVE.Utils.get_health_icon(undefined, true), + text: '', + title: '' + }, + + style: { + 'text-align':'center' + }, + + tpl: [ + '

{title}

', + '', + '

', + '{text}' + ], + + updateHealth: function(data) { + var me = this; + me.update(Ext.apply(me.data, data)); + }, + + initComponent: function(){ + var me = this; + + if (me.title) { + me.config.data.title = me.title; + } + + me.callParent(); + } + +}); +/*global u2f*/ +Ext.define('PVE.window.LoginWindow', { + extend: 'Ext.window.Window', + + controller: { + + xclass: 'Ext.app.ViewController', + + onLogon: function() { + var me = this; + + var form = this.lookupReference('loginForm'); + var unField = this.lookupReference('usernameField'); + var saveunField = this.lookupReference('saveunField'); + var view = this.getView(); + + if (!form.isValid()) { + return; + } + + view.el.mask(gettext('Please wait...'), 'x-mask-loading'); + + // set or clear username + var sp = Ext.state.Manager.getProvider(); + if (saveunField.getValue() === true) { + sp.set(unField.getStateId(), unField.getValue()); + } else { + sp.clear(unField.getStateId()); + } + sp.set(saveunField.getStateId(), saveunField.getValue()); + + form.submit({ + failure: function(f, resp){ + me.failure(resp); + }, + success: function(f, resp){ + view.el.unmask(); + + var data = resp.result.data; + if (Ext.isDefined(data.NeedTFA)) { + // Store first factor login information first: + data.LoggedOut = true; + Proxmox.Utils.setAuthData(data); + + if (Ext.isDefined(data.U2FChallenge)) { + me.perform_u2f(data); + } else { + me.perform_otp(); + } + } else { + me.success(data); + } + } + }); + + }, + failure: function(resp) { + var me = this; + var view = me.getView(); + view.el.unmask(); + var handler = function() { + var uf = me.lookupReference('usernameField'); + uf.focus(true, true); + }; + + Ext.MessageBox.alert(gettext('Error'), + gettext("Login failed. Please try again"), + handler); + }, + success: function(data) { + var me = this; + var view = me.getView(); + var handler = view.handler || Ext.emptyFn; + handler.call(me, data); + view.close(); + }, + + perform_otp: function() { + var me = this; + var win = Ext.create('PVE.window.TFALoginWindow', { + onLogin: function(value) { + me.finish_tfa(value); + }, + onCancel: function() { + Proxmox.LoggedOut = false; + Proxmox.Utils.authClear(); + me.getView().show(); + } + }); + win.show(); + }, + + perform_u2f: function(data) { + var me = this; + // Show the message: + var msg = Ext.Msg.show({ + title: 'U2F: '+gettext('Verification'), + message: gettext('Please press the button on your U2F Device'), + buttons: [] + }); + var chlg = data.U2FChallenge; + var key = { + version: chlg.version, + keyHandle: chlg.keyHandle + }; + u2f.sign(chlg.appId, chlg.challenge, [key], function(res) { + msg.close(); + if (res.errorCode) { + Proxmox.Utils.authClear(); + Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode)); + return; + } + delete res.errorCode; + me.finish_tfa(JSON.stringify(res)); + }); + }, + finish_tfa: function(res) { + var me = this; + var view = me.getView(); + view.el.mask(gettext('Please wait...'), 'x-mask-loading'); + var params = { response: res }; + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'POST', + timeout: 5000, // it'll delay both success & failure + success: function(resp, opts) { + view.el.unmask(); + // Fill in what we copy over from the 1st factor: + var data = resp.result.data; + data.CSRFPreventionToken = Proxmox.CSRFPreventionToken; + data.username = Proxmox.UserName; + // Finish logging in: + me.success(data); + }, + failure: function(resp, opts) { + Proxmox.Utils.authClear(); + me.failure(resp); + } + }); + }, + + control: { + 'field[name=username]': { + specialkey: function(f, e) { + if (e.getKey() === e.ENTER) { + var pf = this.lookupReference('passwordField'); + if (!pf.getValue()) { + pf.focus(false); + } + } + } + }, + 'field[name=lang]': { + change: function(f, value) { + var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10); + Ext.util.Cookies.set('PVELangCookie', value, dt); + this.getView().mask(gettext('Please wait...'), 'x-mask-loading'); + window.location.reload(); + } + }, + 'button[reference=loginButton]': { + click: 'onLogon' + }, + '#': { + show: function() { + var sp = Ext.state.Manager.getProvider(); + var checkboxField = this.lookupReference('saveunField'); + var unField = this.lookupReference('usernameField'); + + var checked = sp.get(checkboxField.getStateId()); + checkboxField.setValue(checked); + + if(checked === true) { + var username = sp.get(unField.getStateId()); + unField.setValue(username); + var pwField = this.lookupReference('passwordField'); + pwField.focus(); + } + } + } + } + }, + + width: 400, + + modal: true, + + border: false, + + draggable: true, + + closable: false, + + resizable: false, + + layout: 'auto', + + title: gettext('Proxmox VE Login'), + + defaultFocus: 'usernameField', + + defaultButton: 'loginButton', + + items: [{ + xtype: 'form', + layout: 'form', + url: '/api2/extjs/access/ticket', + reference: 'loginForm', + + fieldDefaults: { + labelAlign: 'right', + allowBlank: false + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('User name'), + name: 'username', + itemId: 'usernameField', + reference: 'usernameField', + stateId: 'login-username' + }, + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Password'), + name: 'password', + reference: 'passwordField' + }, + { + xtype: 'pveRealmComboBox', + name: 'realm' + }, + { + xtype: 'proxmoxLanguageSelector', + fieldLabel: gettext('Language'), + value: Ext.util.Cookies.get('PVELangCookie') || Proxmox.defaultLang || 'en', + name: 'lang', + reference: 'langField', + submitValue: false + } + ], + buttons: [ + { + xtype: 'checkbox', + fieldLabel: gettext('Save User name'), + name: 'saveusername', + reference: 'saveunField', + stateId: 'login-saveusername', + labelWidth: 'auto', + labelAlign: 'right', + submitValue: false + }, + { + text: gettext('Login'), + reference: 'loginButton' + } + ] + }] + }); +Ext.define('PVE.window.TFALoginWindow', { + extend: 'Ext.window.Window', + + modal: true, + resizable: false, + title: 'Two-Factor Authentication', + layout: 'form', + defaultButton: 'loginButton', + defaultFocus: 'otpField', + + controller: { + xclass: 'Ext.app.ViewController', + login: function() { + var me = this; + var view = me.getView(); + view.onLogin(me.lookup('otpField').getValue()); + view.close(); + }, + cancel: function() { + var me = this; + var view = me.getView(); + view.onCancel(); + view.close(); + } + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Please enter your OTP verification code:'), + name: 'otp', + itemId: 'otpField', + reference: 'otpField', + allowBlank: false + } + ], + + buttons: [ + { + text: gettext('Login'), + reference: 'loginButton', + handler: 'login' + }, + { + text: gettext('Cancel'), + handler: 'cancel' + } + ] +}); +Ext.define('PVE.window.Wizard', { + extend: 'Ext.window.Window', + + activeTitle: '', // used for automated testing + + width: 700, + height: 510, + + modal: true, + border: false, + + draggable: true, + closable: true, + resizable: false, + + layout: 'border', + + getValues: function(dirtyOnly) { + var me = this; + + var values = {}; + + var form = me.down('form').getForm(); + + form.getFields().each(function(field) { + if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) { + Proxmox.Utils.assemble_field_data(values, field.getSubmitData()); + } + }); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly)); + }); + + return values; + }, + + initComponent: function() { + var me = this; + + var tabs = me.items || []; + delete me.items; + + /* + * Items may have the following functions: + * validator(): per tab custom validation + * onSubmit(): submit handler + * onGetValues(): overwrite getValues results + */ + + Ext.Array.each(tabs, function(tab) { + tab.disabled = true; + }); + tabs[0].disabled = false; + + var maxidx = 0; + var curidx = 0; + + var check_card = function(card) { + var valid = true; + var fields = card.query('field, fieldcontainer'); + if (card.isXType('fieldcontainer')) { + fields.unshift(card); + } + Ext.Array.each(fields, function(field) { + // Note: not all fielcontainer have isValid() + if (Ext.isFunction(field.isValid) && !field.isValid()) { + valid = false; + } + }); + + if (Ext.isFunction(card.validator)) { + return card.validator(); + } + + return valid; + }; + + var disable_at = function(card) { + var tp = me.down('#wizcontent'); + var idx = tp.items.indexOf(card); + for(;idx < tp.items.getCount();idx++) { + var nc = tp.items.getAt(idx); + if (nc) { + nc.disable(); + } + } + }; + + var tabchange = function(tp, newcard, oldcard) { + if (newcard.onSubmit) { + me.down('#next').setVisible(false); + me.down('#submit').setVisible(true); + } else { + me.down('#next').setVisible(true); + me.down('#submit').setVisible(false); + } + var valid = check_card(newcard); + me.down('#next').setDisabled(!valid); + me.down('#submit').setDisabled(!valid); + me.down('#back').setDisabled(tp.items.indexOf(newcard) == 0); + + var idx = tp.items.indexOf(newcard); + if (idx > maxidx) { + maxidx = idx; + } + curidx = idx; + + var next = idx + 1; + var ntab = tp.items.getAt(next); + if (valid && ntab && !newcard.onSubmit) { + ntab.enable(); + } + }; + + if (me.subject && !me.title) { + me.title = Proxmox.Utils.dialog_title(me.subject, true, false); + } + + var sp = Ext.state.Manager.getProvider(); + var advchecked = sp.get('proxmox-advanced-cb'); + + Ext.apply(me, { + items: [ + { + xtype: 'form', + region: 'center', + layout: 'fit', + border: false, + margins: '5 5 0 5', + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [{ + itemId: 'wizcontent', + xtype: 'tabpanel', + activeItem: 0, + bodyPadding: 10, + listeners: { + afterrender: function(tp) { + var atab = this.getActiveTab(); + tabchange(tp, atab); + }, + tabchange: function(tp, newcard, oldcard) { + tabchange(tp, newcard, oldcard); + } + }, + items: tabs + }] + } + ], + fbar: [ + { + xtype: 'proxmoxHelpButton', + itemId: 'help' + }, + '->', + { + xtype: 'proxmoxcheckbox', + boxLabelAlign: 'before', + boxLabel: gettext('Advanced'), + value: advchecked, + listeners: { + change: function(cb, val) { + var tp = me.down('#wizcontent'); + tp.query('inputpanel').forEach(function(ip) { + ip.setAdvancedVisible(val); + }); + + sp.set('proxmox-advanced-cb', val); + } + } + }, + { + text: gettext('Back'), + disabled: true, + itemId: 'back', + minWidth: 60, + handler: function() { + var tp = me.down('#wizcontent'); + var atab = tp.getActiveTab(); + var prev = tp.items.indexOf(atab) - 1; + if (prev < 0) { + return; + } + var ntab = tp.items.getAt(prev); + if (ntab) { + tp.setActiveTab(ntab); + } + } + }, + { + text: gettext('Next'), + disabled: true, + itemId: 'next', + minWidth: 60, + handler: function() { + + var form = me.down('form').getForm(); + + var tp = me.down('#wizcontent'); + var atab = tp.getActiveTab(); + if (!check_card(atab)) { + return; + } + + var next = tp.items.indexOf(atab) + 1; + var ntab = tp.items.getAt(next); + if (ntab) { + ntab.enable(); + tp.setActiveTab(ntab); + } + + } + }, + { + text: gettext('Finish'), + minWidth: 60, + hidden: true, + itemId: 'submit', + handler: function() { + var tp = me.down('#wizcontent'); + var atab = tp.getActiveTab(); + atab.onSubmit(); + } + } + ] + }); + me.callParent(); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + panel.setAdvancedVisible(advchecked); + }); + + Ext.Array.each(me.query('field'), function(field) { + var validcheck = function() { + var tp = me.down('#wizcontent'); + + // check tabs from current to the last enabled for validity + // since we might have changed a validity on a later one + var i; + for (i = curidx; i <= maxidx && i < tp.items.getCount(); i++) { + var tab = tp.items.getAt(i); + var valid = check_card(tab); + + // only set the buttons on the current panel + if (i === curidx) { + me.down('#next').setDisabled(!valid); + me.down('#submit').setDisabled(!valid); + } + + // if a panel is invalid, then disable it and all following, + // else enable it and go to the next + var ntab = tp.items.getAt(i + 1); + if (!valid) { + disable_at(ntab); + return; + } else if (ntab && !tab.onSubmit) { + ntab.enable(); + } + } + }; + field.on('change', validcheck); + field.on('validitychange', validcheck); + }); + } +}); +Ext.define('PVE.window.NotesEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.apply(me, { + title: gettext('Notes'), + width: 600, + height: '400px', + resizable: true, + layout: 'fit', + defaultButton: undefined, + items: { + xtype: 'textarea', + name: 'description', + height: '100%', + value: '', + hideLabel: true + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('PVE.window.Backup', { + extend: 'Ext.window.Window', + + resizable: false, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.vmtype) { + throw "no VM type specified"; + } + + var storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: me.nodename, + name: 'storage', + value: me.storage, + fieldLabel: gettext('Storage'), + storageContent: 'backup', + allowBlank: false + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [ + storagesel, + { + xtype: 'pveBackupModeSelector', + fieldLabel: gettext('Mode'), + value: 'snapshot', + name: 'mode' + }, + { + xtype: 'pveCompressionSelector', + name: 'compress', + value: 'lzo', + fieldLabel: gettext('Compression') + }, + { + xtype: 'textfield', + fieldLabel: gettext('Send email to'), + name: 'mailto', + emptyText: Proxmox.Utils.noneText + } + ] + }); + + var form = me.formPanel.getForm(); + + var submitBtn = Ext.create('Ext.Button', { + text: gettext('Backup'), + handler: function(){ + var storage = storagesel.getValue(); + var values = form.getValues(); + var params = { + storage: storage, + vmid: me.vmid, + mode: values.mode, + remove: 0 + }; + + if ( values.mailto ) { + params.mailto = values.mailto; + } + + if (values.compress) { + params.compress = values.compress; + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/vzdump', + params: params, + method: 'POST', + failure: function (response, opts) { + Ext.Msg.alert('Error',response.htmlStatus); + }, + success: function(response, options) { + // close later so we reload the grid + // after the task has completed + me.hide(); + + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + listeners: { + close: function() { + me.close(); + } + } + }); + win.show(); + } + }); + } + }); + + var helpBtn = Ext.create('Proxmox.button.Help', { + onlineHelp: 'chapter_vzdump', + listenToGlobalEvent: false, + hidden: false + }); + + var title = gettext('Backup') + " " + + ((me.vmtype === 'lxc') ? "CT" : "VM") + + " " + me.vmid; + + Ext.apply(me, { + title: title, + width: 350, + modal: true, + layout: 'auto', + border: false, + items: [ me.formPanel ], + buttons: [ helpBtn, '->', submitBtn ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.window.Restore', { + extend: 'Ext.window.Window', // fixme: Proxmox.window.Edit? + + resizable: false, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.volid) { + throw "no volume ID specified"; + } + + if (!me.vmtype) { + throw "no vmtype specified"; + } + + var storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: me.nodename, + name: 'storage', + value: '', + fieldLabel: gettext('Storage'), + storageContent: (me.vmtype === 'lxc') ? 'rootdir' : 'images', + allowBlank: true + }); + + var IDfield; + if (me.vmid) { + IDfield = Ext.create('Ext.form.field.Display', { + name: 'vmid', + value: me.vmid, + fieldLabel: (me.vmtype === 'lxc') ? 'CT' : 'VM' + }); + } else { + IDfield = Ext.create('PVE.form.GuestIDSelector', { + name: 'vmid', + guestType: me.vmtype, + loadNextFreeID: true, + validateExists: false + }); + } + + var items = [ + { + xtype: 'displayfield', + value: me.volidText || me.volid, + fieldLabel: gettext('Source') + }, + storagesel, + IDfield, + { + xtype: 'proxmoxintegerfield', + name: 'bwlimit', + fieldLabel: gettext('Read Limit (MiB/s)'), + minValue: 0, + emptyText: gettext('Defaults to target storage restore limit'), + autoEl: { + tag: 'div', + 'data-qtip': gettext("Use '0' to disable all bandwidth limits.") + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'unique', + fieldLabel: gettext('Unique'), + hidden: !!me.vmid, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Autogenerate unique properties, e.g., MAC addresses') + }, + checked: false + } + ]; + + /*jslint confusion: true*/ + if (me.vmtype === 'lxc') { + items.push({ + xtype: 'proxmoxcheckbox', + name: 'unprivileged', + value: true, + fieldLabel: gettext('Unprivileged container') + }); + } + /*jslint confusion: false*/ + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var doRestore = function(url, params) { + Proxmox.Utils.API2Request({ + url: url, + params: params, + method: 'POST', + waitMsgTarget: me, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + me.close(); + } + }); + }; + + var submitBtn = Ext.create('Ext.Button', { + text: gettext('Restore'), + handler: function(){ + var storage = storagesel.getValue(); + var values = form.getValues(); + + var params = { + storage: storage, + vmid: me.vmid || values.vmid, + force: me.vmid ? 1 : 0 + }; + if (values.unique) { params.unique = 1; } + + if (values.bwlimit !== undefined) { + params.bwlimit = values.bwlimit * 1024; + } + + var url; + var msg; + if (me.vmtype === 'lxc') { + url = '/nodes/' + me.nodename + '/lxc'; + params.ostemplate = me.volid; + params.restore = 1; + if (values.unprivileged) { params.unprivileged = 1; } + msg = Proxmox.Utils.format_task_description('vzrestore', params.vmid); + } else if (me.vmtype === 'qemu') { + url = '/nodes/' + me.nodename + '/qemu'; + params.archive = me.volid; + msg = Proxmox.Utils.format_task_description('qmrestore', params.vmid); + } else { + throw 'unknown VM type'; + } + + if (me.vmid) { + msg += '. ' + gettext('This will permanently erase current VM data.'); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + doRestore(url, params); + }); + } else { + doRestore(url, params); + } + } + }); + + form.on('validitychange', function(f, valid) { + submitBtn.setDisabled(!valid); + }); + + var title = gettext('Restore') + ": " + ( + (me.vmtype === 'lxc') ? 'CT' : 'VM'); + + if (me.vmid) { + title += " " + me.vmid; + } + + Ext.apply(me, { + title: title, + width: 500, + modal: true, + layout: 'auto', + border: false, + items: [ me.formPanel ], + buttons: [ submitBtn ] + }); + + me.callParent(); + } +}); +/* Popup a message window + * where the user has to manually enter the resource ID + * to enable the destroy button + */ +Ext.define('PVE.window.SafeDestroy', { + extend: 'Ext.window.Window', + alias: 'widget.pveSafeDestroy', + + title: gettext('Confirm'), + modal: true, + buttonAlign: 'center', + bodyPadding: 10, + width: 450, + layout: { type:'hbox' }, + defaultFocus: 'confirmField', + showProgress: false, + + config: { + item: { + id: undefined, + type: undefined + }, + url: undefined, + params: {} + }, + + getParams: function() { + var me = this; + if (Ext.Object.isEmpty(me.params)) { + return ''; + } + return '?' + Ext.Object.toQueryString(me.params); + }, + + controller: { + + xclass: 'Ext.app.ViewController', + + control: { + 'field[name=confirm]': { + change: function(f, value) { + var view = this.getView(); + var removeButton = this.lookupReference('removeButton'); + if (value === view.getItem().id.toString()) { + removeButton.enable(); + } else { + removeButton.disable(); + } + }, + specialkey: function (field, event) { + var removeButton = this.lookupReference('removeButton'); + if (!removeButton.isDisabled() && event.getKey() == event.ENTER) { + removeButton.fireEvent('click', removeButton, event); + } + } + }, + 'button[reference=removeButton]': { + click: function() { + var view = this.getView(); + Proxmox.Utils.API2Request({ + url: view.getUrl() + view.getParams(), + method: 'DELETE', + waitMsgTarget: view, + failure: function(response, opts) { + view.close(); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var hasProgressBar = view.showProgress && + response.result.data ? true : false; + + if (hasProgressBar) { + // stay around so we can trigger our close events + // when background action is completed + view.hide(); + + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + listeners: { + destroy: function () { + view.close(); + } + } + }); + win.show(); + } else { + view.close(); + } + } + }); + } + } + } + }, + + items: [ + { + xtype: 'component', + cls: [ Ext.baseCSSPrefix + 'message-box-icon', + Ext.baseCSSPrefix + 'message-box-warning', + Ext.baseCSSPrefix + 'dlg-icon'] + }, + { + xtype: 'container', + flex: 1, + layout: { + type: 'vbox', + align: 'stretch' + }, + items: [ + { + xtype: 'component', + reference: 'messageCmp' + }, + { + itemId: 'confirmField', + reference: 'confirmField', + xtype: 'textfield', + name: 'confirm', + labelWidth: 300, + hideTrigger: true, + allowBlank: false + } + ] + } + ], + buttons: [ + { + reference: 'removeButton', + text: gettext('Remove'), + disabled: true + } + ], + + initComponent : function() { + var me = this; + + me.callParent(); + + var item = me.getItem(); + + if (!Ext.isDefined(item.id)) { + throw "no ID specified"; + } + + if (!Ext.isDefined(item.type)) { + throw "no VM type specified"; + } + + var messageCmp = me.lookupReference('messageCmp'); + var msg; + + if (item.type === 'VM') { + msg = Proxmox.Utils.format_task_description('qmdestroy', item.id); + } else if (item.type === 'CT') { + msg = Proxmox.Utils.format_task_description('vzdestroy', item.id); + } else if (item.type === 'CephPool') { + msg = Proxmox.Utils.format_task_description('cephdestroypool', item.id); + } else if (item.type === 'Image') { + msg = Proxmox.Utils.format_task_description('unknownimgdel', item.id); + } else { + throw "unknown item type specified"; + } + + messageCmp.setHtml(msg); + + var confirmField = me.lookupReference('confirmField'); + msg = gettext('Please enter the ID to confirm') + + ' (' + item.id + ')'; + confirmField.setFieldLabel(msg); + } +}); +Ext.define('PVE.window.BackupConfig', { + extend: 'Ext.window.Window', + title: gettext('Configuration'), + width: 600, + height: 400, + layout: 'fit', + modal: true, + items: { + xtype: 'component', + itemId: 'configtext', + autoScroll: true, + style: { + 'background-color': 'white', + 'white-space': 'pre', + 'font-family': 'monospace', + padding: '5px' + } + }, + + initComponent: function() { + var me = this; + + if (!me.volume) { + throw "no volume specified"; + } + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.callParent(); + + Proxmox.Utils.API2Request({ + url: "/nodes/" + nodename + "/vzdump/extractconfig", + method: 'GET', + params: { + volume: me.volume + }, + failure: function(response, opts) { + me.close(); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response,options) { + me.show(); + me.down('#configtext').update(Ext.htmlEncode(response.result.data)); + } + }); + } +}); +Ext.define('PVE.window.Settings', { + extend: 'Ext.window.Window', + + width: '800px', + title: gettext('My Settings'), + iconCls: 'fa fa-gear', + modal: true, + bodyPadding: 10, + resizable: false, + + buttons: [ + { + xtype: 'proxmoxHelpButton', + onlineHelp: 'gui_my_settings', + hidden: false + }, + '->', + { + text: gettext('Close'), + handler: function() { + this.up('window').close(); + } + } + ], + + layout: { + type: 'hbox', + align: 'top' + }, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + var me = this; + var sp = Ext.state.Manager.getProvider(); + + var username = sp.get('login-username') || Proxmox.Utils.noneText; + me.lookupReference('savedUserName').setValue(username); + + var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight']; + settings.forEach(function(setting) { + var val = localStorage.getItem('pve-xterm-' + setting); + if (val !== undefined && val !== null) { + var field = me.lookup(setting); + field.setValue(val); + field.resetOriginalValue(); + } + }); + }, + + set_button_status: function() { + var me = this; + + var form = me.lookup('xtermform'); + var valid = form.isValid(); + var dirty = form.isDirty(); + + var hasvalues = false; + var values = form.getValues(); + Ext.Object.eachValue(values, function(value) { + if (value) { + hasvalues = true; + return false; + } + }); + + me.lookup('xtermsave').setDisabled(!dirty || !valid); + me.lookup('xtermreset').setDisabled(!hasvalues); + }, + + control: { + '#xtermjs form': { + dirtychange: 'set_button_status', + validitychange: 'set_button_status' + }, + '#xtermjs button': { + click: function(button) { + var me = this; + var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight']; + settings.forEach(function(setting) { + var field = me.lookup(setting); + if (button.reference === 'xtermsave') { + var value = field.getValue(); + if (value) { + localStorage.setItem('pve-xterm-' + setting, value); + } else { + localStorage.removeItem('pve-xterm-' + setting); + } + } else if (button.reference === 'xtermreset') { + field.setValue(undefined); + localStorage.removeItem('pve-xterm-' + setting); + } + field.resetOriginalValue(); + }); + me.set_button_status(); + } + }, + 'button[name=reset]': { + click: function () { + var blacklist = ['GuiCap', 'login-username', 'dash-storages']; + var sp = Ext.state.Manager.getProvider(); + var state; + for (state in sp.state) { + if (sp.state.hasOwnProperty(state)) { + if (blacklist.indexOf(state) !== -1) { + continue; + } + + sp.clear(state); + } + } + + window.location.reload(); + } + }, + 'button[name=clear-username]': { + click: function () { + var me = this; + var usernamefield = me.lookupReference('savedUserName'); + var sp = Ext.state.Manager.getProvider(); + + usernamefield.setValue(Proxmox.Utils.noneText); + sp.clear('login-username'); + } + }, + 'grid[reference=dashboard-storages]': { + selectionchange: function(grid, selected) { + var me = this; + var sp = Ext.state.Manager.getProvider(); + + // saves the selected storageids as + // "id1,id2,id3,..." + // or clears the variable + if (selected.length > 0) { + sp.set('dash-storages', + Ext.Array.pluck(selected, 'id').join(',')); + } else { + sp.clear('dash-storages'); + } + }, + afterrender: function(grid) { + var me = grid; + var sp = Ext.state.Manager.getProvider(); + var store = me.getStore(); + var items = []; + me.suspendEvent('selectionchange'); + var storages = sp.get('dash-storages') || ''; + storages.split(',').forEach(function(storage){ + // we have to get the records + // to be able to select them + if (storage !== '') { + var item = store.getById(storage); + if (item) { + items.push(item); + } + } + }); + me.getSelectionModel().select(items); + me.resumeEvent('selectionchange'); + } + } + } + }, + + items: [{ + xtype: 'fieldset', + width: '50%', + title: gettext('Webinterface Settings'), + margin: '5', + layout: { + type: 'vbox', + align: 'left' + }, + defaults: { + width: '100%', + margin: '0 0 10 0' + }, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Dashboard Storages'), + labelAlign: 'left', + labelWidth: '50%' + }, + { + xtype: 'grid', + maxHeight: 150, + reference: 'dashboard-storages', + selModel: { + selType: 'checkboxmodel' + }, + columns: [{ + header: gettext('Name'), + dataIndex: 'storage', + flex: 1 + },{ + header: gettext('Node'), + dataIndex: 'node', + flex: 1 + }], + store: { + type: 'diff', + field: ['type', 'storage', 'id', 'node'], + rstore: PVE.data.ResourceStore, + filters: [{ + property: 'type', + value: 'storage' + }], + sorters: [ 'node','storage'] + } + }, + { + xtype: 'box', + autoEl: { tag: 'hr'} + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Saved User name'), + labelAlign: 'left', + labelWidth: '50%', + stateId: 'login-username', + reference: 'savedUserName', + value: '' + }, + { + xtype: 'button', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + text: gettext('Clear User name'), + width: 'auto', + name: 'clear-username' + }, + { + xtype: 'box', + autoEl: { tag: 'hr'} + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Layout'), + labelAlign: 'left', + labelWidth: '50%' + }, + { + xtype: 'button', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + text: gettext('Reset Layout'), + width: 'auto', + name: 'reset' + } + ] + },{ + xtype: 'fieldset', + itemId: 'xtermjs', + width: '50%', + margin: '5', + title: gettext('xterm.js Settings'), + items: [{ + xtype: 'form', + reference: 'xtermform', + border: false, + layout: { + type: 'vbox', + algin: 'left' + }, + defaults: { + width: '100%', + margin: '0 0 10 0' + }, + items: [ + { + xtype: 'textfield', + name: 'fontFamily', + reference: 'fontFamily', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Font-Family') + }, + { + xtype: 'proxmoxintegerfield', + emptyText: Proxmox.Utils.defaultText, + name: 'fontSize', + reference: 'fontSize', + minValue: 1, + fieldLabel: gettext('Font-Size') + }, + { + xtype: 'numberfield', + name: 'letterSpacing', + reference: 'letterSpacing', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Letter Spacing') + }, + { + xtype: 'numberfield', + name: 'lineHeight', + minValue: 0.1, + reference: 'lineHeight', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Line Height') + }, + { + xtype: 'container', + layout: { + type: 'hbox', + pack: 'end' + }, + items: [ + { + xtype: 'button', + reference: 'xtermreset', + disabled: true, + text: gettext('Reset') + }, + { + xtype: 'button', + reference: 'xtermsave', + disabled: true, + text: gettext('Save') + } + ] + } + ] + }] + }], + + onShow: function() { + var me = this; + me.callParent(); + } +}); +Ext.define('PVE.panel.StartupInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'qm_startup_and_shutdown', + + onGetValues: function(values) { + var me = this; + + var res = PVE.Parser.printStartup(values); + + if (res === undefined || res === '') { + return { 'delete': 'startup' }; + } + + return { startup: res }; + }, + + setStartup: function(value) { + var me = this; + + var startup = PVE.Parser.parseStartup(value); + if (startup) { + me.setValues(startup); + } + }, + + initComponent : function() { + var me = this; + + me.items = [ + { + xtype: 'textfield', + name: 'order', + defaultValue: '', + emptyText: 'any', + fieldLabel: gettext('Start/Shutdown order') + }, + { + xtype: 'textfield', + name: 'up', + defaultValue: '', + emptyText: 'default', + fieldLabel: gettext('Startup delay') + }, + { + xtype: 'textfield', + name: 'down', + defaultValue: '', + emptyText: 'default', + fieldLabel: gettext('Shutdown timeout') + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.window.StartupEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveWindowStartupEdit', + onlineHelp: undefined, + + initComponent : function() { + + var me = this; + var ipanelConfig = me.onlineHelp ? {onlineHelp: me.onlineHelp} : {}; + var ipanel = Ext.create('PVE.panel.StartupInputPanel', ipanelConfig); + + Ext.applyIf(me, { + subject: gettext('Start/Shutdown order'), + fieldDefaults: { + labelWidth: 120 + }, + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var i, confid; + me.vmconfig = response.result.data; + ipanel.setStartup(me.vmconfig.startup); + } + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.ceph.Install', { + extend: 'Ext.window.Window', + xtype: 'pveCephInstallWindow', + mixins: ['Proxmox.Mixin.CBind'], + + width: 220, + header: false, + resizable: false, + draggable: false, + modal: true, + nodename: undefined, + shadow: false, + border: false, + bodyBorder: false, + closable: false, + cls: 'install-mask', + bodyCls: 'install-mask', + layout: { + align: 'stretch', + pack: 'center', + type: 'vbox' + }, + viewModel: { + data: { + cephVersion: 'nautilus', + isInstalled: false + }, + formulas: { + buttonText: function (get){ + if (get('isInstalled')) { + return gettext('Configure Ceph'); + } else { + return gettext('Install Ceph-') + get('cephVersion'); + } + }, + windowText: function (get) { + if (get('isInstalled')) { + return '

' + + Ext.String.format(gettext('{0} is not initialized.'), 'Ceph') + ' '+ + gettext('You need to create a initial config once.') + '

'; + } else { + return '

' + + Ext.String.format(gettext('{0} is not installed on this node.'), 'Ceph') + '
' + + gettext('Would you like to install it now?') + '

'; + } + } + } + }, + items: [ + { + bind: { + html: '{windowText}' + }, + border: false, + padding: 5, + bodyCls: 'install-mask' + + }, + { + xtype: 'button', + bind: { + text: '{buttonText}' + }, + viewModel: {}, + cbind: { + nodename: '{nodename}' + }, + handler: function() { + var me = this.up('pveCephInstallWindow'); + var win = Ext.create('PVE.ceph.CephInstallWizard',{ + nodename: me.nodename + }); + win.getViewModel().set('isInstalled', this.getViewModel().get('isInstalled')); + win.show(); + me.mon(win,'beforeClose', function(){ + me.fireEvent("cephInstallWindowClosed"); + me.close(); + }); + + } + } + ] +}); +/*jslint confusion: true*/ +Ext.define('PVE.FirewallEnableEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveFirewallEnableEdit'], + mixins: ['Proxmox.Mixin.CBind'], + + subject: gettext('Firewall'), + cbindData: { + defaultValue: 0 + }, + width: 350, + + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + uncheckedValue: 0, + cbind: { + defaultValue: '{defaultValue}', + checked: '{defaultValue}' + }, + deleteDefaultValue: false, + fieldLabel: gettext('Firewall') + }, + { + xtype: 'displayfield', + name: 'warning', + userCls: 'pve-hint', + value: gettext('Warning: Firewall still disabled at datacenter level!'), + hidden: true + } + ], + + beforeShow: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/cluster/firewall/options', + method: 'GET', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + if (!response.result.data.enable) { + me.down('displayfield[name=warning]').setVisible(true); + } + } + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.FirewallLograteInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveFirewallLograteInputPanel', + + viewModel: {}, + + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + reference: 'enable', + fieldLabel: gettext('Enable'), + value: true + }, + { + layout: 'hbox', + border: false, + items: [ + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Log rate limit'), + minValue: 1, + maxValue: 99, + allowBlank: false, + flex: 2, + value: 1 + }, + { + xtype: 'box', + html: '
/
' + }, + { + xtype: 'proxmoxKVComboBox', + name: 'unit', + comboItems: [['second', 'second'], ['minute', 'minute'], + ['hour', 'hour'], ['day', 'day']], + allowBlank: false, + flex: 1, + value: 'second' + } + ] + }, + { + xtype: 'numberfield', + name: 'burst', + fieldLabel: gettext('Log burst limit'), + minValue: 1, + maxValue: 99, + value: 5 + } + ], + + onGetValues: function(values) { + var me = this; + + var vals = {}; + vals.enable = values.enable !== undefined ? 1 : 0; + vals.rate = values.rate + '/' + values.unit; + vals.burst = values.burst; + var properties = PVE.Parser.printPropertyString(vals, undefined); + if (properties == '') { + return { 'delete': 'log_ratelimit' }; + } + return { log_ratelimit: properties }; + }, + + setValues: function(values) { + var me = this; + + var properties = {}; + if (values.log_ratelimit !== undefined) { + properties = PVE.Parser.parsePropertyString(values.log_ratelimit, 'enable'); + if (properties.rate) { + var matches = properties.rate.match(/^(\d+)\/(second|minute|hour|day)$/); + if (matches) { + properties.rate = matches[1]; + properties.unit = matches[2]; + } + } + } + me.callParent([properties]); + } +}); + +Ext.define('PVE.FirewallLograteEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveFirewallLograteEdit', + + subject: gettext('Log rate limit'), + + items: [{ + xtype: 'pveFirewallLograteInputPanel' + }], + autoLoad: true +}); +Ext.define('PVE.panel.NotesView', { + extend: 'Ext.panel.Panel', + xtype: 'pveNotesView', + + title: gettext("Notes"), + bodyStyle: 'white-space:pre', + bodyPadding: 10, + scrollable: true, + + tbar: { + itemId: 'tbar', + hidden: true, + items: [ + { + text: gettext('Edit'), + handler: function() { + var me = this.up('panel'); + me.run_editor(); + } + } + ] + }, + + run_editor: function() { + var me = this; + var win = Ext.create('PVE.window.NotesEdit', { + pveSelNode: me.pveSelNode, + url: me.url + }); + win.show(); + win.on('destroy', me.load, me); + }, + + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + }, + success: function(response, opts) { + var data = response.result.data.description || ''; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + listeners: { + render: function(c) { + var me = this; + me.getEl().on('dblclick', me.run_editor, me); + } + }, + + tools: [{ + type: 'gear', + handler: function() { + var me = this.up('panel'); + me.run_editor(); + } + }], + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var type = me.pveSelNode.data.type; + if (!Ext.Array.contains(['node', 'qemu', 'lxc'], type)) { + throw 'invalid type specified'; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid && type !== 'node') { + throw "no VM ID specified"; + } + + me.url = '/api2/extjs/nodes/' + nodename + '/'; + + // add the type specific path if qemu/lxc + if (type === 'qemu' || type === 'lxc') { + me.url += type + '/' + vmid + '/'; + } + + me.url += 'config'; + + me.callParent(); + if (type === 'node') { + me.down('#tbar').setVisible(true); + } + me.load(); + } +}); +Ext.define('PVE.grid.ResourceGrid', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveResourceGrid'], + + border: false, + defaultSorter: { + property: 'type', + direction: 'ASC' + }, + initComponent : function() { + var me = this; + + var rstore = PVE.data.ResourceStore; + var sp = Ext.state.Manager.getProvider(); + + var coldef = rstore.defaultColumns(); + + var store = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: me.defaultSorter, + proxy: { type: 'memory' } + }); + + var textfilter = ''; + + var textfilter_match = function(item) { + var match = false; + Ext.each(['name', 'storage', 'node', 'type', 'text'], function(field) { + var v = item.data[field]; + if (v !== undefined) { + v = v.toLowerCase(); + if (v.indexOf(textfilter) >= 0) { + match = true; + return false; + } + } + }); + return match; + }; + + var updateGrid = function() { + + var filterfn = me.viewFilter ? me.viewFilter.filterfn : null; + + //console.log("START GRID UPDATE " + me.viewFilter); + + store.suspendEvents(); + + var nodeidx = {}; + var gather_child_nodes = function(cn) { + if (!cn) { + return; + } + var cs = cn.childNodes; + if (!cs) { + return; + } + var len = cs.length, i = 0, n, res; + + for (; i < len; i++) { + var child = cs[i]; + var orgnode = rstore.data.get(child.data.id); + if (orgnode) { + if ((!filterfn || filterfn(child)) && + (!textfilter || textfilter_match(child))) { + nodeidx[child.data.id] = orgnode; + } + } + gather_child_nodes(child); + } + }; + gather_child_nodes(me.pveSelNode); + + // remove vanished items + var rmlist = []; + store.each(function(olditem) { + var item = nodeidx[olditem.data.id]; + if (!item) { + //console.log("GRID REM UID: " + olditem.data.id); + rmlist.push(olditem); + } + }); + + if (rmlist.length) { + store.remove(rmlist); + } + + // add new items + var addlist = []; + var key; + for (key in nodeidx) { + if (nodeidx.hasOwnProperty(key)) { + var item = nodeidx[key]; + + // getById() use find(), which is slow (ExtJS4 DP5) + //var olditem = store.getById(item.data.id); + var olditem = store.data.get(item.data.id); + + if (!olditem) { + //console.log("GRID ADD UID: " + item.data.id); + var info = Ext.apply({}, item.data); + var child = Ext.create(store.model, info); + addlist.push(item); + continue; + } + // try to detect changes + var changes = false; + var fieldkeys = PVE.data.ResourceStore.fieldNames; + var fieldcount = fieldkeys.length; + var fieldind; + for (fieldind = 0; fieldind < fieldcount; fieldind++) { + var field = fieldkeys[fieldind]; + if (field != 'id' && item.data[field] != olditem.data[field]) { + changes = true; + //console.log("changed item " + item.id + " " + field + " " + item.data[field] + " != " + olditem.data[field]); + olditem.beginEdit(); + olditem.set(field, item.data[field]); + } + } + if (changes) { + olditem.endEdit(true); + olditem.commit(true); + } + } + } + + if (addlist.length) { + store.add(addlist); + } + + store.sort(); + + store.resumeEvents(); + + store.fireEvent('refresh', store); + + //console.log("END GRID UPDATE"); + }; + + var filter_task = new Ext.util.DelayedTask(function(){ + updateGrid(); + }); + + var load_cb = function() { + updateGrid(); + }; + + Ext.apply(me, { + store: store, + stateful: true, + stateId: 'grid-resource', + tbar: [ + '->', + gettext('Search') + ':', ' ', + { + xtype: 'textfield', + width: 200, + value: textfilter, + enableKeyEvents: true, + listeners: { + keyup: function(field, e) { + var v = field.getValue(); + textfilter = v.toLowerCase(); + filter_task.delay(500); + } + } + } + ], + viewConfig: { + stripeRows: true + }, + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + itemdblclick: function(v, record) { + var ws = me.up('pveStdWorkspace'); + ws.selectById(record.data.id); + }, + destroy: function() { + rstore.un("load", load_cb); + } + }, + columns: coldef + }); + me.callParent(); + updateGrid(); + rstore.on("load", load_cb); + } +}); +Ext.define('PVE.pool.AddVM', { + extend: 'Proxmox.window.Edit', + width: 600, + height: 400, + isAdd: true, + isCreate: true, + initComponent : function() { + + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + me.url = "/pools/" + me.pool; + me.method = 'PUT'; + + var vmsField = Ext.create('Ext.form.field.Text', { + name: 'vms', + hidden: true, + allowBlank: false + }); + + var vmStore = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: [ + { + property: 'vmid', + order: 'ASC' + } + ], + filters: [ + function(item) { + return ((item.data.type === 'lxc' || item.data.type === 'qemu') && item.data.pool === ''); + } + ] + }); + + var vmGrid = Ext.create('widget.grid',{ + store: vmStore, + border: true, + height: 300, + scrollable: true, + selModel: { + selType: 'checkboxmodel', + mode: 'SIMPLE', + listeners: { + selectionchange: function(model, selected, opts) { + var selectedVms = []; + selected.forEach(function(vm) { + selectedVms.push(vm.data.vmid); + }); + vmsField.setValue(selectedVms); + } + } + }, + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 60 + }, + { + header: gettext('Node'), + dataIndex: 'node' + }, + { + header: gettext('Status'), + dataIndex: 'uptime', + renderer: function(value) { + if (value) { + return Proxmox.Utils.runningText; + } else { + return Proxmox.Utils.stoppedText; + } + } + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + header: gettext('Type'), + dataIndex: 'type' + } + ] + }); + Ext.apply(me, { + subject: gettext('Virtual Machine'), + items: [ vmsField, vmGrid ] + }); + + me.callParent(); + vmStore.load(); + } +}); + +Ext.define('PVE.pool.AddStorage', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + me.isCreate = true; + me.isAdd = true; + me.url = "/pools/" + me.pool; + me.method = 'PUT'; + + Ext.apply(me, { + subject: gettext('Storage'), + width: 350, + items: [ + { + xtype: 'pveStorageSelector', + name: 'storage', + nodename: 'localhost', + autoSelect: false, + value: '', + fieldLabel: gettext("Storage") + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.grid.PoolMembers', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pvePoolMembers'], + + // fixme: dynamic status update ? + + stateful: true, + stateId: 'grid-pool-members', + + initComponent : function() { + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + var store = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: [ + { + property : 'type', + direction: 'ASC' + } + ], + proxy: { + type: 'proxmox', + root: 'data.members', + url: "/api2/json/pools/" + me.pool + } + }); + + var coldef = PVE.data.ResourceStore.defaultColumns(); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: function (rec) { + return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.id + "'"); + }, + handler: function(btn, event, rec) { + var params = { 'delete': 1 }; + if (rec.data.type === 'storage') { + params.storage = rec.data.storage; + } else if (rec.data.type === 'qemu' || rec.data.type === 'lxc' || rec.data.type === 'openvz') { + params.vms = rec.data.vmid; + } else { + throw "unknown resource type"; + } + + Proxmox.Utils.API2Request({ + url: '/pools/' + me.pool, + method: 'PUT', + params: params, + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Virtual Machine'), + iconCls: 'pve-itype-icon-qemu', + handler: function() { + var win = Ext.create('PVE.pool.AddVM', { pool: me.pool }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('Storage'), + iconCls: 'pve-itype-icon-storage', + handler: function() { + var win = Ext.create('PVE.pool.AddStorage', { pool: me.pool }); + win.on('destroy', reload); + win.show(); + } + } + ] + }) + }, + remove_btn + ], + viewConfig: { + stripeRows: true + }, + columns: coldef, + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + itemdblclick: function(v, record) { + var ws = me.up('pveStdWorkspace'); + ws.selectById(record.data.id); + }, + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.form.FWMacroSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveFWMacroSelector', + allowBlank: true, + autoSelect: false, + valueField: 'macro', + displayField: 'macro', + listConfig: { + columns: [ + { + header: gettext('Macro'), + dataIndex: 'macro', + hideable: false, + width: 100 + }, + { + header: gettext('Description'), + renderer: Ext.String.htmlEncode, + flex: 1, + dataIndex: 'descr' + } + ] + }, + initComponent: function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: [ 'macro', 'descr' ], + idProperty: 'macro', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/firewall/macros" + }, + sorters: { + property: 'macro', + order: 'DESC' + } + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.FirewallRulePanel', { + extend: 'Proxmox.panel.InputPanel', + + allow_iface: false, + + list_refs_url: undefined, + + onGetValues: function(values) { + var me = this; + + // hack: editable ComboGrid returns nothing when empty, so we need to set '' + // Also, disabled text fields return nothing, so we need to set '' + + Ext.Array.each(['source', 'dest', 'macro', 'proto', 'sport', 'dport', 'log'], function(key) { + if (values[key] === undefined) { + values[key] = ''; + } + }); + + delete values.modified_marker; + + return values; + }, + + initComponent : function() { + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + me.column1 = [ + { + // hack: we use this field to mark the form 'dirty' when the + // record has errors- so that the user can safe the unmodified + // form again. + xtype: 'hiddenfield', + name: 'modified_marker', + value: '' + }, + { + xtype: 'proxmoxKVComboBox', + name: 'type', + value: 'in', + comboItems: [['in', 'in'], ['out', 'out']], + fieldLabel: gettext('Direction'), + allowBlank: false + }, + { + xtype: 'proxmoxKVComboBox', + name: 'action', + value: 'ACCEPT', + comboItems: [['ACCEPT', 'ACCEPT'], ['DROP', 'DROP'], ['REJECT', 'REJECT']], + fieldLabel: gettext('Action'), + allowBlank: false + } + ]; + + if (me.allow_iface) { + me.column1.push({ + xtype: 'proxmoxtextfield', + name: 'iface', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('Interface') + }); + } else { + me.column1.push({ + xtype: 'displayfield', + fieldLabel: '', + value: '' + }); + } + + me.column1.push( + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '' + }, + { + xtype: 'pveIPRefSelector', + name: 'source', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('Source') + + }, + { + xtype: 'pveIPRefSelector', + name: 'dest', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('Destination') + } + ); + + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Enable') + }, + { + xtype: 'pveFWMacroSelector', + name: 'macro', + fieldLabel: gettext('Macro'), + editable: true, + allowBlank: true, + listeners: { + change: function(f, value) { + if (value === null) { + me.down('field[name=proto]').setDisabled(false); + me.down('field[name=sport]').setDisabled(false); + me.down('field[name=dport]').setDisabled(false); + } else { + me.down('field[name=proto]').setDisabled(true); + me.down('field[name=proto]').setValue(''); + me.down('field[name=sport]').setDisabled(true); + me.down('field[name=sport]').setValue(''); + me.down('field[name=dport]').setDisabled(true); + me.down('field[name=dport]').setValue(''); + } + } + } + }, + { + xtype: 'pveIPProtocolSelector', + name: 'proto', + autoSelect: false, + editable: true, + value: '', + fieldLabel: gettext('Protocol') + }, + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '' + }, + { + xtype: 'textfield', + name: 'sport', + value: '', + fieldLabel: gettext('Source port') + }, + { + xtype: 'textfield', + name: 'dport', + value: '', + fieldLabel: gettext('Dest. port') + } + ]; + + me.advancedColumn1 = [ + { + xtype: 'pveFirewallLogLevels' + } + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.FirewallRuleEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + list_refs_url: undefined, + + allow_iface: false, + + initComponent : function() { + + var me = this; + + if (!me.base_url) { + throw "no base_url specified"; + } + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + me.isCreate = (me.rule_pos === undefined); + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString(); + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.FirewallRulePanel', { + isCreate: me.isCreate, + list_refs_url: me.list_refs_url, + allow_iface: me.allow_iface, + rule_pos: me.rule_pos + }); + + Ext.apply(me, { + subject: gettext('Rule'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + if (values.errors) { + var field = me.query('[isFormField][name=modified_marker]')[0]; + field.setValue(1); + Ext.Function.defer(function() { + var form = ipanel.up('form').getForm(); + form.markInvalid(values.errors); + }, 100); + } + } + }); + } else if (me.rec) { + ipanel.setValues(me.rec.data); + } + } +}); + +Ext.define('PVE.FirewallGroupRuleEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + + allow_iface: false, + + initComponent : function() { + + var me = this; + + me.isCreate = (me.rule_pos === undefined); + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString(); + me.method = 'PUT'; + } + + var column1 = [ + { + xtype: 'hiddenfield', + name: 'type', + value: 'group' + }, + { + xtype: 'pveSecurityGroupsSelector', + name: 'action', + value: '', + fieldLabel: gettext('Security Group'), + allowBlank: false + } + ]; + + if (me.allow_iface) { + column1.push({ + xtype: 'proxmoxtextfield', + name: 'iface', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('Interface') + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + column1: column1, + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Enable') + } + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + + Ext.apply(me, { + subject: gettext('Rule'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('PVE.FirewallRules', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveFirewallRules', + + onlineHelp: 'chapter_pve_firewall', + + stateful: true, + stateId: 'grid-firewall-rules', + + base_url: undefined, + list_refs_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + groupBtn: undefined, + + tbar_prefix: undefined, + + allow_groups: true, + allow_iface: false, + + setBaseUrl: function(url) { + var me = this; + + me.base_url = url; + + if (url === undefined) { + me.addBtn.setDisabled(true); + if (me.groupBtn) { + me.groupBtn.setDisabled(true); + } + me.store.removeAll(); + } else { + me.addBtn.setDisabled(false); + me.removeBtn.baseurl = url + '/'; + if (me.groupBtn) { + me.groupBtn.setDisabled(false); + } + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json' + url + }); + + me.store.load(); + } + }, + + moveRule: function(from, to) { + var me = this; + + if (!me.base_url) { + return; + } + + Proxmox.Utils.API2Request({ + url: me.base_url + "/" + from, + method: 'PUT', + params: { moveto: to }, + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + } + }); + }, + + updateRule: function(rule) { + var me = this; + + if (!me.base_url) { + return; + } + + rule.enable = rule.enable ? 1 : 0; + + var pos = rule.pos; + delete rule.pos; + delete rule.errors; + + Proxmox.Utils.API2Request({ + url: me.base_url + '/' + pos.toString(), + method: 'PUT', + params: rule, + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + } + }); + }, + + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + var store = Ext.create('Ext.data.Store',{ + model: 'pve-fw-rule' + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var type = rec.data.type; + + var editor; + if (type === 'in' || type === 'out') { + editor = 'PVE.FirewallRuleEdit'; + } else if (type === 'group') { + editor = 'PVE.FirewallGroupRuleEdit'; + } else { + return; + } + + var win = Ext.create(editor, { + digest: rec.data.digest, + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url, + rule_pos: rec.data.pos + }); + + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = Ext.create('Proxmox.button.Button',{ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = Ext.create('Ext.Button', { + text: gettext('Add'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.FirewallRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url + }); + win.on('destroy', reload); + win.show(); + } + }); + + var run_copy_editor = function() { + var rec = sm.getSelection()[0]; + + if (!rec) { + return; + } + var type = rec.data.type; + + + if (!(type === 'in' || type === 'out')) { + return; + } + + var win = Ext.create('PVE.FirewallRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url, + rec: rec + }); + + win.show(); + win.on('destroy', reload); + }; + + me.copyBtn = Ext.create('Proxmox.button.Button',{ + text: gettext('Copy'), + selModel: sm, + enableFn: function(rec) { + return (rec.data.type === 'in' || rec.data.type === 'out'); + }, + disabled: true, + handler: run_copy_editor + }); + + if (me.allow_groups) { + me.groupBtn = Ext.create('Ext.Button', { + text: gettext('Insert') + ': ' + + gettext('Security Group'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.FirewallGroupRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url + }); + win.on('destroy', reload); + win.show(); + } + }); + } + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton',{ + selModel: sm, + baseurl: me.base_url + '/', + confirmMsg: false, + getRecordName: function(rec) { + var rule = rec.data; + return rule.pos.toString() + + '?digest=' + encodeURIComponent(rule.digest); + }, + callback: function() { + me.store.load(); + } + }); + + var tbar = me.tbar_prefix ? [ me.tbar_prefix ] : []; + tbar.push(me.addBtn, me.copyBtn); + if (me.groupBtn) { + tbar.push(me.groupBtn); + } + tbar.push(me.removeBtn, me.editBtn); + + var render_errors = function(name, value, metaData, record) { + var errors = record.data.errors; + if (errors && errors[name]) { + metaData.tdCls = 'proxmox-invalid-row'; + var html = '

' + Ext.htmlEncode(errors[name]) + '

'; + metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + + html.replace(/\"/g,'"') + '"'; + } + return value; + }; + + var columns = [ + { + // similar to xtype: 'rownumberer', + dataIndex: 'pos', + resizable: false, + width: 23, + sortable: false, + align: 'right', + hideable: false, + menuDisabled: true, + renderer: function(value, metaData, record, rowIdx, colIdx, store) { + metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special'; + if (value >= 0) { + return value; + } + return ''; + } + }, + { + xtype: 'checkcolumn', + header: gettext('Enable'), + dataIndex: 'enable', + listeners: { + checkchange: function(column, recordIndex, checked) { + var record = me.getStore().getData().items[recordIndex]; + record.commit(); + var data = {}; + Ext.Array.forEach(record.getFields(), function(field) { + data[field.name] = record.get(field.name); + }); + if (!me.allow_iface || !data.iface) { + delete data.iface; + } + me.updateRule(data); + } + }, + width: 50 + }, + { + header: gettext('Type'), + dataIndex: 'type', + renderer: function(value, metaData, record) { + return render_errors('type', value, metaData, record); + }, + width: 50 + }, + { + header: gettext('Action'), + dataIndex: 'action', + renderer: function(value, metaData, record) { + return render_errors('action', value, metaData, record); + }, + width: 80 + }, + { + header: gettext('Macro'), + dataIndex: 'macro', + renderer: function(value, metaData, record) { + return render_errors('macro', value, metaData, record); + }, + width: 80 + } + ]; + + if (me.allow_iface) { + columns.push({ + header: gettext('Interface'), + dataIndex: 'iface', + renderer: function(value, metaData, record) { + return render_errors('iface', value, metaData, record); + }, + width: 80 + }); + } + + columns.push( + { + header: gettext('Source'), + dataIndex: 'source', + renderer: function(value, metaData, record) { + return render_errors('source', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Destination'), + dataIndex: 'dest', + renderer: function(value, metaData, record) { + return render_errors('dest', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Protocol'), + dataIndex: 'proto', + renderer: function(value, metaData, record) { + return render_errors('proto', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Dest. port'), + dataIndex: 'dport', + renderer: function(value, metaData, record) { + return render_errors('dport', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Source port'), + dataIndex: 'sport', + renderer: function(value, metaData, record) { + return render_errors('sport', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Log level'), + dataIndex: 'log', + renderer: function(value, metaData, record) { + return render_errors('log', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + flex: 1, + renderer: function(value, metaData, record) { + return render_errors('comment', Ext.util.Format.htmlEncode(value), metaData, record); + } + } + ); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + plugins: [ + { + ptype: 'gridviewdragdrop', + dragGroup: 'FWRuleDDGroup', + dropGroup: 'FWRuleDDGroup' + } + ], + listeners: { + beforedrop: function(node, data, dropRec, dropPosition) { + if (!dropRec) { + return false; // empty view + } + var moveto = dropRec.get('pos'); + if (dropPosition === 'after') { + moveto++; + } + var pos = data.records[0].get('pos'); + me.moveRule(pos, moveto); + return 0; + }, + itemdblclick: run_editor + } + }, + sortableColumns: false, + columns: columns + }); + + me.callParent(); + + if (me.base_url) { + me.setBaseUrl(me.base_url); // load + } + } +}, function() { + + Ext.define('pve-fw-rule', { + extend: 'Ext.data.Model', + fields: [ { name: 'enable', type: 'boolean' }, + 'type', 'action', 'macro', 'source', 'dest', 'proto', 'iface', + 'dport', 'sport', 'comment', 'pos', 'digest', 'errors' ], + idProperty: 'pos' + }); + +}); +Ext.define('PVE.FirewallAliasEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + + alias_name: undefined, + + initComponent : function() { + + var me = this; + + me.isCreate = (me.alias_name === undefined); + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.alias_name; + me.method = 'PUT'; + } + + var items = [ + { + xtype: 'textfield', + name: me.isCreate ? 'name' : 'rename', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'cidr', + fieldLabel: gettext('IP/CIDR'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + } + ]; + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + items: items + }); + + Ext.apply(me, { + subject: gettext('Alias'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + values.rename = values.name; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('pve-fw-aliases', { + extend: 'Ext.data.Model', + + fields: [ 'name', 'cidr', 'comment', 'digest' ], + idProperty: 'name' +}); + +Ext.define('PVE.FirewallAliases', { + extend: 'Ext.grid.Panel', + alias: ['widget.pveFirewallAliases'], + + onlineHelp: 'pve_firewall_ip_aliases', + + stateful: true, + stateId: 'grid-firewall-aliases', + + base_url: undefined, + + title: gettext('Alias'), + + initComponent : function() { + + var me = this; + + if (!me.base_url) { + throw "missing base_url configuration"; + } + + var store = new Ext.data.Store({ + model: 'pve-fw-aliases', + proxy: { + type: 'proxmox', + url: "/api2/json" + me.base_url + }, + sorters: { + property: 'name', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + var oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('name', oldrec.data.name); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.FirewallAliasEdit', { + base_url: me.base_url, + alias_name: rec.data.name + }); + + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = Ext.create('Ext.Button', { + text: gettext('Add'), + handler: function() { + var win = Ext.create('PVE.FirewallAliasEdit', { + base_url: me.base_url + }); + win.on('destroy', reload); + win.show(); + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload + }); + + + Ext.apply(me, { + store: store, + tbar: [ me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: gettext('Name'), dataIndex: 'name', width: 100 }, + { header: gettext('IP/CIDR'), dataIndex: 'cidr', width: 100 }, + { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 } + ], + listeners: { + itemdblclick: run_editor + } + }); + + me.callParent(); + me.on('activate', reload); + } +}); +Ext.define('PVE.FirewallOptions', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveFirewallOptions'], + + fwtype: undefined, // 'dc', 'node' or 'vm' + + base_url: undefined, + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + if (!me.base_url) { + throw "missing base_url configuration"; + } + + if (me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm') { + if (me.fwtype === 'node') { + me.cwidth1 = 250; + } + } else { + throw "unknown firewall option type"; + } + + me.rows = {}; + + var add_boolean_row = function(name, text, defaultValue) { + me.add_boolean_row(name, text, { defaultValue: defaultValue }); + }; + var add_integer_row = function(name, text, minValue, labelWidth) { + me.add_integer_row(name, text, { + minValue: minValue, + deleteEmpty: true, + labelWidth: labelWidth, + renderer: function(value) { + if (value === undefined) { + return Proxmox.Utils.defaultText; + } + + return value; + } + }); + }; + + var add_log_row = function(name, labelWidth) { + me.rows[name] = { + header: name, + required: true, + defaultValue: 'nolog', + editor: { + xtype: 'proxmoxWindowEdit', + subject: name, + fieldDefaults: { labelWidth: labelWidth || 100 }, + items: { + xtype: 'pveFirewallLogLevels', + name: name, + fieldLabel: name + } + } + }; + }; + + if (me.fwtype === 'node') { + me.rows.enable = { + required: true, + defaultValue: 1, + header: gettext('Firewall'), + renderer: Proxmox.Utils.format_boolean, + editor: { + xtype: 'pveFirewallEnableEdit', + defaultValue: 1 + } + }; + add_boolean_row('nosmurfs', gettext('SMURFS filter'), 1); + add_boolean_row('tcpflags', gettext('TCP flags filter'), 0); + add_boolean_row('ndp', 'NDP', 1); + add_integer_row('nf_conntrack_max', 'nf_conntrack_max', 32768, 120); + add_integer_row('nf_conntrack_tcp_timeout_established', + 'nf_conntrack_tcp_timeout_established', 7875, 250); + add_log_row('log_level_in'); + add_log_row('log_level_out'); + add_log_row('tcp_flags_log_level', 120); + add_log_row('smurf_log_level'); + } else if (me.fwtype === 'vm') { + me.rows.enable = { + required: true, + defaultValue: 0, + header: gettext('Firewall'), + renderer: Proxmox.Utils.format_boolean, + editor: { + xtype: 'pveFirewallEnableEdit', + defaultValue: 0 + } + }; + add_boolean_row('dhcp', 'DHCP', 1); + add_boolean_row('ndp', 'NDP', 1); + add_boolean_row('radv', gettext('Router Advertisement'), 0); + add_boolean_row('macfilter', gettext('MAC filter'), 1); + add_boolean_row('ipfilter', gettext('IP filter'), 0); + add_log_row('log_level_in'); + add_log_row('log_level_out'); + } else if (me.fwtype === 'dc') { + add_boolean_row('enable', gettext('Firewall'), 0); + add_boolean_row('ebtables', 'ebtables', 1); + me.rows.log_ratelimit = { + header: gettext('Log rate limit'), + required: true, + defaultValue: gettext('Default') + ' (enable=1,rate1/second,burst=5)', + editor: { + xtype: 'pveFirewallLograteEdit', + defaultValue: 'enable=1' + } + }; + } + + if (me.fwtype === 'dc' || me.fwtype === 'vm') { + me.rows.policy_in = { + header: gettext('Input Policy'), + required: true, + defaultValue: 'DROP', + editor: { + xtype: 'proxmoxWindowEdit', + subject: gettext('Input Policy'), + items: { + xtype: 'pveFirewallPolicySelector', + name: 'policy_in', + value: 'DROP', + fieldLabel: gettext('Input Policy') + } + } + }; + + me.rows.policy_out = { + header: gettext('Output Policy'), + required: true, + defaultValue: 'ACCEPT', + editor: { + xtype: 'proxmoxWindowEdit', + subject: gettext('Output Policy'), + items: { + xtype: 'pveFirewallPolicySelector', + name: 'policy_out', + value: 'ACCEPT', + fieldLabel: gettext('Output Policy') + } + } + }; + } + + var edit_btn = new Ext.Button({ + text: gettext('Edit'), + disabled: true, + handler: function() { me.run_editor(); } + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + var rowdef = me.rows[rec.data.key]; + edit_btn.setDisabled(!rowdef.editor); + }; + + Ext.apply(me, { + url: "/api2/json" + me.base_url, + tbar: [ edit_btn ], + editorConfig: { + url: '/api2/extjs/' + me.base_url + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + } +}); + + +Ext.define('PVE.FirewallLogLevels', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveFirewallLogLevels'], + + name: 'log', + fieldLabel: gettext('Log level'), + value: 'nolog', + comboItems: [['nolog', 'nolog'], ['emerg', 'emerg'], ['alert', 'alert'], + ['crit', 'crit'], ['err', 'err'], ['warning', 'warning'], + ['notice', 'notice'], ['info', 'info'], ['debug', 'debug']] +}); +/* + * Left Treepanel, containing all the resources we manage in this datacenter: server nodes, server storages, VMs and Containers + */ +Ext.define('PVE.tree.ResourceTree', { + extend: 'Ext.tree.TreePanel', + alias: ['widget.pveResourceTree'], + + statics: { + typeDefaults: { + node: { + iconCls: 'fa fa-building', + text: gettext('Nodes') + }, + pool: { + iconCls: 'fa fa-tags', + text: gettext('Resource Pool') + }, + storage: { + iconCls: 'fa fa-database', + text: gettext('Storage') + }, + qemu: { + iconCls: 'fa fa-desktop', + text: gettext('Virtual Machine') + }, + lxc: { + //iconCls: 'x-tree-node-lxc', + iconCls: 'fa fa-cube', + text: gettext('LXC Container') + }, + template: { + iconCls: 'fa fa-file-o' + } + } + }, + + useArrows: true, + + // private + nodeSortFn: function(node1, node2) { + var n1 = node1.data; + var n2 = node2.data; + + if ((n1.groupbyid && n2.groupbyid) || + !(n1.groupbyid || n2.groupbyid)) { + + var tcmp; + + var v1 = n1.type; + var v2 = n2.type; + + if ((tcmp = v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)) != 0) { + return tcmp; + } + + // numeric compare for VM IDs + // sort templates after regular VMs + if (v1 === 'qemu' || v1 === 'lxc') { + if (n1.template && !n2.template) { + return 1; + } else if (n2.template && !n1.template) { + return -1; + } + v1 = n1.vmid; + v2 = n2.vmid; + if ((tcmp = v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)) != 0) { + return tcmp; + } + } + + return n1.id > n2.id ? 1 : (n1.id < n2.id ? -1 : 0); + } else if (n1.groupbyid) { + return -1; + } else if (n2.groupbyid) { + return 1; + } + }, + + // private: fast binary search + findInsertIndex: function(node, child, start, end) { + var me = this; + + var diff = end - start; + + var mid = start + (diff>>1); + + if (diff <= 0) { + return start; + } + + var res = me.nodeSortFn(child, node.childNodes[mid]); + if (res <= 0) { + return me.findInsertIndex(node, child, start, mid); + } else { + return me.findInsertIndex(node, child, mid + 1, end); + } + }, + + setIconCls: function(info) { + var me = this; + + var cls = PVE.Utils.get_object_icon_class(info.type, info); + + if (cls !== '') { + info.iconCls = cls; + } + }, + + // add additional elements to text + // at the moment only the usage indicator for storages + setText: function(info) { + var me = this; + + var status = ''; + if (info.type === 'storage') { + var maxdisk = info.maxdisk; + var disk = info.disk; + var usage = disk/maxdisk; + var cls = ''; + if (usage <= 1.0 && usage >= 0.0) { + var height = (usage*100).toFixed(0); + var neg_height = (100-usage*100).toFixed(0); + status = '
'; + status += '
'; + status += '
'; + status += '
'; + } + } + + info.text = status + info.text; + }, + + setToolTip: function(info) { + if (info.type === 'pool' || info.groupbyid !== undefined) { + return; + } + + var qtips = [gettext('Status') + ': ' + (info.qmpstatus || info.status)]; + if (info.lock) { + qtips.push('Config locked (' + info.lock + ')'); + } + if (info.hastate != 'unmanaged') { + qtips.push(gettext('HA State') + ": " + info.hastate); + } + + info.qtip = qtips.join(', '); + }, + + // private + addChildSorted: function(node, info) { + var me = this; + + me.setIconCls(info); + me.setText(info); + me.setToolTip(info); + + var defaults; + if (info.groupbyid) { + info.text = info.groupbyid; + if (info.type === 'type') { + defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid]; + if (defaults && defaults.text) { + info.text = defaults.text; + } + } + } + var child = Ext.create('PVETree', info); + + var cs = node.childNodes; + var pos; + if (cs) { + pos = cs[me.findInsertIndex(node, child, 0, cs.length)]; + } + + node.insertBefore(child, pos); + + return child; + }, + + // private + groupChild: function(node, info, groups, level) { + var me = this; + + var groupby = groups[level]; + var v = info[groupby]; + + if (v) { + var group = node.findChild('groupbyid', v); + if (!group) { + var groupinfo; + if (info.type === groupby) { + groupinfo = info; + } else { + groupinfo = { + type: groupby, + id : groupby + "/" + v + }; + if (groupby !== 'type') { + groupinfo[groupby] = v; + } + } + groupinfo.leaf = false; + groupinfo.groupbyid = v; + group = me.addChildSorted(node, groupinfo); + } + if (info.type === groupby) { + return group; + } + if (group) { + return me.groupChild(group, info, groups, level + 1); + } + } + + return me.addChildSorted(node, info); + }, + + initComponent : function() { + var me = this; + + var rstore = PVE.data.ResourceStore; + var sp = Ext.state.Manager.getProvider(); + + if (!me.viewFilter) { + me.viewFilter = {}; + } + + var pdata = { + dataIndex: {}, + updateCount: 0 + }; + + var store = Ext.create('Ext.data.TreeStore', { + model: 'PVETree', + root: { + expanded: true, + id: 'root', + text: gettext('Datacenter'), + iconCls: 'fa fa-server' + } + }); + + var stateid = 'rid'; + + var updateTree = function() { + var tmp; + + store.suspendEvents(); + + var rootnode = me.store.getRootNode(); + // remember selected node (and all parents) + var sm = me.getSelectionModel(); + + var lastsel = sm.getSelection()[0]; + var reselect = false; + var parents = []; + var p = lastsel; + while (p && !!(p = p.parentNode)) { + parents.push(p); + } + + var index = pdata.dataIndex; + + var groups = me.viewFilter.groups || []; + var filterfn = me.viewFilter.filterfn; + + // remove vanished or moved items + // update in place changed items + var key; + for (key in index) { + if (index.hasOwnProperty(key)) { + var olditem = index[key]; + + // getById() use find(), which is slow (ExtJS4 DP5) + //var item = rstore.getById(olditem.data.id); + var item = rstore.data.get(olditem.data.id); + + var changed = false; + var moved = false; + if (item) { + // test if any grouping attributes changed + // this will also catch migrated nodes + // in server view + var i, len; + for (i = 0, len = groups.length; i < len; i++) { + var attr = groups[i]; + if (item.data[attr] != olditem.data[attr]) { + //console.log("changed " + attr); + moved = true; + break; + } + } + + // explicitly check for node, since + // in some views, node is not a grouping + // attribute + if (!moved && item.data.node !== olditem.data.node) { + moved = true; + } + + // tree item has been updated + var fields = [ + 'text', 'running', 'template', 'status', + 'qmpstatus', 'hastate', 'lock' + ]; + + var field; + for (i = 0; i < fields.length; i++) { + field = fields[i]; + if (item.data[field] !== olditem.data[field]) { + changed = true; + break; + } + } + + // fixme: also test filterfn()? + } + + if (changed) { + olditem.beginEdit(); + //console.log("REM UPDATE UID: " + key + " ITEM " + item.data.running); + var info = olditem.data; + Ext.apply(info, item.data); + me.setIconCls(info); + me.setText(info); + me.setToolTip(info); + olditem.commit(); + } + if ((!item || moved) && olditem.isLeaf()) { + //console.log("REM UID: " + key + " ITEM " + olditem.data.id); + delete index[key]; + var parentNode = olditem.parentNode; + // when the selected item disappears, + // we have to deselect it here, and reselect it + // later + if (lastsel && olditem.data.id === lastsel.data.id) { + reselect = true; + sm.deselect(olditem); + } + // since the store events are suspended, we + // manually remove the item from the store also + store.remove(olditem); + parentNode.removeChild(olditem, true); + } + } + } + + // add new items + rstore.each(function(item) { + var olditem = index[item.data.id]; + if (olditem) { + return; + } + + if (filterfn && !filterfn(item)) { + return; + } + + //console.log("ADD UID: " + item.data.id); + + var info = Ext.apply({ leaf: true }, item.data); + + var child = me.groupChild(rootnode, info, groups, 0); + if (child) { + index[item.data.id] = child; + } + }); + + store.resumeEvents(); + store.fireEvent('refresh', store); + + // select parent node is selection vanished + if (lastsel && !rootnode.findChild('id', lastsel.data.id, true)) { + lastsel = rootnode; + while (!!(p = parents.shift())) { + if (!!(tmp = rootnode.findChild('id', p.data.id, true))) { + lastsel = tmp; + break; + } + } + me.selectById(lastsel.data.id); + } else if (lastsel && reselect) { + me.selectById(lastsel.data.id); + } + + // on first tree load set the selection from the stateful provider + if (!pdata.updateCount) { + rootnode.expand(); + me.applyState(sp.get(stateid)); + } + + pdata.updateCount++; + }; + + var statechange = function(sp, key, value) { + if (key === stateid) { + me.applyState(value); + } + }; + + sp.on('statechange', statechange); + + Ext.apply(me, { + allowSelection: true, + store: store, + viewConfig: { + // note: animate cause problems with applyState + animate: false + }, + //useArrows: true, + //rootVisible: false, + //title: 'Resource Tree', + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + destroy: function() { + rstore.un("load", updateTree); + }, + beforecellmousedown: function (tree, td, cellIndex, record, tr, rowIndex, ev) { + var sm = me.getSelectionModel(); + // disable selection when right clicking + // except the record is already selected + me.allowSelection = (ev.button !== 2) || sm.isSelected(record); + }, + beforeselect: function (tree, record, index, eopts) { + var allow = me.allowSelection; + me.allowSelection = true; + return allow; + }, + itemdblclick: PVE.Utils.openTreeConsole + }, + setViewFilter: function(view) { + me.viewFilter = view; + me.clearTree(); + updateTree(); + }, + setDatacenterText: function(clustername) { + var rootnode = me.store.getRootNode(); + + var rnodeText = gettext('Datacenter'); + if (clustername !== undefined) { + rnodeText += ' (' + clustername + ')'; + } + + rootnode.beginEdit(); + rootnode.data.text = rnodeText; + rootnode.commit(); + }, + clearTree: function() { + pdata.updateCount = 0; + var rootnode = me.store.getRootNode(); + rootnode.collapse(); + rootnode.removeAll(); + pdata.dataIndex = {}; + me.getSelectionModel().deselectAll(); + }, + selectExpand: function(node) { + var sm = me.getSelectionModel(); + if (!sm.isSelected(node)) { + sm.select(node); + var cn = node; + while (!!(cn = cn.parentNode)) { + if (!cn.isExpanded()) { + cn.expand(); + } + } + me.getView().focusRow(node); + } + }, + selectById: function(nodeid) { + var rootnode = me.store.getRootNode(); + var sm = me.getSelectionModel(); + var node; + if (nodeid === 'root') { + node = rootnode; + } else { + node = rootnode.findChild('id', nodeid, true); + } + if (node) { + me.selectExpand(node); + } + return node; + }, + applyState : function(state) { + var sm = me.getSelectionModel(); + if (state && state.value) { + me.selectById(state.value); + } else { + sm.deselectAll(); + } + } + }); + + me.callParent(); + + var sm = me.getSelectionModel(); + sm.on('select', function(sm, n) { + sp.set(stateid, { value: n.data.id}); + }); + + rstore.on("load", updateTree); + rstore.startUpdate(); + //rstore.stopUpdate(); + } + +}); +Ext.define('pve-fw-ipsets', { + extend: 'Ext.data.Model', + fields: [ 'name', 'comment', 'digest' ], + idProperty: 'name' +}); + +Ext.define('PVE.IPSetList', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveIPSetList', + + stateful: true, + stateId: 'grid-firewall-ipsetlist', + + ipset_panel: undefined, + + base_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + initComponent: function() { + + var me = this; + + if (me.ipset_panel == undefined) { + throw "no rule panel specified"; + } + + if (me.base_url == undefined) { + throw "no base_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-fw-ipsets', + proxy: { + type: 'proxmox', + url: "/api2/json" + me.base_url + }, + sorters: { + property: 'name', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + var oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('name', oldrec.data.name); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('Proxmox.window.Edit', { + subject: "IPSet '" + rec.data.name + "'", + url: me.base_url, + method: 'POST', + digest: rec.data.digest, + items: [ + { + xtype: 'hiddenfield', + name: 'rename', + value: rec.data.name + }, + { + xtype: 'textfield', + name: 'name', + value: rec.data.name, + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: rec.data.comment, + fieldLabel: gettext('Comment') + } + ] + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Create'), + handler: function() { + sm.deselectAll(); + var win = Ext.create('Proxmox.window.Edit', { + subject: 'IPSet', + url: me.base_url, + method: 'POST', + items: [ + { + xtype: 'textfield', + name: 'name', + value: '', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + win.show(); + win.on('destroy', reload); + + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload + }); + + Ext.apply(me, { + store: store, + tbar: [ 'IPSet:', me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: 'IPSet', dataIndex: 'name', width: '100' }, + { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 } + ], + listeners: { + itemdblclick: run_editor, + select: function(sm, rec) { + var url = me.base_url + '/' + rec.data.name; + me.ipset_panel.setBaseUrl(url); + }, + deselect: function() { + me.ipset_panel.setBaseUrl(undefined); + }, + show: reload + } + }); + + me.callParent(); + + store.load(); + } +}); + +Ext.define('PVE.IPSetCidrEdit', { + extend: 'Proxmox.window.Edit', + + cidr: undefined, + + initComponent : function() { + + var me = this; + + me.isCreate = (me.cidr === undefined); + + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.cidr; + me.method = 'PUT'; + } + + var column1 = []; + + if (me.isCreate) { + if (!me.list_refs_url) { + throw "no alias_base_url specified"; + } + + column1.push({ + xtype: 'pveIPRefSelector', + name: 'cidr', + ref_type: 'alias', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('IP/CIDR') + }); + } else { + column1.push({ + xtype: 'displayfield', + name: 'cidr', + value: '', + fieldLabel: gettext('IP/CIDR') + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + column1: column1, + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'nomatch', + checked: false, + uncheckedValue: 0, + fieldLabel: 'nomatch' + } + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + + Ext.apply(me, { + subject: gettext('IP/CIDR'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('PVE.IPSetGrid', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveIPSetGrid', + + stateful: true, + stateId: 'grid-firewall-ipsets', + + base_url: undefined, + list_refs_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + setBaseUrl: function(url) { + var me = this; + + me.base_url = url; + + if (url === undefined) { + me.addBtn.setDisabled(true); + me.store.removeAll(); + } else { + me.addBtn.setDisabled(false); + me.removeBtn.baseurl = url + '/'; + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json' + url + }); + + me.store.load(); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.list_refs_url) { + throw "no1 list_refs_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-ipset' + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('PVE.IPSetCidrEdit', { + base_url: me.base_url, + cidr: rec.data.cidr + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Add'), + disabled: true, + handler: function() { + if (!me.base_url) { + return; + } + var win = Ext.create('PVE.IPSetCidrEdit', { + base_url: me.base_url, + list_refs_url: me.list_refs_url + }); + win.show(); + win.on('destroy', reload); + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload + }); + + var render_errors = function(value, metaData, record) { + var errors = record.data.errors; + if (errors) { + var msg = errors.cidr || errors.nomatch; + if (msg) { + metaData.tdCls = 'proxmox-invalid-row'; + var html = '

' + Ext.htmlEncode(msg) + '

'; + metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + + html.replace(/\"/g,'"') + '"'; + } + } + return value; + }; + + Ext.apply(me, { + tbar: [ 'IP/CIDR:', me.addBtn, me.removeBtn, me.editBtn ], + store: store, + selModel: sm, + listeners: { + itemdblclick: run_editor + }, + columns: [ + { + xtype: 'rownumberer' + }, + { + header: gettext('IP/CIDR'), + dataIndex: 'cidr', + width: 150, + renderer: function(value, metaData, record) { + value = render_errors(value, metaData, record); + if (record.data.nomatch) { + return '! ' + value; + } + return value; + } + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + flex: 1, + renderer: function(value) { + return Ext.util.Format.htmlEncode(value); + } + } + ] + }); + + me.callParent(); + + if (me.base_url) { + me.setBaseUrl(me.base_url); // load + } + } +}, function() { + + Ext.define('pve-ipset', { + extend: 'Ext.data.Model', + fields: [ { name: 'nomatch', type: 'boolean' }, + 'cidr', 'comment', 'errors' ], + idProperty: 'cidr' + }); + +}); + +Ext.define('PVE.IPSet', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveIPSet', + + title: 'IPSet', + + onlineHelp: 'pve_firewall_ip_sets', + + list_refs_url: undefined, + + initComponent: function() { + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + var ipset_panel = Ext.createWidget('pveIPSetGrid', { + region: 'center', + list_refs_url: me.list_refs_url, + border: false + }); + + var ipset_list = Ext.createWidget('pveIPSetList', { + region: 'west', + ipset_panel: ipset_panel, + base_url: me.base_url, + width: '50%', + border: false, + split: true + }); + + Ext.apply(me, { + layout: 'border', + items: [ ipset_list, ipset_panel ], + listeners: { + show: function() { + ipset_list.fireEvent('show', ipset_list); + } + } + }); + + me.callParent(); + } +}); +/* + * Base class for all the multitab config panels + * + * How to use this: + * + * You create a subclass of this, and then define your wanted tabs + * as items like this: + * + * items: [{ + * title: "myTitle", + * xytpe: "somextype", + * iconCls: 'fa fa-icon', + * groups: ['somegroup'], + * expandedOnInit: true, + * itemId: 'someId' + * }] + * + * this has to be in the declarative syntax, else we + * cannot save them for later + * (so no Ext.create or Ext.apply of an item in the subclass) + * + * the groups array expects the itemids of the items + * which are the parents, which have to come before they + * are used + * + * if you want following the tree: + * + * Option1 + * Option2 + * -> SubOption1 + * -> SubSubOption1 + * + * the suboption1 group array has to look like this: + * groups: ['itemid-of-option2'] + * + * and of subsuboption1: + * groups: ['itemid-of-option2', 'itemid-of-suboption1'] + * + * setting the expandedOnInit determines if the item/group is expanded + * initially (false by default) + */ +Ext.define('PVE.panel.Config', { + extend: 'Ext.panel.Panel', + alias: 'widget.pvePanelConfig', + + showSearch: true, // add a resource grid with a search button as first tab + viewFilter: undefined, // a filter to pass to that resource grid + + tbarSpacing: true, // if true, adds a spacer after the title in tbar + + dockedItems: [{ + // this is needed for the overflow handler + xtype: 'toolbar', + overflowHandler: 'scroller', + dock: 'left', + style: { + backgroundColor: '#f5f5f5', + padding: 0, + margin: 0 + }, + items: { + xtype: 'treelist', + itemId: 'menu', + ui: 'nav', + expanderOnly: true, + expanderFirst: false, + animation: false, + singleExpand: false, + listeners: { + selectionchange: function(treeList, selection) { + var me = this.up('panel'); + me.suspendLayout = true; + me.activateCard(selection.data.id); + me.suspendLayout = false; + me.updateLayout(); + }, + itemclick: function(treelist, info) { + var olditem = treelist.getSelection(); + var newitem = info.node; + + // when clicking on the expand arrow, + // we don't select items, but still want + // the original behaviour + if (info.select === false) { + return; + } + + // if you click on a different item which is open, + // leave it open + // else toggle the clicked item + if (olditem.data.id !== newitem.data.id && + newitem.data.expanded === true) { + info.toggle = false; + } else { + info.toggle = true; + } + } + } + } + }, + { + xtype: 'toolbar', + itemId: 'toolbar', + dock: 'top', + height: 36, + overflowHandler: 'scroller' + }], + + firstItem: '', + layout: 'card', + border: 0, + + // used for automated test + selectById: function(cardid) { + var me = this; + + var root = me.store.getRoot(); + var selection = root.findChild('id', cardid, true); + + if (selection) { + selection.expand(); + var menu = me.down('#menu'); + menu.setSelection(selection); + return cardid; + } + }, + + activateCard: function(cardid) { + var me = this; + if (me.savedItems[cardid]) { + var curcard = me.getLayout().getActiveItem(); + var newcard = me.add(me.savedItems[cardid]); + me.helpButton.setOnlineHelp(newcard.onlineHelp || me.onlineHelp); + if (curcard) { + me.setActiveItem(cardid); + me.remove(curcard, true); + + // trigger state change + + var ncard = cardid; + // Note: '' is alias for first tab. + // First tab can be 'search' or something else + if (cardid === me.firstItem) { + ncard = ''; + } + if (me.hstateid) { + me.sp.set(me.hstateid, { value: ncard }); + } + } + } + }, + + initComponent: function() { + var me = this; + + var stateid = me.hstateid; + + me.sp = Ext.state.Manager.getProvider(); + + var activeTab; // leaving this undefined means items[0] will be the default tab + + if (stateid) { + var state = me.sp.get(stateid); + if (state && state.value) { + // if this tab does not exists, it chooses the first + activeTab = state.value; + } + } + + // get title + var title = me.title || me.pveSelNode.data.text; + me.title = undefined; + + // create toolbar + var tbar = me.tbar || []; + me.tbar = undefined; + + if (!me.onlineHelp) { + switch(me.pveSelNode.data.id) { + case 'type/storage':me.onlineHelp = 'chapter-pvesm.html'; break; + case 'type/qemu':me.onlineHelp = 'chapter-qm.html'; break; + case 'type/lxc':me.onlineHelp = 'chapter-pct.html'; break; + case 'type/pool':me.onlineHelp = 'chapter-pveum.html#_pools'; break; + case 'type/node':me.onlineHelp = 'chapter-sysadmin.html'; break; + } + } + + if (me.tbarSpacing) { + tbar.unshift('->'); + } + tbar.unshift({ + xtype: 'tbtext', + text: title, + baseCls: 'x-panel-header-text' + }); + + me.helpButton = Ext.create('Proxmox.button.Help', { + hidden: false, + listenToGlobalEvent: false, + onlineHelp: me.onlineHelp || undefined + }); + + tbar.push(me.helpButton); + + me.dockedItems[1].items = tbar; + + // include search tab + me.items = me.items || []; + if (me.showSearch) { + me.items.unshift({ + itemId: 'search', + title: gettext('Search'), + iconCls: 'fa fa-search', + xtype: 'pveResourceGrid', + pveSelNode: me.pveSelNode + }); + } + + me.savedItems = {}; + /*jslint confusion:true*/ + if (me.items[0]) { + me.firstItem = me.items[0].itemId; + } + /*jslint confusion:false*/ + + me.store = Ext.create('Ext.data.TreeStore', { + root: { + expanded: true + } + }); + var root = me.store.getRoot(); + me.items.forEach(function(item){ + var treeitem = Ext.create('Ext.data.TreeModel',{ + id: item.itemId, + text: item.title, + iconCls: item.iconCls, + leaf: true, + expanded: item.expandedOnInit + }); + item.header = false; + if (me.savedItems[item.itemId] !== undefined) { + throw "itemId already exists, please use another"; + } + me.savedItems[item.itemId] = item; + + var group; + var curnode = root; + + // get/create the group items + while (Ext.isArray(item.groups) && item.groups.length > 0) { + group = item.groups.shift(); + + var child = curnode.findChild('id', group); + if (child === null) { + // did not find the group item + // so add it where we are + break; + } + curnode = child; + } + + // insert the item + + // lets see if it already exists + var node = curnode.findChild('id', item.itemId); + + if (node === null) { + curnode.appendChild(treeitem); + } else { + // should not happen! + throw "id already exists"; + } + }); + + delete me.items; + me.defaults = me.defaults || {}; + Ext.apply(me.defaults, { + pveSelNode: me.pveSelNode, + viewFilter: me.viewFilter, + workspace: me.workspace, + border: 0 + }); + + me.callParent(); + + var menu = me.down('#menu'); + var selection = root.findChild('id', activeTab, true) || root.firstChild; + var node = selection; + while (node !== root) { + node.expand(); + node = node.parentNode; + } + menu.setStore(me.store); + menu.setSelection(selection); + + // on a state change, + // select the new item + var statechange = function(sp, key, state) { + // it the state change is for this panel + if (stateid && (key === stateid) && state) { + // get active item + var acard = me.getLayout().getActiveItem().itemId; + // get the itemid of the new value + var ncard = state.value || me.firstItem; + if (ncard && (acard != ncard)) { + // select the chosen item + menu.setSelection(root.findChild('id', ncard, true) || root.firstChild); + } + } + }; + + if (stateid) { + me.mon(me.sp, 'statechange', statechange); + } + } +}); +Ext.define('PVE.grid.BackupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveBackupView'], + + onlineHelp: 'chapter_vzdump', + + stateful: true, + stateId: 'grid-guest-backup', + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var vmtype = me.pveSelNode.data.type; + if (!vmtype) { + throw "no VM type specified"; + } + + var vmtypeFilter; + if (vmtype === 'openvz') { + vmtypeFilter = function(item) { + return item.data.volid.match(':backup/vzdump-openvz-'); + }; + } else if (vmtype === 'lxc') { + vmtypeFilter = function(item) { + return item.data.volid.match(':backup/vzdump-lxc-'); + }; + } else if (vmtype === 'qemu') { + vmtypeFilter = function(item) { + return item.data.volid.match(':backup/vzdump-qemu-'); + }; + } else { + throw "unsupported VM type '" + vmtype + "'"; + } + + var searchFilter = { + property: 'volid', + // on initial store display only our vmid backups + // surround with minus sign to prevent the 2016 VMID bug + value: vmtype + '-' + vmid + '-', + anyMatch: true, + caseSensitive: false + }; + + me.store = Ext.create('Ext.data.Store', { + model: 'pve-storage-content', + sorters: { + property: 'volid', + order: 'DESC' + }, + filters: [ + vmtypeFilter, + searchFilter + ] + }); + + var reload = Ext.Function.createBuffered(function() { + if (me.store) { + me.store.load(); + } + }, 100); + + var setStorage = function(storage) { + var url = '/api2/json/nodes/' + nodename + '/storage/' + storage + '/content'; + url += '?content=backup'; + + me.store.setProxy({ + type: 'proxmox', + url: url + }); + + reload(); + }; + + var storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: nodename, + fieldLabel: gettext('Storage'), + labelAlign: 'right', + storageContent: 'backup', + allowBlank: false, + listeners: { + change: function(f, value) { + setStorage(value); + } + } + }); + + var storagefilter = Ext.create('Ext.form.field.Text', { + fieldLabel: gettext('Search'), + labelWidth: 50, + labelAlign: 'right', + enableKeyEvents: true, + value: searchFilter.value, + listeners: { + buffer: 500, + keyup: function(field) { + me.store.clearFilter(true); + searchFilter.value = field.getValue(); + me.store.filter([ + vmtypeFilter, + searchFilter + ]); + } + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var backup_btn = Ext.create('Ext.button.Button', { + text: gettext('Backup now'), + handler: function() { + var win = Ext.create('PVE.window.Backup', { + nodename: nodename, + vmid: vmid, + vmtype: vmtype, + storage: storagesel.getValue(), + listeners : { + close: function() { + reload(); + } + } + }); + win.show(); + } + }); + + var restore_btn = Ext.create('Proxmox.button.Button', { + text: gettext('Restore'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return !!rec; + }, + handler: function(b, e, rec) { + var volid = rec.data.volid; + + var win = Ext.create('PVE.window.Restore', { + nodename: nodename, + vmid: vmid, + volid: rec.data.volid, + volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec), + vmtype: vmtype + }); + win.show(); + win.on('destroy', reload); + } + }); + + var delete_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + dangerous: true, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.volid + "'"); + msg += " " + gettext('This will permanently erase all data.'); + + return msg; + }, + getUrl: function(rec) { + var storage = storagesel.getValue(); + return '/nodes/' + nodename + '/storage/' + storage + '/content/' + rec.data.volid; + }, + callback: function() { + reload(); + } + }); + + var config_btn = Ext.create('Proxmox.button.Button', { + text: gettext('Show Configuration'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return !!rec; + }, + handler: function(b, e, rec) { + var storage = storagesel.getValue(); + if (!storage) { + return; + } + + var win = Ext.create('PVE.window.BackupConfig', { + volume: rec.data.volid, + pveSelNode: me.pveSelNode + }); + + win.show(); + } + }); + + Ext.apply(me, { + selModel: sm, + tbar: [ backup_btn, restore_btn, delete_btn,config_btn, '->', storagesel, storagefilter ], + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + renderer: PVE.Utils.render_storage_content, + dataIndex: 'volid' + }, + { + header: gettext('Format'), + width: 100, + dataIndex: 'format' + }, + { + header: gettext('Size'), + width: 100, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + } + ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.CephCreateService', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCephCreateService', + + showProgress: true, + + setNode: function(nodename) { + var me = this; + + me.nodename = nodename; + me.url = "/nodes/" + nodename + "/ceph/" + me.type + "/" + nodename; + }, + + method: 'POST', + isCreate: true, + + items: [ + { + xtype: 'pveNodeSelector', + submitValue: false, + fieldLabel: gettext('Host'), + selectCurNode: true, + allowBlank: false, + listeners: { + change: function(f, value) { + var me = this.up('pveCephCreateService'); + me.setNode(value); + } + } + } + ], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.type) { + throw "no type specified"; + } + + me.setNode(me.nodename); + + me.callParent(); + } +}); + +Ext.define('PVE.node.CephServiceList', { + extend: 'Ext.grid.GridPanel', + xtype: 'pveNodeCephServiceList', + + onlineHelp: 'chapter_pveceph', + emptyText: gettext('No such service configured.'), + + stateful: true, + + // will be called when the store loads + storeLoadCallback: Ext.emptyFn, + + // if set to true, does shows the ceph install mask if needed + showCephInstallMask: false, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + if (view.pveSelNode) { + view.nodename = view.pveSelNode.data.node; + } + if (!view.nodename) { + throw "no node name specified"; + } + + if (!view.type) { + throw "no type specified"; + } + + view.rstore = Ext.create('Proxmox.data.UpdateStore', { + autoLoad: true, + autoStart: true, + interval: 3000, + storeid: 'ceph-' + view.type + '-list' + view.nodename, + model: 'ceph-service-list', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + view.nodename + "/ceph/" + view.type + } + }); + + view.setStore(Ext.create('Proxmox.data.DiffStore', { + rstore: view.rstore, + sorters: [{ property: 'name' }] + })); + + if (view.storeLoadCallback) { + view.rstore.on('load', view.storeLoadCallback, this); + } + view.on('destroy', view.rstore.stopUpdate); + + if (view.showCephInstallMask) { + var regex = new RegExp("not (installed|initialized)", "i"); + PVE.Utils.handleStoreErrorOrMask(view, view.rstore, regex, function(me, error) { + view.rstore.stopUpdate(); + PVE.Utils.showCephInstallOrMask(view.ownerCt, error.statusText, view.nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + view.rstore.startUpdate(); + }); + } + ); + }); + } + }, + + service_cmd: function(rec, cmd) { + var view = this.getView(); + if (!rec.data.host) { + Ext.Msg.alert(gettext('Error'), "entry has no host"); + return; + } + Proxmox.Utils.API2Request({ + url: "/nodes/" + rec.data.host + "/ceph/" + cmd, + method: 'POST', + params: { service: view.type + '.' + rec.data.name }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + taskDone: function() { + view.rstore.load(); + } + }); + win.show(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + onChangeService: function(btn) { + var me = this; + var view = this.getView(); + var cmd = btn.action; + var rec = view.getSelection()[0]; + me.service_cmd(rec, cmd); + }, + + showSyslog: function() { + var view = this.getView(); + var rec = view.getSelection()[0]; + var servicename = 'ceph-' + view.type + '@' + rec.data.name; + var url = "/api2/extjs/nodes/" + rec.data.host + "/syslog?service=" + encodeURIComponent(servicename); + var win = Ext.create('Ext.window.Window', { + title: gettext('Syslog') + ': ' + servicename, + modal: true, + width: 800, + height: 400, + layout: 'fit', + items: [{ + xtype: 'proxmoxLogView', + url: url, + log_select_timespan: 1 + }] + }); + win.show(); + }, + + onCreate: function() { + var view = this.getView(); + var win = Ext.create('PVE.CephCreateService', { + autoShow: true, + nodename: view.nodename, + subject: view.getTitle(), + type: view.type, + taskDone: function() { + view.rstore.load(); + } + }); + } + }, + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Start'), + iconCls: 'fa fa-play', + action: 'start', + disabled: true, + enableFn: function(rec) { + return rec.data.state === 'stopped' || + rec.data.state === 'unknown'; + }, + handler: 'onChangeService' + }, + { + xtype: 'proxmoxButton', + text: gettext('Stop'), + iconCls: 'fa fa-stop', + action: 'stop', + enableFn: function(rec) { + return rec.data.state !== 'stopped'; + }, + disabled: true, + handler: 'onChangeService' + }, + { + xtype: 'proxmoxButton', + text: gettext('Restart'), + iconCls: 'fa fa-refresh', + action: 'restart', + disabled: true, + enableFn: function(rec) { + return rec.data.state !== 'stopped'; + }, + handler: 'onChangeService' + }, + '-', + { + text: gettext('Create'), + reference: 'createButton', + handler: 'onCreate' + }, + { + text: gettext('Destroy'), + xtype: 'proxmoxStdRemoveButton', + getUrl: function(rec) { + var view = this.up('grid'); + if (!rec.data.host) { + Ext.Msg.alert(gettext('Error'), "entry has no host"); + return; + } + return "/nodes/" + rec.data.host + "/ceph/" + view.type + "/" + rec.data.name; + }, + callback: function(options, success, response) { + var view = this.up('grid'); + if (!success) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + return; + } + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + taskDone: function() { + view.rstore.load(); + } + }); + win.show(); + } + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Syslog'), + disabled: true, + handler: 'showSyslog' + } + ], + + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + renderer: function(v) { + return this.type + '.' + v; + }, + dataIndex: 'name' + }, + { + header: gettext('Host'), + flex: 1, + sortable: true, + renderer: function(v) { + return v || Proxmox.Utils.unknownText; + }, + dataIndex: 'host' + }, + { + header: gettext('Status'), + flex: 1, + sortable: false, + dataIndex: 'state' + }, + { + header: gettext('Address'), + flex: 3, + sortable: true, + renderer: function(v) { + return v || Proxmox.Utils.unknownText; + }, + dataIndex: 'addr' + }, + { + header: gettext('Version'), + flex: 3, + sortable: true, + dataIndex: 'version' + } + ], + + initComponent: function() { + var me = this; + + if (me.additionalColumns) { + me.columns = me.columns.concat(me.additionalColumns); + } + + me.callParent(); + } + +}, function() { + + Ext.define('ceph-service-list', { + extend: 'Ext.data.Model', + fields: [ 'addr', 'name', 'rank', 'host', 'quorum', 'state', + 'ceph_version', 'ceph_version_short', + { type: 'string', name: 'version', calculate: function(data) { + return PVE.Utils.parse_ceph_version(data); + } } + ], + idProperty: 'name' + }); +}); +/*jslint confusion: true */ +Ext.define('PVE.CephCreateFS', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveCephCreateFS', + + showTaskViewer: true, + onlineHelp: 'pveceph_fs_create', + + subject: 'Ceph FS', + isCreate: true, + method: 'POST', + + setFSName: function(fsName) { + var me = this; + + if (fsName === '' || fsName === undefined) { + fsName = 'cephfs'; + } + + me.url = "/nodes/" + me.nodename + "/ceph/fs/" + fsName; + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Name'), + name: 'name', + value: 'cephfs', + listeners: { + change: function(f, value) { + this.up('pveCephCreateFS').setFSName(value); + } + }, + submitValue: false, // already encoded in apicall URL + emptyText: 'cephfs' + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: 'Placement Groups', + name: 'pg_num', + value: 128, + emptyText: 128, + minValue: 8, + maxValue: 32768, + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Add as Storage'), + value: true, + name: 'add-storage', + autoEl: { + tag: 'div', + 'data-qtip': gettext('Add the new CephFS to the cluster storage configuration.'), + }, + } + ], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + me.setFSName(); + + me.callParent(); + } +}); + +Ext.define('PVE.NodeCephFSPanel', { + extend: 'Ext.panel.Panel', + xtype: 'pveNodeCephFSPanel', + mixins: ['Proxmox.Mixin.CBind'], + + title: gettext('CephFS'), + onlineHelp: 'pveceph_fs', + + border: false, + defaults: { + border: false, + cbind: { + nodename: '{nodename}' + } + }, + + viewModel: { + parent: null, + data: { + cephfsConfigured: false, + mdsCount: 0 + }, + formulas: { + canCreateFS: function(get) { + return (!get('cephfsConfigured') && get('mdsCount') > 0); + } + } + }, + + items: [ + { + xtype: 'grid', + emptyText: Ext.String.format(gettext('No {0} configured.'), 'CephFS'), + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.rstore = Ext.create('Proxmox.data.UpdateStore', { + autoLoad: true, + xtype: 'update', + interval: 5 * 1000, + autoStart: true, + storeid: 'pve-ceph-fs', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + view.nodename + '/ceph/fs' + }, + model: 'pve-ceph-fs' + }); + view.setStore(Ext.create('Proxmox.data.DiffStore', { + rstore: view.rstore, + sorters: { + property: 'name', + order: 'DESC' + } + })); + var regex = new RegExp("not (installed|initialized)", "i"); + PVE.Utils.handleStoreErrorOrMask(view, view.rstore, regex, function(me, error){ + me.rstore.stopUpdate(); + PVE.Utils.showCephInstallOrMask(me.ownerCt, error.statusText, view.nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.rstore.startUpdate(); + }); + } + ); + }); + view.rstore.on('load', this.onLoad, this); + view.on('destroy', view.rstore.stopUpdate); + }, + + onCreate: function() { + var view = this.getView(); + view.rstore.stopUpdate(); + var win = Ext.create('PVE.CephCreateFS', { + autoShow: true, + nodename: view.nodename, + listeners: { + destroy: function() { + view.rstore.startUpdate(); + } + } + }); + }, + + onLoad: function(store, records, success) { + var vm = this.getViewModel(); + if (!(success && records && records.length > 0)) { + vm.set('cephfsConfigured', false); + return; + } + vm.set('cephfsConfigured', true); + } + }, + tbar: [ + { + text: gettext('Create CephFS'), + reference: 'createButton', + handler: 'onCreate', + bind: { + // only one CephFS per Ceph cluster makes sense for now + disabled: '{!canCreateFS}' + } + } + ], + columns: [ + { + header: gettext('Name'), + flex: 1, + dataIndex: 'name' + }, + { + header: 'Data Pool', + flex: 1, + dataIndex: 'data_pool' + }, + { + header: 'Metadata Pool', + flex: 1, + dataIndex: 'metadata_pool' + } + ], + cbind: { + nodename: '{nodename}' + } + }, + { + xtype: 'pveNodeCephServiceList', + title: gettext('Metadata Servers'), + stateId: 'grid-ceph-mds', + type: 'mds', + storeLoadCallback: function(store, records, success) { + var vm = this.getViewModel(); + if (!success || !records) { + vm.set('mdsCount', 0); + return; + } + vm.set('mdsCount', records.length); + }, + cbind: { + nodename: '{nodename}' + } + } + ] +}, function() { + Ext.define('pve-ceph-fs', { + extend: 'Ext.data.Model', + fields: [ 'name', 'data_pool', 'metadata_pool' ], + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/localhost/ceph/fs" + }, + idProperty: 'name' + }); +}); +Ext.define('PVE.CephCreatePool', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveCephCreatePool', + + showProgress: true, + onlineHelp: 'pve_ceph_pools', + + subject: 'Ceph Pool', + isCreate: true, + method: 'POST', + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Name'), + name: 'name', + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Size'), + name: 'size', + value: 3, + minValue: 1, + maxValue: 7, + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Min. Size'), + name: 'min_size', + value: 2, + minValue: 1, + maxValue: 7, + allowBlank: false + }, + { + xtype: 'pveCephRuleSelector', + fieldLabel: 'Crush Rule', // do not localize + name: 'crush_rule', + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: 'pg_num', + name: 'pg_num', + value: 128, + minValue: 8, + maxValue: 32768, + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Add as Storage'), + value: true, + name: 'add_storages', + autoEl: { + tag: 'div', + 'data-qtip': gettext('Add the new pool to the cluster storage configuration.'), + }, + } + ], + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: "/nodes/" + me.nodename + "/ceph/pools", + defaults: { + nodename: me.nodename + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.CephPoolList', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveNodeCephPoolList', + + onlineHelp: 'chapter_pveceph', + + stateful: true, + stateId: 'grid-ceph-pools', + bufferedRenderer: false, + + features: [ { ftype: 'summary'} ], + + columns: [ + { + header: gettext('Name'), + width: 120, + sortable: true, + dataIndex: 'pool_name' + }, + { + header: gettext('Size') + '/min', + width: 100, + align: 'right', + renderer: function(v, meta, rec) { + return v + '/' + rec.data.min_size; + }, + dataIndex: 'size' + }, + { + text: '# Placement Groups', // pg_num', + width: 180, + align: 'right', + dataIndex: 'pg_num' + }, + { + text: 'CRUSH Rule', + columns: [ + { + text: 'ID', + align: 'right', + width: 50, + dataIndex: 'crush_rule' + }, + { + text: gettext('Name'), + width: 150, + dataIndex: 'crush_rule_name', + }, + ] + }, + { + text: gettext('Used'), + columns: [ + { + text: '%', + width: 100, + sortable: true, + align: 'right', + renderer: function(val) { + return Ext.util.Format.percent(val, '0.00'); + }, + dataIndex: 'percent_used', + summaryType: 'sum', + summaryRenderer: function(val) { + return Ext.util.Format.percent(val, '0.00'); + }, + }, + { + text: gettext('Total'), + width: 100, + sortable: true, + renderer: PVE.Utils.render_size, + align: 'right', + dataIndex: 'bytes_used', + summaryType: 'sum', + summaryRenderer: PVE.Utils.render_size + } + ] + } + ], + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var rstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: 'ceph-pool-list' + nodename, + model: 'ceph-pool-list', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/ceph/pools" + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore }); + + var regex = new RegExp("not (installed|initialized)", "i"); + PVE.Utils.handleStoreErrorOrMask(me, rstore, regex, function(me, error){ + me.store.rstore.stopUpdate(); + PVE.Utils.showCephInstallOrMask(me, error.statusText, nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.store.rstore.startUpdate(); + }); + } + ); + }); + + var create_btn = new Ext.Button({ + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.CephCreatePool', { + nodename: nodename + }); + win.show(); + win.on('destroy', function() { + rstore.load(); + }); + } + }); + + var destroy_btn = Ext.create('Proxmox.button.Button', { + text: gettext('Destroy'), + selModel: sm, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + + if (!rec.data.pool_name) { + return; + } + var base_url = '/nodes/' + nodename + '/ceph/pools/' + + rec.data.pool_name; + + var win = Ext.create('PVE.window.SafeDestroy', { + showProgress: true, + url: base_url, + params: { + remove_storages: 1 + }, + item: { type: 'CephPool', id: rec.data.pool_name } + }).show(); + win.on('destroy', function() { + rstore.load(); + }); + } + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ create_btn, destroy_btn ], + listeners: { + activate: rstore.startUpdate, + destroy: rstore.stopUpdate + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('ceph-pool-list', { + extend: 'Ext.data.Model', + fields: [ 'pool_name', + { name: 'pool', type: 'integer'}, + { name: 'size', type: 'integer'}, + { name: 'min_size', type: 'integer'}, + { name: 'pg_num', type: 'integer'}, + { name: 'bytes_used', type: 'integer'}, + { name: 'percent_used', type: 'number'}, + { name: 'crush_rule', type: 'integer'}, + { name: 'crush_rule_name', type: 'string'} + ], + idProperty: 'pool_name' + }); +}); + +Ext.define('PVE.form.CephRuleSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCephRuleSelector', + + allowBlank: false, + valueField: 'name', + displayField: 'name', + editable: false, + queryMode: 'local', + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['name'], + sorters: 'name', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/ceph/rules' + } + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + store.load({ + callback: function(rec, op, success){ + if (success && rec.length > 0) { + me.select(rec[0]); + } + } + }); + } + +}); +Ext.define('PVE.CephCreateOsd', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCephCreateOsd', + + subject: 'Ceph OSD', + + showProgress: true, + + onlineHelp: 'pve_ceph_osds', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/ceph/osd", + method: 'POST', + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + Object.keys(values || {}).forEach(function(name) { + if (values[name] === '') { + delete values[name]; + } + }); + + return values; + }, + column1: [ + { + xtype: 'pveDiskSelector', + name: 'dev', + nodename: me.nodename, + diskType: 'unused', + fieldLabel: gettext('Disk'), + allowBlank: false + } + ], + column2: [ + { + xtype: 'pveDiskSelector', + name: 'db_dev', + nodename: me.nodename, + diskType: 'journal_disks', + fieldLabel: gettext('DB Disk'), + value: '', + autoSelect: false, + allowBlank: true, + emptyText: 'use OSD disk', + listeners: { + change: function(field, val) { + me.down('field[name=db_size]').setDisabled(!val); + } + } + }, + { + xtype: 'numberfield', + name: 'db_size', + fieldLabel: gettext('DB size') + ' (GiB)', + minValue: 1, + maxValue: 128*1024, + decimalPrecision: 2, + allowBlank: true, + disabled: true, + emptyText: gettext('Automatic') + } + ], + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + name: 'encrypted', + fieldLabel: gettext('Encrypt OSD') + }, + ], + advancedColumn2: [ + { + xtype: 'pveDiskSelector', + name: 'wal_dev', + nodename: me.nodename, + diskType: 'journal_disks', + fieldLabel: gettext('WAL Disk'), + value: '', + autoSelect: false, + allowBlank: true, + emptyText: 'use OSD/DB disk', + listeners: { + change: function(field, val) { + me.down('field[name=wal_size]').setDisabled(!val); + } + } + }, + { + xtype: 'numberfield', + name: 'wal_size', + fieldLabel: gettext('WAL size') + ' (GiB)', + minValue: 0.5, + maxValue: 128*1024, + decimalPrecision: 2, + allowBlank: true, + disabled: true, + emptyText: gettext('Automatic') + } + ] + }, + { + xtype: 'displayfield', + padding: '5 0 0 0', + userCls: 'pve-hint', + value: 'Note: Ceph is not compatible with disks backed by a hardware ' + + 'RAID controller. For details see ' + + 'the reference documentation.', + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.CephRemoveOsd', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveCephRemoveOsd'], + + isRemove: true, + + showProgress: true, + method: 'DELETE', + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'cleanup', + checked: true, + labelWidth: 130, + fieldLabel: gettext('Cleanup Disks') + } + ], + initComponent : function() { + + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + if (me.osdid === undefined || me.osdid < 0) { + throw "no osdid specified"; + } + + me.isCreate = true; + + me.title = gettext('Destroy') + ': Ceph OSD osd.' + me.osdid.toString(); + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/ceph/osd/" + me.osdid.toString() + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.CephOsdTree', { + extend: 'Ext.tree.Panel', + alias: ['widget.pveNodeCephOsdTree'], + onlineHelp: 'chapter_pveceph', + + viewModel: { + data: { + nodename: '', + flags: [], + maxversion: '0', + versions: {}, + isOsd: false, + downOsd: false, + upOsd: false, + inOsd: false, + outOsd: false, + osdid: '', + osdhost: '', + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + + reload: function() { + var me = this.getView(); + var vm = this.getViewModel(); + var nodename = vm.get('nodename'); + var sm = me.getSelectionModel(); + Proxmox.Utils.API2Request({ + url: "/nodes/" + nodename + "/ceph/osd", + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + var msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask(me, msg, nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', this.reload); + } + ); + }, + success: function(response, opts) { + var data = response.result.data; + var selected = me.getSelection(); + var name; + if (selected.length) { + name = selected[0].data.name; + } + vm.set('versions', data.versions); + // extract max version + var maxversion = vm.get('maxversion'); + Object.values(data.versions || {}).forEach(function(version) { + if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) { + maxversion = version; + } + }); + vm.set('maxversion', maxversion); + sm.deselectAll(); + me.setRootNode(data.root); + me.expandAll(); + if (name) { + var node = me.getRootNode().findChild('name', name, true); + if (node) { + me.setSelection([node]); + } + } + + var flags = data.flags.split(','); + vm.set('flags', flags); + var noout = flags.includes('noout'); + me.down('#nooutBtn').setText(noout ? gettext("Unset noout") : gettext("Set noout")); + } + }); + }, + + osd_cmd: function(comp) { + var me = this; + var vm = this.getViewModel(); + var cmd = comp.cmd; + var params = comp.params || {}; + var osdid = vm.get('osdid'); + + var doRequest = function() { + Proxmox.Utils.API2Request({ + url: "/nodes/" + vm.get('osdhost') + "/ceph/osd/" + osdid + '/' + cmd, + waitMsgTarget: me.getView(), + method: 'POST', + params: params, + success: () => { me.reload(); }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + if (cmd === 'scrub') { + Ext.MessageBox.defaultButton = params.deep === 1 ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: params.deep === 1 ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: params.deep !== 1 ? + Ext.String.format(gettext("Scrub OSD.{0}"), osdid) : + Ext.String.format(gettext("Deep Scrub OSD.{0}"), osdid) + + "
Caution: This can reduce performance while it is running.", + buttons: Ext.Msg.YESNO, + callback: function(btn) { + if (btn !== 'yes') { + return; + } + doRequest(); + } + }); + } else { + doRequest(); + } + }, + + create_osd: function() { + var me = this; + var vm = this.getViewModel(); + Ext.create('PVE.CephCreateOsd', { + nodename: vm.get('nodename'), + taskDone: () => { me.reload(); } + }).show(); + }, + + destroy_osd: function() { + var me = this; + var vm = this.getViewModel(); + Ext.create('PVE.CephRemoveOsd', { + nodename: vm.get('osdhost'), + osdid: vm.get('osdid'), + taskDone: () => { me.reload(); } + }).show(); + }, + + set_flag: function() { + var me = this; + var vm = this.getViewModel(); + var flags = vm.get('flags'); + Proxmox.Utils.API2Request({ + url: "/nodes/" + vm.get('nodename') + "/ceph/flags/noout", + waitMsgTarget: me.getView(), + method: flags.includes('noout') ? 'DELETE' : 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: () => { me.reload(); } + }); + }, + + service_cmd: function(comp) { + var me = this; + var vm = this.getViewModel(); + var cmd = comp.cmd || comp; + Proxmox.Utils.API2Request({ + url: "/nodes/" + vm.get('osdhost') + "/ceph/" + cmd, + params: { service: "osd." + vm.get('osdid') }, + waitMsgTarget: me.getView(), + method: 'POST', + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + taskDone: () => { me.reload(); } + }); + win.show(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + set_selection_status: function(tp, selection) { + if (selection.length < 1) { + return; + } + var rec = selection[0]; + var vm = this.getViewModel(); + + var isOsd = (rec.data.host && (rec.data.type === 'osd') && (rec.data.id >= 0)); + + vm.set('isOsd', isOsd); + vm.set('downOsd', isOsd && rec.data.status === 'down'); + vm.set('upOsd', isOsd && rec.data.status !== 'down'); + vm.set('inOsd', isOsd && rec.data.in); + vm.set('outOsd', isOsd && !rec.data.in); + vm.set('osdid', isOsd ? rec.data.id : undefined); + vm.set('osdhost', isOsd ? rec.data.host : undefined); + }, + + render_status: function(value, metaData, rec) { + if (!value) { + return value; + } + var inout = rec.data['in'] ? 'in' : 'out'; + var updownicon = value === 'up' ? 'good fa-arrow-circle-up' : + 'critical fa-arrow-circle-down'; + + var inouticon = rec.data['in'] ? 'good fa-circle' : + 'warning fa-circle-o'; + + var text = value + ' / ' + + inout + ' '; + + return text; + }, + + render_wal: function(value, metaData, rec) { + if (!value && + rec.data.osdtype === 'bluestore' && + rec.data.type === 'osd') { + return 'N/A'; + } + return value; + }, + + render_version: function(value, metadata, rec) { + var vm = this.getViewModel(); + var versions = vm.get('versions'); + var icon = ""; + var version = value || ""; + if (value && value != vm.get('maxversion')) { + icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD'); + } + + if (!value && rec.data.type == 'host') { + version = versions[rec.data.name] || Proxmox.Utils.unknownText; + } + + return icon + version; + }, + + render_osd_val: function(value, metaData, rec) { + return (rec.data.type === 'osd') ? value : ''; + }, + render_osd_weight: function(value, metaData, rec) { + if (rec.data.type !== 'osd') { + return ''; + } + return Ext.util.Format.number(value, '0.00###'); + }, + + render_osd_latency: function(value, metaData, rec) { + if (rec.data.type !== 'osd') { + return ''; + } + let commit_ms = rec.data.commit_latency_ms, + apply_ms = rec.data.apply_latency_ms; + return apply_ms + ' / ' + commit_ms; + }, + + render_osd_size: function(value, metaData, rec) { + return this.render_osd_val(PVE.Utils.render_size(value), metaData, rec); + }, + + control: { + '#': { + selectionchange: 'set_selection_status' + } + }, + + init: function(view) { + var me = this; + var vm = this.getViewModel(); + + if (!view.pveSelNode.data.node) { + throw "no node name specified"; + } + + vm.set('nodename', view.pveSelNode.data.node); + + me.callParent(); + me.reload(); + } + }, + + stateful: true, + stateId: 'grid-ceph-osd', + rootVisible: false, + useArrows: true, + + columns: [ + { + xtype: 'treecolumn', + text: 'Name', + dataIndex: 'name', + width: 150 + }, + { + text: 'Type', + dataIndex: 'type', + hidden: true, + align: 'right', + width: 75 + }, + { + text: gettext("Class"), + dataIndex: 'device_class', + align: 'right', + width: 75 + }, + { + text: "OSD Type", + dataIndex: 'osdtype', + align: 'right', + width: 100 + }, + { + text: "Bluestore Device", + dataIndex: 'blfsdev', + align: 'right', + width: 75, + hidden: true + }, + { + text: "DB Device", + dataIndex: 'dbdev', + align: 'right', + width: 75, + hidden: true + }, + { + text: "WAL Device", + dataIndex: 'waldev', + align: 'right', + renderer: 'render_wal', + width: 75, + hidden: true + }, + { + text: 'Status', + dataIndex: 'status', + align: 'right', + renderer: 'render_status', + width: 120 + }, + { + text: gettext('Version'), + dataIndex: 'version', + align: 'right', + renderer: 'render_version' + }, + { + text: 'weight', + dataIndex: 'crush_weight', + align: 'right', + renderer: 'render_osd_weight', + width: 90 + }, + { + text: 'reweight', + dataIndex: 'reweight', + align: 'right', + renderer: 'render_osd_weight', + width: 90 + }, + { + text: gettext('Used') + ' (%)', + dataIndex: 'percent_used', + align: 'right', + renderer: function(value, metaData, rec) { + if (rec.data.type !== 'osd') { + return ''; + } + return Ext.util.Format.number(value, '0.00'); + }, + width: 100 + }, + { + text: gettext('Total'), + dataIndex: 'total_space', + align: 'right', + renderer: 'render_osd_size', + width: 100 + }, + { + text: 'Apply/Commit
Latency (ms)', + dataIndex: 'apply_latency_ms', + align: 'right', + renderer: 'render_osd_latency', + width: 120 + } + ], + + + tbar: { + items: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: 'reload' + }, + '-', + { + text: gettext('Create') + ': OSD', + handler: 'create_osd', + }, + { + text: gettext('Set noout'), + itemId: 'nooutBtn', + handler: 'set_flag', + }, + '->', + { + xtype: 'tbtext', + data: { + osd: undefined + }, + bind: { + data: { + osd: "{osdid}" + } + }, + tpl: [ + '', + 'osd.{osd}:', + '', + gettext('No OSD selected'), + '' + ] + }, + { + text: gettext('Start'), + iconCls: 'fa fa-play', + disabled: true, + bind: { + disabled: '{!downOsd}' + }, + cmd: 'start', + handler: 'service_cmd' + }, + { + text: gettext('Stop'), + iconCls: 'fa fa-stop', + disabled: true, + bind: { + disabled: '{!upOsd}' + }, + cmd: 'stop', + handler: 'service_cmd' + }, + { + text: gettext('Restart'), + iconCls: 'fa fa-refresh', + disabled: true, + bind: { + disabled: '{!upOsd}' + }, + cmd: 'restart', + handler: 'service_cmd' + }, + '-', + { + text: 'Out', + iconCls: 'fa fa-circle-o', + disabled: true, + bind: { + disabled: '{!inOsd}' + }, + cmd: 'out', + handler: 'osd_cmd' + }, + { + text: 'In', + iconCls: 'fa fa-circle', + disabled: true, + bind: { + disabled: '{!outOsd}' + }, + cmd: 'in', + handler: 'osd_cmd' + }, + '-', + { + text: gettext('More'), + iconCls: 'fa fa-bars', + disabled: true, + bind: { + disabled: '{!isOsd}' + }, + menu: [ + { + text: gettext('Scrub'), + iconCls: 'fa fa-shower', + cmd: 'scrub', + handler: 'osd_cmd' + }, + { + text: gettext('Deep Scrub'), + iconCls: 'fa fa-bath', + cmd: 'scrub', + params: { + deep: 1, + }, + handler: 'osd_cmd' + }, + { + text: gettext('Destroy'), + itemId: 'remove', + iconCls: 'fa fa-fw fa-trash-o', + bind: { + disabled: '{!downOsd}' + }, + handler: 'destroy_osd' + } + ], + } + ] + }, + + fields: [ + 'name', 'type', 'status', 'host', 'in', 'id' , + { type: 'number', name: 'reweight' }, + { type: 'number', name: 'percent_used' }, + { type: 'integer', name: 'bytes_used' }, + { type: 'integer', name: 'total_space' }, + { type: 'integer', name: 'apply_latency_ms' }, + { type: 'integer', name: 'commit_latency_ms' }, + { type: 'string', name: 'device_class' }, + { type: 'string', name: 'osdtype' }, + { type: 'string', name: 'blfsdev' }, + { type: 'string', name: 'dbdev' }, + { type: 'string', name: 'waldev' }, + { type: 'string', name: 'version', calculate: function(data) { + return PVE.Utils.parse_ceph_version(data); + } }, + { type: 'string', name: 'iconCls', calculate: function(data) { + var iconMap = { + host: 'fa-building', + osd: 'fa-hdd-o', + root: 'fa-server', + }; + return 'fa x-fa-tree ' + iconMap[data.type]; + } }, + { type: 'number', name: 'crush_weight' } + ], +}); +Ext.define('PVE.node.CephMonMgrList', { + extend: 'Ext.container.Container', + xtype: 'pveNodeCephMonMgr', + + mixins: ['Proxmox.Mixin.CBind' ], + + onlineHelp: 'chapter_pveceph', + + defaults: { + border: false, + onlineHelp: 'chapter_pveceph', + flex: 1 + }, + + layout: { + type: 'vbox', + align: 'stretch' + }, + + items: [ + { + xtype: 'pveNodeCephServiceList', + cbind: { pveSelNode: '{pveSelNode}' }, + type: 'mon', + additionalColumns: [ + { + header: gettext('Quorum'), + width: 70, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'quorum' + } + ], + stateId: 'grid-ceph-monitor', + showCephInstallMask: true, + title: gettext('Monitor') + }, + { + xtype: 'pveNodeCephServiceList', + type: 'mgr', + stateId: 'grid-ceph-manager', + cbind: { pveSelNode: '{pveSelNode}' }, + title: gettext('Manager') + } + ] +}); +Ext.define('PVE.node.CephCrushMap', { + extend: 'Ext.panel.Panel', + alias: ['widget.pveNodeCephCrushMap'], + bodyStyle: 'white-space:pre', + bodyPadding: 5, + border: false, + stateful: true, + stateId: 'layout-ceph-crush', + scrollable: true, + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + var msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.load(); + }); + } + ); + }, + success: function(response, opts) { + var data = response.result.data; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: '/nodes/' + nodename + '/ceph/crush', + + listeners: { + activate: function() { + me.load(); + } + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('PVE.node.CephStatus', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephStatus', + + onlineHelp: 'chapter_pveceph', + + scrollable: true, + + bodyPadding: 5, + + layout: { + type: 'column' + }, + + defaults: { + padding: 5 + }, + + items: [ + { + xtype: 'panel', + title: gettext('Health'), + bodyPadding: 10, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + minHeight: 230, + columnWidth: 1 + }, + 'width >= 1900': { + minHeight: 500, + columnWidth: 0.5 + } + }, + layout: { + type: 'hbox', + align: 'stretch' + }, + items: [ + { + flex: 1, + itemId: 'overallhealth', + xtype: 'pveHealthWidget', + title: gettext('Status') + }, + { + flex: 2, + itemId: 'warnings', + stateful: true, + stateId: 'ceph-status-warnings', + xtype: 'grid', + // since we load the store manually, + // to show the emptytext, we have to + // specify an empty store + store: { data:[] }, + emptyText: gettext('No Warnings/Errors'), + columns: [ + { + dataIndex: 'severity', + header: gettext('Severity'), + align: 'center', + width: 70, + renderer: function(value) { + var health = PVE.Utils.map_ceph_health[value]; + var classes = PVE.Utils.get_health_icon(health); + + return ''; + }, + sorter: { + sorterFn: function(a,b) { + var healthArr = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK']; + return healthArr.indexOf(b.data.severity) - healthArr.indexOf(a.data.severity); + } + } + }, + { + dataIndex: 'summary', + header: gettext('Summary'), + flex: 1 + }, + { + xtype: 'actioncolumn', + width: 40, + align: 'center', + tooltip: gettext('Detail'), + items: [ + { + iconCls: 'x-fa fa-info-circle', + handler: function(grid, rowindex, colindex, item, e, record) { + var win = Ext.create('Ext.window.Window', { + title: gettext('Detail'), + resizable: true, + modal: true, + width: 650, + height: 400, + layout: { + type: 'fit' + }, + items: [{ + scrollable: true, + padding: 10, + xtype: 'box', + html: [ + '' + Ext.htmlEncode(record.data.summary) + '', + '
' + Ext.htmlEncode(record.data.detail) + '
' + ] + }] + }); + win.show(); + } + } + ] + } + ] + } + ] + }, + { + xtype: 'pveCephStatusDetail', + itemId: 'statusdetail', + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1, + minHeight: 250 + }, + 'width >= 1900': { + columnWidth: 0.5, + minHeight: 300 + } + }, + title: gettext('Status') + }, + { + title: gettext('Services'), + xtype: 'pveCephServices', + itemId: 'services', + plugins: 'responsive', + layout: { + type: 'hbox', + align: 'stretch' + }, + responsiveConfig: { + 'width < 1900': { + columnWidth: 1, + minHeight: 200 + }, + 'width >= 1900': { + columnWidth: 0.5, + minHeight: 200 + } + } + }, + { + xtype: 'panel', + title: gettext('Performance'), + columnWidth: 1, + bodyPadding: 5, + layout: { + type: 'hbox', + align: 'center' + }, + items: [ + { + flex: 1, + xtype: 'proxmoxGauge', + itemId: 'space', + title: gettext('Usage') + }, + { + flex: 2, + xtype: 'container', + defaults: { + padding: 0, + height: 100 + }, + items: [ + { + itemId: 'reads', + xtype: 'pveRunningChart', + title: gettext('Reads'), + renderer: PVE.Utils.render_bandwidth + }, + { + itemId: 'writes', + xtype: 'pveRunningChart', + title: gettext('Writes'), + renderer: PVE.Utils.render_bandwidth + }, + { + itemId: 'iops', + xtype: 'pveRunningChart', + hidden: true, + title: 'IOPS', // do not localize + renderer: Ext.util.Format.numberRenderer('0,000') + }, + { + itemId: 'readiops', + xtype: 'pveRunningChart', + hidden: true, + title: 'IOPS: ' + gettext('Reads'), + renderer: Ext.util.Format.numberRenderer('0,000') + }, + { + itemId: 'writeiops', + xtype: 'pveRunningChart', + hidden: true, + title: 'IOPS: ' + gettext('Writes'), + renderer: Ext.util.Format.numberRenderer('0,000') + } + ] + } + ] + } + ], + + generateCheckData: function(health) { + var result = []; + var checks = health.checks || {}; + var keys = Ext.Object.getKeys(checks).sort(); + + Ext.Array.forEach(keys, function(key) { + var details = checks[key].detail || []; + result.push({ + id: key, + summary: checks[key].summary.message, + detail: Ext.Array.reduce( + checks[key].detail, + function(first, second) { + return first + '\n' + second.message; + }, + '' + ), + severity: checks[key].severity + }); + }); + + return result; + }, + + updateAll: function(store, records, success) { + if (!success || records.length === 0) { + return; + } + + var me = this; + var rec = records[0]; + me.status = rec.data; + + // add health panel + me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {})); + // add errors to gridstore + me.down('#warnings').getStore().loadRawData(me.generateCheckData(rec.data.health || {}), false); + + // update services + me.getComponent('services').updateAll(me.metadata || {}, rec.data); + + // update detailstatus panel + me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data); + + // add performance data + var used = rec.data.pgmap.bytes_used; + var total = rec.data.pgmap.bytes_total; + + var text = Ext.String.format(gettext('{0} of {1}'), + PVE.Utils.render_size(used), + PVE.Utils.render_size(total) + ); + + // update the usage widget + me.down('#space').updateValue(used/total, text); + + // TODO: logic for jewel (iops split in read/write) + + var iops = rec.data.pgmap.op_per_sec; + var readiops = rec.data.pgmap.read_op_per_sec; + var writeiops = rec.data.pgmap.write_op_per_sec; + var reads = rec.data.pgmap.read_bytes_sec || 0; + var writes = rec.data.pgmap.write_bytes_sec || 0; + + if (iops !== undefined && me.version !== 'hammer') { + me.change_version('hammer'); + } else if((readiops !== undefined || writeiops !== undefined) && me.version !== 'jewel') { + me.change_version('jewel'); + } + // update the graphs + me.reads.addDataPoint(reads); + me.writes.addDataPoint(writes); + me.iops.addDataPoint(iops); + me.readiops.addDataPoint(readiops); + me.writeiops.addDataPoint(writeiops); + }, + + change_version: function(version) { + var me = this; + me.version = version; + me.sp.set('ceph-version', version); + me.iops.setVisible(version === 'hammer'); + me.readiops.setVisible(version === 'jewel'); + me.writeiops.setVisible(version === 'jewel'); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + + me.callParent(); + var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph'; + me.store = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'ceph-status-' + (nodename || 'cluster'), + interval: 5000, + proxy: { + type: 'proxmox', + url: baseurl + '/status' + } + }); + + me.metadatastore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'ceph-metadata-' + (nodename || 'cluster'), + interval: 15*1000, + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/ceph/metadata' + } + }); + + // save references for the updatefunction + me.iops = me.down('#iops'); + me.readiops = me.down('#readiops'); + me.writeiops = me.down('#writeiops'); + me.reads = me.down('#reads'); + me.writes = me.down('#writes'); + + // get ceph version + me.sp = Ext.state.Manager.getProvider(); + me.version = me.sp.get('ceph-version'); + me.change_version(me.version); + + var regex = new RegExp("not (installed|initialized)", "i"); + PVE.Utils.handleStoreErrorOrMask(me, me.store, regex, function(me, error){ + me.store.stopUpdate(); + PVE.Utils.showCephInstallOrMask(me, error.statusText, (nodename || 'localhost'), + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.store.startUpdate(); + }); + } + ); + }); + + me.mon(me.store, 'load', me.updateAll, me); + me.mon(me.metadatastore, 'load', function(store, records, success) { + if (!success || records.length < 1) { + return; + } + var rec = records[0]; + me.metadata = rec.data; + + // update services + me.getComponent('services').updateAll(rec.data, me.status || {}); + + // update detailstatus panel + me.getComponent('statusdetail').updateAll(rec.data, me.status || {}); + + }, me); + + me.on('destroy', me.store.stopUpdate); + me.on('destroy', me.metadatastore.stopUpdate); + me.store.startUpdate(); + me.metadatastore.startUpdate(); + } + +}); +Ext.define('PVE.ceph.StatusDetail', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveCephStatusDetail', + + layout: { + type: 'hbox', + align: 'stretch' + }, + + bodyPadding: '0 5', + defaults: { + xtype: 'box', + style: { + 'text-align':'center' + } + }, + + items: [{ + flex: 1, + itemId: 'osds', + maxHeight: 250, + scrollable: true, + padding: '0 10 5 10', + data: { + total: 0, + upin: 0, + upout: 0, + downin: 0, + downout: 0, + oldosds: [] + }, + tpl: [ + '

' + 'OSDs' + '

', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
', + gettext('In'), + '', + gettext('Out'), + '
', + gettext('Up'), + '{upin}{upout}
', + gettext('Down'), + '{downin}{downout}
', + '
', + gettext('Total'), + ': {total}', + '

', + '', + ' ' + gettext('Outdated OSDs') + "
", + '
', + '', + '
osd.{id}:
', + '
{version}

', + '
', + '
', + '
', + '
' + ] + }, + { + flex: 1, + border: false, + itemId: 'pgchart', + xtype: 'polar', + height: 184, + innerPadding: 5, + insetPadding: 5, + colors: [ + '#CFCFCF', + '#21BF4B', + '#FFCC00', + '#FF6C59' + ], + store: { }, + series: [ + { + type: 'pie', + donut: 60, + angleField: 'count', + tooltip: { + trackMouse: true, + renderer: function(tooltip, record, ctx) { + var html = record.get('text'); + html += '
'; + record.get('states').forEach(function(state) { + html += '
' + + state.state_name + ': ' + state.count.toString(); + }); + tooltip.setHtml(html); + } + }, + subStyle: { + strokeStyle: false + } + } + ] + }, + { + flex: 1.6, + itemId: 'pgs', + padding: '0 10', + maxHeight: 250, + scrollable: true, + data: { + states: [] + }, + tpl: [ + '

' + 'PGs' + '

', + '', + '
{state_name}:
', + '
{count}

', + '
', + '
' + ] + }], + + // similar to mgr dashboard + pgstates: { + // clean + clean: 1, + active: 1, + + // working + activating: 2, + backfill_wait: 2, + backfilling: 2, + creating: 2, + deep: 2, + degraded: 2, + forced_backfill: 2, + forced_recovery: 2, + peered: 2, + peering: 2, + recovering: 2, + recovery_wait: 2, + repair: 2, + scrubbing: 2, + snaptrim: 2, + snaptrim_wait: 2, + + // error + backfill_toofull: 3, + backfill_unfound: 3, + down: 3, + incomplete: 3, + inconsistent: 3, + recovery_toofull: 3, + recovery_unfound: 3, + remapped: 3, + snaptrim_error: 3, + stale: 3, + undersized: 3 + }, + + statecategories: [ + { + text: gettext('Unknown'), + count: 0, + states: [], + cls: 'faded' + }, + { + text: gettext('Clean'), + cls: 'good' + }, + { + text: gettext('Working'), + cls: 'warning' + }, + { + text: gettext('Error'), + cls: 'critical' + } + ], + + updateAll: function(metadata, status) { + var me = this; + me.suspendLayout = true; + + var maxversion = "0"; + Object.values(metadata.version || {}).forEach(function(version) { + if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) { + maxversion = version; + } + }); + + var oldosds = []; + + if (metadata.osd) { + metadata.osd.forEach(function(osd) { + var version = PVE.Utils.parse_ceph_version(osd); + if (version != maxversion) { + oldosds.push({ + id: osd.id, + version: version + }); + } + }); + } + + var pgmap = status.pgmap || {}; + var health = status.health || {}; + var osdmap = status.osdmap || { osdmap: {} }; + + + // update pgs sorted + var pgs_by_state = pgmap.pgs_by_state || []; + pgs_by_state.sort(function(a,b){ + return (a.state_name < b.state_name)?-1:(a.state_name === b.state_name)?0:1; + }); + + me.statecategories.forEach(function(cat) { + cat.count = 0; + cat.states = []; + }); + + pgs_by_state.forEach(function(state) { + var i; + var states = state.state_name.split(/[^a-z]+/); + var result = 0; + for (i = 0; i < states.length; i++) { + if (me.pgstates[states[i]] > result) { + result = me.pgstates[states[i]]; + } + } + // for the list + state.cls = me.statecategories[result].cls; + + me.statecategories[result].count += state.count; + me.statecategories[result].states.push(state); + }); + + me.getComponent('pgchart').getStore().setData(me.statecategories); + me.getComponent('pgs').update({states: pgs_by_state}); + + var downinregex = /(\d+) osds down/; + var downin_osds = 0; + + // we collect monitor/osd information from the checks + Ext.Object.each(health.checks, function(key, value, obj) { + var found = null; + if (key === 'OSD_DOWN') { + found = value.summary.message.match(downinregex); + if (found !== null) { + downin_osds = parseInt(found[1],10); + } + } + }); + + // update osds counts + + var total_osds = osdmap.osdmap.num_osds || 0; + var in_osds = osdmap.osdmap.num_in_osds || 0; + var up_osds = osdmap.osdmap.num_up_osds || 0; + var out_osds = total_osds - in_osds; + var down_osds = total_osds - up_osds; + + var downout_osds = down_osds - downin_osds; + var upin_osds = in_osds - downin_osds; + var upout_osds = up_osds - upin_osds; + var osds = { + total: total_osds, + upin: upin_osds, + upout: upout_osds, + downin: downin_osds, + downout: downout_osds, + oldosds: oldosds + }; + var osdcomponent = me.getComponent('osds'); + osdcomponent.update(Ext.apply(osdcomponent.data, osds)); + + me.suspendLayout = false; + me.updateLayout(); + } +}); + +Ext.define('PVE.ceph.Services', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveCephServices', + + layout: { + type: 'hbox', + align: 'stretch' + }, + + bodyPadding: '0 5 20', + defaults: { + xtype: 'box', + style: { + 'text-align':'center' + } + }, + + items: [ + { + flex: 1, + xtype: 'pveCephServiceList', + itemId: 'mons', + title: gettext('Monitors') + }, + { + flex: 1, + xtype: 'pveCephServiceList', + itemId: 'mgrs', + title: gettext('Managers') + }, + { + flex: 1, + xtype: 'pveCephServiceList', + itemId: 'mdss', + title: gettext('Meta Data Servers') + } + ], + + updateAll: function(metadata, status) { + var me = this; + + var healthstates = { + 'HEALTH_UNKNOWN': 0, + 'HEALTH_ERR': 1, + 'HEALTH_WARN': 2, + 'HEALTH_OLD': 3, + 'HEALTH_OK': 4 + }; + var healthmap = [ + 'HEALTH_UNKNOWN', + 'HEALTH_ERR', + 'HEALTH_WARN', + 'HEALTH_OLD', + 'HEALTH_OK' + ]; + var reduceFn = function(first, second) { + return first + '\n' + second.message; + }; + var services = ['mon','mgr','mds']; + var maxversion = "00.0.00"; + Object.values(metadata.version || {}).forEach(function(version) { + if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) { + maxversion = version; + } + }); + var i; + var quorummap = (status && status.quorum_names) ? status.quorum_names : []; + var monmessages = {}; + var mgrmessages = {}; + var mdsmessages = {}; + if (status) { + if (status.health) { + Ext.Object.each(status.health.checks, function(key, value, obj) { + if (!Ext.String.startsWith(key, "MON_")) { + return; + } + + var i; + for (i = 0; i < value.detail.length; i++) { + var match = value.detail[i].message.match(/mon.([a-zA-Z0-9\-\.]+)/); + if (!match) { + continue; + } + var monid = match[1]; + + if (!monmessages[monid]) { + monmessages[monid] = { + worstSeverity: healthstates.HEALTH_OK, + messages: [] + }; + } + + + monmessages[monid].messages.push( + PVE.Utils.get_ceph_icon_html(value.severity, true) + + Ext.Array.reduce(value.detail, reduceFn, '') + ); + if (healthstates[value.severity] < monmessages[monid].worstSeverity) { + monmessages[monid].worstSeverity = healthstates[value.severity]; + } + } + }); + } + + if (status.mgrmap) { + mgrmessages[status.mgrmap.active_name] = "active"; + status.mgrmap.standbys.forEach(function(mgr) { + mgrmessages[mgr.name] = "standby"; + }); + } + + if (status.fsmap) { + status.fsmap.by_rank.forEach(function(mds) { + mdsmessages[mds.name] = 'rank: ' + mds.rank + "; " + mds.status; + }); + } + } + + var checks = { + mon: function(mon) { + if (quorummap.indexOf(mon.name) !== -1) { + mon.health = healthstates.HEALTH_OK; + } else { + mon.health = healthstates.HEALTH_ERR; + } + if (monmessages[mon.name]) { + if (monmessages[mon.name].worstSeverity < mon.health) { + mon.health = monmessages[mon.name].worstSeverity; + } + Array.prototype.push.apply(mon.messages, monmessages[mon.name].messages); + } + return mon; + }, + mgr: function(mgr) { + if (mgrmessages[mgr.name] === 'active') { + mgr.title = '' + mgr.title + ''; + mgr.statuses.push(gettext('Status') + ': active'); + } else if (mgrmessages[mgr.name] === 'standby') { + mgr.statuses.push(gettext('Status') + ': standby'); + } else if (mgr.health > healthstates.HEALTH_WARN) { + mgr.health = healthstates.HEALTH_WARN; + } + + return mgr; + }, + mds: function(mds) { + if (mdsmessages[mds.name]) { + mds.title = '' + mds.title + ''; + mds.statuses.push(gettext('Status') + ': ' + mdsmessages[mds.name]+""); + } else if (mds.addr !== Proxmox.Utils.unknownText) { + mds.statuses.push(gettext('Status') + ': standby'); + } + + return mds; + } + }; + + for (i = 0; i < services.length; i++) { + var type = services[i]; + var ids = Object.keys(metadata[type] || {}); + me[type] = {}; + + var j; + for (j = 0; j < ids.length; j++) { + var id = ids[j]; + var tmp = id.split('@'); + var name = tmp[0]; + var host = tmp[1]; + var result = { + id: id, + health: healthstates.HEALTH_OK, + statuses: [], + messages: [], + name: name, + title: metadata[type][id].name || name, + host: host, + version: PVE.Utils.parse_ceph_version(metadata[type][id]), + service: metadata[type][id].service, + addr: metadata[type][id].addr || metadata[type][id].addrs || Proxmox.Utils.unknownText + }; + + result.statuses = [ + gettext('Host') + ": " + result.host, + gettext('Address') + ": " + result.addr + ]; + + if (checks[type]) { + result = checks[type](result); + } + + if (result.service && !result.version) { + result.messages.push( + PVE.Utils.get_ceph_icon_html('HEALTH_UNKNOWN', true) + + gettext('Stopped') + ); + result.health = healthstates.HEALTH_UNKNOWN; + } + + if (!result.version && result.addr === Proxmox.Utils.unknownText) { + result.health = healthstates.HEALTH_UNKNOWN; + } + + if (result.version) { + result.statuses.push(gettext('Version') + ": " + result.version); + + if (result.version != maxversion) { + if (result.health > healthstates.HEALTH_OLD) { + result.health = healthstates.HEALTH_OLD; + } + result.messages.push( + PVE.Utils.get_ceph_icon_html('HEALTH_OLD', true) + + gettext('Not Current Version, please upgrade') + ); + } + } + + result.statuses.push(''); // empty line + result.text = result.statuses.concat(result.messages).join('
'); + + result.health = healthmap[result.health]; + + me[type][id] = result; + } + } + + me.getComponent('mons').updateAll(Object.values(me.mon)); + me.getComponent('mgrs').updateAll(Object.values(me.mgr)); + me.getComponent('mdss').updateAll(Object.values(me.mds)); + } +}); + +Ext.define('PVE.ceph.ServiceList', { + extend: 'Ext.container.Container', + xtype: 'pveCephServiceList', + + style: { + 'text-align':'center' + }, + defaults: { + xtype: 'box', + style: { + 'text-align':'center' + } + }, + + items: [ + { + itemId: 'title', + data: { + title: '' + }, + tpl: '

{title}

' + } + ], + + updateAll: function(list) { + var me = this; + me.suspendLayout = true; + + var i; + list.sort(function(a,b) { + return a.id > b.id ? 1 : a.id < b.id ? -1 : 0; + }); + var ids = {}; + if (me.ids) { + me.ids.forEach(function(id) { + ids[id] = true; + }); + } + for (i = 0; i < list.length; i++) { + var service = me.getComponent(list[i].id); + if (!service) { + // since services are already sorted, and + // we always have a sorted list + // we can add it at the service+1 position (because of the title) + service = me.insert(i+1, { + xtype: 'pveCephServiceWidget', + itemId: list[i].id + }); + if (!me.ids) { + me.ids = []; + } + me.ids.push(list[i].id); + } else { + delete ids[list[i].id]; + } + service.updateService(list[i].title, list[i].text, list[i].health); + } + + Object.keys(ids).forEach(function(id) { + me.remove(id); + }); + me.suspendLayout = false; + me.updateLayout(); + }, + + initComponent: function() { + var me = this; + me.callParent(); + me.getComponent('title').update({ + title: me.title + }); + } +}); + +/*jslint confusion: true*/ +Ext.define('PVE.ceph.ServiceWidget', { + extend: 'Ext.Component', + alias: 'widget.pveCephServiceWidget', + + userCls: 'monitor inline-block', + data: { + title: '0', + health: 'HEALTH_ERR', + text: '', + iconCls: PVE.Utils.get_health_icon() + }, + + tpl: [ + '{title}: ', + '' + ], + + updateService: function(title, text, health) { + var me = this; + + me.update(Ext.apply(me.data, { + health: health, + text: text, + title: title, + iconCls: PVE.Utils.get_health_icon(PVE.Utils.map_ceph_health[health]) + })); + + if (me.tooltip) { + me.tooltip.setHtml(text); + } + }, + + listeners: { + destroy: function() { + var me = this; + if (me.tooltip) { + me.tooltip.destroy(); + delete me.tooltip; + } + }, + mouseenter: { + element: 'el', + fn: function(events, element) { + var me = this.component; + if (!me) { + return; + } + if (!me.tooltip) { + me.tooltip = Ext.create('Ext.tip.ToolTip', { + target: me.el, + trackMouse: true, + dismissDelay: 0, + renderTo: Ext.getBody(), + html: me.data.text + }); + } + me.tooltip.show(); + } + }, + mouseleave: { + element: 'el', + fn: function(events, element) { + var me = this.component; + if (me.tooltip) { + me.tooltip.destroy(); + delete me.tooltip; + } + } + } + } +}); +Ext.define('PVE.node.CephConfigDb', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveNodeCephConfigDb', + + border: false, + store: { + proxy: { + type: 'proxmox' + } + }, + + columns: [ + { + dataIndex: 'section', + text: 'WHO', + width: 100, + }, + { + dataIndex: 'mask', + text: 'MASK', + hidden: true, + width: 80, + }, + { + dataIndex: 'level', + hidden: true, + text: 'LEVEL', + }, + { + dataIndex: 'name', + flex: 1, + text: 'OPTION', + }, + { + dataIndex: 'value', + flex: 1, + text: 'VALUE', + }, + { + dataIndex: 'can_update_at_runtime', + text: 'Runtime Updatable', + hidden: true, + width: 80, + renderer: Proxmox.Utils.format_boolean + }, + ], + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.store.proxy.url = '/api2/json/nodes/' + nodename + '/ceph/configdb'; + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore()); + me.getStore().load(); + } +}); +Ext.define('PVE.node.CephConfig', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephConfig', + + bodyStyle: 'white-space:pre', + bodyPadding: 5, + border: false, + scrollable: true, + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + var msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.load(); + }); + } + ); + + }, + success: function(response, opts) { + var data = response.result.data; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: '/nodes/' + nodename + '/ceph/config', + listeners: { + activate: function() { + me.load(); + } + } + }); + + me.callParent(); + + me.load(); + } +}); + +Ext.define('PVE.node.CephConfigCrush', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephConfigCrush', + + onlineHelp: 'chapter_pveceph', + + layout: 'border', + items: [{ + title: gettext('Configuration'), + xtype: 'pveNodeCephConfig', + region: 'center' + }, + { + title: 'Crush Map', // do not localize + xtype: 'pveNodeCephCrushMap', + region: 'east', + split: true, + width: '50%' + }, + { + title: gettext('Configuration Database'), + xtype: 'pveNodeCephConfigDb', + region: 'south', + split: true, + weight: -30, + height: '50%' + }], + + initComponent: function() { + var me = this; + me.defaults = { + pveSelNode: me.pveSelNode + }; + me.callParent(); + } +}); +Ext.define('PVE.ceph.Log', { + extend: 'Proxmox.panel.LogView', + xtype: 'cephLogView', + nodename: undefined, + failCallback: function(response) { + var me = this; + var msg = response.htmlStatus; + var windowShow = PVE.Utils.showCephInstallOrMask(me, msg, me.nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.loadTask.delay(200); + }); + } + ); + if (!windowShow) { + Proxmox.Utils.setErrorMask(me, msg); + } + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.ceph.CephInstallWizard', { + extend: 'PVE.window.Wizard', + alias: 'widget.pveCephInstallWizard', + mixins: ['Proxmox.Mixin.CBind'], + resizable: false, + nodename: undefined, + viewModel: { + data: { + nodename: '', + configuration: true, + isInstalled: false + } + }, + cbindData: { + nodename: undefined + }, + title: gettext('Setup'), + navigateNext: function() { + var tp = this.down('#wizcontent'); + var atab = tp.getActiveTab(); + + var next = tp.items.indexOf(atab) + 1; + var ntab = tp.items.getAt(next); + if (ntab) { + ntab.enable(); + tp.setActiveTab(ntab); + } + }, + setInitialTab: function (index) { + var tp = this.down('#wizcontent'); + var initialTab = tp.items.getAt(index); + initialTab.enable(); + tp.setActiveTab(initialTab); + }, + onShow: function() { + this.callParent(arguments); + var isInstalled = this.getViewModel().get('isInstalled'); + if (isInstalled) { + this.getViewModel().set('configuration', false); + this.setInitialTab(2); + } + }, + items: [ + { + title: gettext('Info'), + xtype: 'panel', + border: false, + bodyBorder: false, + onlineHelp: 'chapter_pveceph', + html: '

Ceph?

'+ + '

"Ceph is a unified, distributed storage system designed for excellent performance, reliability and scalability."

'+ + '

Ceph is currently not installed on this node, click on the next button below to start the installation.'+ + ' This wizard will guide you through the necessary steps, after the initial installation you will be offered to create an initial configuration.'+ + ' The configuration step is only needed once per cluster and will be skipped if a config is already present.

'+ + '

Please take a look at our documentation, by clicking the help button below, before starting the installation, '+ + 'if you want to gain deeper knowledge about Ceph visit ceph.com.

', + listeners: { + activate: function() { + // notify owning container that it should display a help button + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp); + } + this.up('pveCephInstallWizard').down('#back').hide(true); + this.up('pveCephInstallWizard').down('#next').setText(gettext('Start installation')); + }, + deactivate: function() { + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp); + } + this.up('pveCephInstallWizard').down('#next').setText(gettext('Next')); + } + } + }, + { + title: gettext('Installation'), + xtype: 'panel', + layout: 'fit', + cbind:{ + nodename: '{nodename}' + }, + viewModel: {}, // needed to inherit parent viewModel data + listeners: { + afterrender: function() { + var me = this; + if (this.getViewModel().get('isInstalled')) { + this.mask("Ceph is already installed, click next to create your configuration.",['pve-static-mask']); + } else { + me.down('pveNoVncConsole').fireEvent('activate'); + } + }, + activate: function() { + var me = this; + var nodename = me.nodename; + me.updateStore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'ceph-status-' + nodename, + interval: 1000, + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + nodename + '/ceph/status' + }, + listeners: { + load: function(rec, response, success, operation) { + + if (success) { + me.updateStore.stopUpdate(); + me.down('textfield').setValue('success'); + } else if (operation.error.statusText.match("not initialized", "i")) { + me.updateStore.stopUpdate(); + me.up('pveCephInstallWizard').getViewModel().set('configuration',false); + me.down('textfield').setValue('success'); + } else if (operation.error.statusText.match("rados_connect failed", "i")) { + me.updateStore.stopUpdate(); + me.up('pveCephInstallWizard').getViewModel().set('configuration',true); + me.down('textfield').setValue('success'); + } else if (!operation.error.statusText.match("not installed", "i")) { + Proxmox.Utils.setErrorMask(me, operation.error.statusText); + } + } + } + }); + me.updateStore.startUpdate(); + }, + destroy: function() { + var me = this; + if (me.updateStore) { + me.updateStore.stopUpdate(); + } + } + }, + items: [ + { + itemId: 'jsconsole', + consoleType: 'cmd', + xtermjs: true, + xtype: 'pveNoVncConsole', + cbind:{ + nodename: '{nodename}' + }, + cmd: 'ceph_install' + }, + { + xtype: 'textfield', + name: 'installSuccess', + value: '', + allowBlank: false, + submitValue: false, + hidden: true + } + ] + }, + { + xtype: 'inputpanel', + title: gettext('Configuration'), + onlineHelp: 'chapter_pveceph', + cbind: { + nodename: '{nodename}' + }, + viewModel: { + data: { + replicas: undefined, + minreplicas: undefined + } + }, + listeners: { + activate: function() { + this.up('pveCephInstallWizard').down('#submit').setText(gettext('Next')); + }, + beforeshow: function() { + if (this.up('pveCephInstallWizard').getViewModel().get('configuration')) { + this.mask("Coniguration already initialized",['pve-static-mask']); + } else { + this.unmask(); + } + }, + deactivate: function() { + this.up('pveCephInstallWizard').down('#submit').setText(gettext('Finish')); + } + }, + column1: [ + { + xtype: 'displayfield', + value: gettext('Ceph cluster configuration') + ':' + }, + { + xtype: 'proxmoxNetworkSelector', + name: 'network', + value: '', + fieldLabel: 'Public Network IP/CIDR', + bind: { + allowBlank: '{configuration}' + } + }, + { + xtype: 'proxmoxNetworkSelector', + name: 'cluster-network', + fieldLabel: 'Cluster Network IP/CIDR', + allowBlank: true, + autoSelect: false, + emptyText: gettext('Same as Public Network') + } + // FIXME: add hint about cluster network and/or reference user to docs?? + ], + column2: [ + { + xtype: 'displayfield', + value: gettext('First Ceph monitor') + ':' + }, + { + xtype: 'pveNodeSelector', + fieldLabel: gettext('Monitor node'), + name: 'mon-node', + selectCurNode: true, + allowBlank: false + }, + { + xtype: 'displayfield', + value: gettext('Additional monitors are recommended. They can be created at any time in the Monitor tab.'), + userCls: 'pve-hint' + } + ], + advancedColumn1: [ + { + xtype: 'numberfield', + name: 'size', + fieldLabel: 'Number of replicas', + bind: { + value: '{replicas}' + }, + maxValue: 7, + minValue: 2, + emptyText: '3' + }, + { + xtype: 'numberfield', + name: 'min_size', + fieldLabel: 'Minimum replicas', + bind: { + maxValue: '{replicas}', + value: '{minreplicas}' + }, + minValue: 2, + maxValue: 3, + setMaxValue: function(value) { + this.maxValue = Ext.Number.from(value, 2); + // allow enough to avoid split brains with max 'size', but more makes simply no sense + if (this.maxValue > 4) { + this.maxValue = 4; + } + this.toggleSpinners(); + this.validate(); + }, + emptyText: '2' + } + ], + onGetValues: function(values) { + ['cluster-network', 'size', 'min_size'].forEach(function(field) { + if (!values[field]) { + delete values[field]; + } + }); + return values; + }, + onSubmit: function() { + var me = this; + if (!this.up('pveCephInstallWizard').getViewModel().get('configuration')) { + var wizard = me.up('window'); + var kv = wizard.getValues(); + delete kv['delete']; + var monNode = kv['mon-node']; + delete kv['mon-node']; + var nodename = me.nodename; + delete kv.nodename; + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/ceph/init', + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function() { + Proxmox.Utils.API2Request({ + url: '/nodes/' + monNode + '/ceph/mon/' + monNode, + waitMsgTarget: wizard, + method: 'POST', + success: function() { + me.up('pveCephInstallWizard').navigateNext(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + + } else { + me.up('pveCephInstallWizard').navigateNext(); + } + } + }, + { + title: gettext('Success'), + xtype: 'panel', + border: false, + bodyBorder: false, + onlineHelp: 'pve_ceph_install', + html: '

Installation successful!

'+ + '

The basic installation and configuration is completed, depending on your setup some of the following steps are required to start using Ceph:

'+ + '
  1. Install Ceph on other nodes
  2. '+ + '
  3. Create additional Ceph Monitors
  4. '+ + '
  5. Create Ceph OSDs
  6. '+ + '
  7. Create Ceph Pools
'+ + '

To learn more click on the help button below.

', + listeners: { + activate: function() { + // notify owning container that it should display a help button + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp); + } + + var tp = this.up('#wizcontent'); + var idx = tp.items.indexOf(this)-1; + for(;idx >= 0;idx--) { + var nc = tp.items.getAt(idx); + if (nc) { + nc.disable(); + } + } + }, + deactivate: function() { + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp); + } + } + }, + onSubmit: function() { + var wizard = this.up('pveCephInstallWizard'); + wizard.close(); + } + } + ] + }); +Ext.define('PVE.node.DiskList', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveNodeDiskList', + + emptyText: gettext('No Disks found'), + + stateful: true, + stateId: 'grid-node-disks', + + columns: [ + { + header: gettext('Device'), + width: 150, + sortable: true, + dataIndex: 'devpath' + }, + { + header: gettext('Type'), + width: 80, + sortable: true, + dataIndex: 'type', + renderer: function(v) { + if (v === 'ssd') { + return 'SSD'; + } else if (v === 'hdd') { + return 'Hard Disk'; + } else if (v === 'usb'){ + return 'USB'; + } else { + return gettext('Unknown'); + } + } + }, + { + header: gettext('Usage'), + width: 150, + sortable: false, + renderer: function(v, metaData, rec) { + if (rec) { + if (rec.data.osdid >= 0) { + var bluestore = ''; + if (rec.data.bluestore === 1) { + bluestore = ' (Bluestore)'; + } + return "Ceph osd." + rec.data.osdid.toString() + bluestore; + } + + var types = []; + if (rec.data.journals > 0) { + types.push('Journal'); + } + + if (rec.data.db > 0) { + types.push('DB'); + } + + if (rec.data.wal > 0) { + types.push('WAL'); + } + + if (types.length > 0) { + return 'Ceph (' + types.join(', ') + ')'; + } + } + + return v || Proxmox.Utils.noText; + }, + dataIndex: 'used' + }, + { + header: gettext('Size'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + }, + { + header: 'GPT', + width: 60, + align: 'right', + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'gpt' + }, + { + header: gettext('Vendor'), + width: 100, + sortable: true, + hidden: true, + renderer: Ext.String.htmlEncode, + dataIndex: 'vendor' + }, + { + header: gettext('Model'), + width: 200, + sortable: true, + renderer: Ext.String.htmlEncode, + dataIndex: 'model' + }, + { + header: gettext('Serial'), + width: 200, + sortable: true, + renderer: Ext.String.htmlEncode, + dataIndex: 'serial' + }, + { + header: 'S.M.A.R.T.', + width: 100, + sortable: true, + renderer: Ext.String.htmlEncode, + dataIndex: 'health' + }, + { + header: 'Wearout', + width: 90, + sortable: true, + align: 'right', + dataIndex: 'wearout', + renderer: function(value) { + if (Ext.isNumeric(value)) { + return (100 - value).toString() + '%'; + } + return 'N/A'; + } + } + ], + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var store = Ext.create('Ext.data.Store', { + storeid: 'node-disk-list' + nodename, + model: 'node-disk-list', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/disks/list" + }, + sorters: [ + { + property : 'dev', + direction: 'ASC' + } + ] + }); + + var reloadButton = Ext.create('Proxmox.button.Button', { + text: gettext('Reload'), + handler: function() { + me.store.load(); + } + }); + + var smartButton = Ext.create('Proxmox.button.Button', { + text: gettext('Show S.M.A.R.T. values'), + selModel: sm, + enableFn: function() { + return !!sm.getSelection().length; + }, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + + var win = Ext.create('PVE.DiskSmartWindow', { + nodename: nodename, + dev: rec.data.devpath + }); + win.show(); + } + }); + + var initButton = Ext.create('Proxmox.button.Button', { + text: gettext('Initialize Disk with GPT'), + selModel: sm, + enableFn: function() { + var selection = sm.getSelection(); + + if (!selection.length || selection[0].data.used) { + return false; + } else { + return true; + } + }, + disabled: true, + + handler: function() { + var rec = sm.getSelection()[0]; + Proxmox.Utils.API2Request({ + url: '/api2/extjs/nodes/' + nodename + '/disks/initgpt', + waitMsgTarget: me, + method: 'POST', + params: { disk: rec.data.devpath}, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid + }); + win.show(); + } + }); + } + }); + + me.loadCount = 1; // avoid duplicate loadmask + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ reloadButton, smartButton, initButton ], + listeners: { + itemdblclick: function() { + var rec = sm.getSelection()[0]; + + var win = Ext.create('PVE.DiskSmartWindow', { + nodename: nodename, + dev: rec.data.devpath + }); + win.show(); + } + } + }); + + + me.callParent(); + me.store.load(); + } +}, function() { + + Ext.define('node-disk-list', { + extend: 'Ext.data.Model', + fields: [ 'devpath', 'used', { name: 'size', type: 'number'}, + {name: 'osdid', type: 'number'}, + 'vendor', 'model', 'serial', 'rpm', 'type', 'health', 'wearout' ], + idProperty: 'devpath' + }); +}); + +Ext.define('PVE.DiskSmartWindow', { + extend: 'Ext.window.Window', + alias: 'widget.pveSmartWindow', + + modal: true, + + items: [ + { + xtype: 'gridpanel', + layout: { + type: 'fit' + }, + emptyText: gettext('No S.M.A.R.T. Values'), + scrollable: true, + flex: 1, + itemId: 'smarts', + reserveScrollbar: true, + columns: [ + { text: 'ID', dataIndex: 'id', width: 50 }, + { text: gettext('Attribute'), flex: 1, dataIndex: 'name', renderer: Ext.String.htmlEncode }, + { text: gettext('Value'), dataIndex: 'raw', renderer: Ext.String.htmlEncode }, + { text: gettext('Normalized'), dataIndex: 'value', width: 60}, + { text: gettext('Threshold'), dataIndex: 'threshold', width: 60}, + { text: gettext('Worst'), dataIndex: 'worst', width: 60}, + { text: gettext('Flags'), dataIndex: 'flags'}, + { text: gettext('Failing'), dataIndex: 'fail', renderer: Ext.String.htmlEncode } + ] + }, + { + xtype: 'component', + itemId: 'text', + layout: { + type: 'fit' + }, + hidden: true, + style: { + 'background-color': '#23272a', + 'white-space': 'pre', + 'font-family': 'monospace' + } + } + ], + + buttons: [ + { + text: gettext('Reload'), + name: 'reload', + handler: function() { + var me = this; + me.up('window').store.reload(); + } + }, + { + text: gettext('Close'), + name: 'close', + handler: function() { + var me = this; + me.up('window').close(); + } + } + ], + + layout: { + type: 'vbox', + align: 'stretch' + }, + width: 800, + height: 500, + minWidth: 600, + minHeight: 400, + bodyPadding: 5, + title: gettext('S.M.A.R.T. Values'), + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + if (!nodename) { + throw "no node name specified"; + } + + var dev = me.dev; + if (!dev) { + throw "no device specified"; + } + + me.store = Ext.create('Ext.data.Store', { + model: 'disk-smart', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/disks/smart?disk=" + dev + } + }); + + me.callParent(); + var grid = me.down('#smarts'); + var text = me.down('#text'); + + Proxmox.Utils.monStoreErrors(grid, me.store); + me.mon(me.store, 'load', function(s, records, success) { + if (success && records.length > 0) { + var rec = records[0]; + switch (rec.data.type) { + case 'text': + grid.setVisible(false); + text.setVisible(true); + text.setHtml(Ext.String.htmlEncode(rec.data.text)); + break; + default: + // includes 'ata' + // cannot use empty case because + // of jslint + grid.setVisible(true); + text.setVisible(false); + grid.setStore(rec.attributes()); + break; + } + } + }); + + me.store.load(); + } +}, function() { + + Ext.define('disk-smart', { + extend: 'Ext.data.Model', + fields: [ + { name:'health'}, + { name:'type'}, + { name:'text'} + ], + hasMany: {model: 'smart-attribute', name: 'attributes'} + }); + Ext.define('smart-attribute', { + extend: 'Ext.data.Model', + fields: [ + { name:'id', type:'number' }, 'name', 'value', 'worst', 'threshold', 'flags', 'fail', 'raw' + ] + }); +}); +Ext.define('PVE.node.CreateLVM', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateLVM', + + subject: 'LVM Volume Group', + + showProgress: true, + + onlineHelp: 'chapter_lvm', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/disks/lvm", + method: 'POST', + items: [ + { + xtype: 'pveDiskSelector', + name: 'device', + nodename: me.nodename, + diskType: 'unused', + fieldLabel: gettext('Disk'), + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1' + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.LVMList', { + extend: 'Ext.tree.Panel', + xtype: 'pveLVMList', + emptyText: gettext('No Volume Groups found'), + stateful: true, + stateId: 'grid-node-lvm', + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + text: gettext('Number of LVs'), + dataIndex: 'lvcount', + width: 150, + align: 'right' + }, + { + header: gettext('Usage'), + width: 110, + dataIndex: 'usage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Size'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + }, + { + header: gettext('Free'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'free' + } + ], + + rootVisible: false, + useArrows: true, + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + var me = this.up('panel'); + me.reload(); + } + }, + { + text: gettext('Create') + ': Volume Group', + handler: function() { + var me = this.up('panel'); + var win = Ext.create('PVE.node.CreateLVM', { + nodename: me.nodename, + taskDone: function() { + me.reload(); + } + }).show(); + } + } + ], + + reload: function() { + var me = this; + var sm = me.getSelectionModel(); + Proxmox.Utils.API2Request({ + url: "/nodes/" + me.nodename + "/disks/lvm", + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + }, + success: function(response, opts) { + sm.deselectAll(); + me.setRootNode(response.result.data); + me.expandAll(); + } + }); + }, + + listeners: { + activate: function() { + var me = this; + me.reload(); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + var sm = Ext.create('Ext.selection.TreeModel', {}); + + Ext.apply(me, { + selModel: sm, + fields: ['name', 'size', 'free', + { + type: 'string', + name: 'iconCls', + calculate: function(data) { + var txt = 'fa x-fa-tree fa-'; + txt += (data.leaf) ? 'hdd-o' : 'object-group'; + return txt; + } + }, + { + type: 'number', + name: 'usage', + calculate: function(data) { + return ((data.size-data.free)/data.size); + } + } + ], + sorters: 'name' + }); + + me.callParent(); + + me.reload(); + } +}); + +Ext.define('PVE.node.CreateLVMThin', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateLVMThin', + + subject: 'LVM Thinpool', + + showProgress: true, + + onlineHelp: 'chapter_lvm', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/disks/lvmthin", + method: 'POST', + items: [ + { + xtype: 'pveDiskSelector', + name: 'device', + nodename: me.nodename, + diskType: 'unused', + fieldLabel: gettext('Disk'), + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1' + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.LVMThinList', { + extend: 'Ext.grid.Panel', + xtype: 'pveLVMThinList', + + emptyText: gettext('No thinpools found'), + stateful: true, + stateId: 'grid-node-lvmthin', + columns: [ + { + text: gettext('Name'), + dataIndex: 'lv', + flex: 1 + }, + { + header: gettext('Usage'), + width: 110, + dataIndex: 'usage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Size'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'lv_size' + }, + { + header: gettext('Used'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'used' + }, + { + header: gettext('Metadata Usage'), + width: 120, + dataIndex: 'metadata_usage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Metadata Size'), + width: 120, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'metadata_size' + }, + { + header: gettext('Metadata Used'), + width: 125, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'metadata_used' + } + ], + + rootVisible: false, + useArrows: true, + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + var me = this.up('panel'); + me.reload(); + } + }, + { + text: gettext('Create') + ': Thinpool', + handler: function() { + var me = this.up('panel'); + var win = Ext.create('PVE.node.CreateLVMThin', { + nodename: me.nodename, + taskDone: function() { + me.reload(); + } + }).show(); + } + } + ], + + reload: function() { + var me = this; + me.store.load(); + me.store.sort(); + }, + + listeners: { + activate: function() { + var me = this; + me.reload(); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + store: { + fields: ['lv', 'lv_size', 'used', 'metadata_size', 'metadata_used', + { + type: 'number', + name: 'usage', + calculate: function(data) { + return data.used/data.lv_size; + } + }, + { + type: 'number', + name: 'metadata_usage', + calculate: function(data) { + return data.metadata_used/data.metadata_size; + } + } + ], + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + '/disks/lvmthin' + }, + sorters: 'lv' + } + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore(), true); + me.reload(); + } +}); + +Ext.define('PVE.node.CreateDirectory', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateDirectory', + + subject: Proxmox.Utils.directoryText, + + showProgress: true, + + onlineHelp: 'chapter_storage', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/disks/directory", + method: 'POST', + items: [ + { + xtype: 'pveDiskSelector', + name: 'device', + nodename: me.nodename, + diskType: 'unused', + fieldLabel: gettext('Disk'), + allowBlank: false + }, + { + xtype: 'proxmoxKVComboBox', + comboItems: [ + ['ext4', 'ext4'], + ['xfs', 'xfs'] + ], + fieldLabel: gettext('Filesystem'), + name: 'filesystem', + value: '', + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1' + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.Directorylist', { + extend: 'Ext.grid.Panel', + xtype: 'pveDirectoryList', + + stateful: true, + stateId: 'grid-node-directory', + columns: [ + { + text: gettext('Path'), + dataIndex: 'path', + flex: 1 + }, + { + header: gettext('Device'), + flex: 1, + dataIndex: 'device' + }, + { + header: gettext('Type'), + width: 100, + dataIndex: 'type' + }, + { + header: gettext('Options'), + width: 100, + dataIndex: 'options' + }, + { + header: gettext('Unit File'), + hidden: true, + dataIndex: 'unitfile' + } + ], + + rootVisible: false, + useArrows: true, + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + var me = this.up('panel'); + me.reload(); + } + }, + { + text: gettext('Create') + ': Directory', + handler: function() { + var me = this.up('panel'); + var win = Ext.create('PVE.node.CreateDirectory', { + nodename: me.nodename + }).show(); + win.on('destroy', function() { me.reload(); }); + } + } + ], + + reload: function() { + var me = this; + me.store.load(); + me.store.sort(); + }, + + listeners: { + activate: function() { + var me = this; + me.reload(); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + store: { + fields: ['path', 'device', 'type', 'options', 'unitfile' ], + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + '/disks/directory' + }, + sorters: 'path' + } + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore(), true); + me.reload(); + } +}); + +/*jslint confusion: true*/ +Ext.define('PVE.node.CreateZFS', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateZFS', + + subject: 'ZFS', + + showProgress: true, + + onlineHelp: 'chapter_zfs', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + var update_disklist = function() { + var grid = me.down('#disklist'); + var disks = grid.getSelection(); + + var val = []; + disks.sort(function(a,b) { + var aorder = a.get('order') || 0; + var border = b.get('order') || 0; + return (aorder - border); + }); + + disks.forEach(function(disk) { + val.push(disk.get('devpath')); + }); + + me.down('field[name=devices]').setValue(val.join(',')); + }; + + Ext.apply(me, { + url: '/nodes/' + me.nodename + '/disks/zfs', + method: 'POST', + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + return values; + }, + column1: [ + { + xtype: 'textfield', + hidden: true, + name: 'devices', + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1' + } + ], + column2: [ + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('RAID Level'), + name: 'raidlevel', + value: 'single', + comboItems: [ + ['single', gettext('Single Disk')], + ['mirror', 'Mirror'], + ['raid10', 'RAID10'], + ['raidz', 'RAIDZ'], + ['raidz2', 'RAIDZ2'], + ['raidz3', 'RAIDZ3'] + ] + }, + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Compression'), + name: 'compression', + value: 'on', + comboItems: [ + ['on', 'on'], + ['off', 'off'], + ['gzip', 'gzip'], + ['lz4', 'lz4'], + ['lzjb', 'lzjb'], + ['zle', 'zle'] + ] + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('ashift'), + minValue: 9, + maxValue: 16, + value: '12', + name: 'ashift' + } + ], + columnB: [ + { + xtype: 'grid', + height: 200, + emptyText: gettext('No Disks unused'), + itemId: 'disklist', + selModel: 'checkboxmodel', + listeners: { + selectionchange: update_disklist + }, + store: { + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/disks/list?type=unused' + } + }, + columns: [ + { + text: gettext('Device'), + dataIndex: 'devpath', + flex: 1 + }, + { + text: gettext('Serial'), + dataIndex: 'serial' + }, + { + text: gettext('Size'), + dataIndex: 'size', + renderer: PVE.Utils.render_size + }, + { + header: gettext('Order'), + xtype: 'widgetcolumn', + dataIndex: 'order', + sortable: true, + widget: { + xtype: 'proxmoxintegerfield', + minValue: 1, + isFormField: false, + listeners: { + change: function(numberfield, value, old_value) { + var record = numberfield.getWidgetRecord(); + record.set('order', value); + update_disklist(record); + } + } + } + } + ] + } + ] + }, + { + xtype: 'displayfield', + padding: '5 0 0 0', + userCls: 'pve-hint', + value: 'Note: ZFS is not compatible with disks backed by a hardware ' + + 'RAID controller. For details see ' + + 'the reference documentation.', + } + ] + }); + + me.callParent(); + me.down('#disklist').getStore().load(); + } +}); + +Ext.define('PVE.node.ZFSDevices', { + extend: 'Ext.tree.Panel', + xtype: 'pveZFSDevices', + stateful: true, + stateId: 'grid-node-zfsstatus', + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + text: gettext('Health'), + renderer: PVE.Utils.render_zfs_health, + dataIndex: 'state' + }, + { + text: 'READ', + dataIndex: 'read' + }, + { + text: 'WRITE', + dataIndex: 'write' + }, + { + text: 'CKSUM', + dataIndex: 'cksum' + }, + { + text: gettext('Message'), + dataIndex: 'msg' + } + ], + + rootVisible: true, + + reload: function() { + var me = this; + var sm = me.getSelectionModel(); + Proxmox.Utils.API2Request({ + url: "/nodes/" + me.nodename + "/disks/zfs/" + me.zpool, + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + }, + success: function(response, opts) { + sm.deselectAll(); + me.setRootNode(response.result.data); + me.expandAll(); + } + }); + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.zpool) { + throw "no zpool specified"; + } + + var sm = Ext.create('Ext.selection.TreeModel', {}); + + Ext.apply(me, { + selModel: sm, + fields: ['name', 'status', + { + type: 'string', + name: 'iconCls', + calculate: function(data) { + var txt = 'fa x-fa-tree fa-'; + if (data.leaf) { + return txt + 'hdd-o'; + } + } + } + ], + sorters: 'name' + }); + + me.callParent(); + + me.reload(); + } +}); + +Ext.define('PVE.node.ZFSStatus', { + extend: 'Proxmox.grid.ObjectGrid', + xtype: 'pveZFSStatus', + layout: 'fit', + border: false, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.zpool) { + throw "no zpool specified"; + } + + me.url = "/api2/extjs/nodes/" + me.nodename + "/disks/zfs/" + me.zpool; + + me.rows = { + scan: { + header: gettext('Scan') + }, + status: { + header: gettext('Status') + }, + action: { + header: gettext('Action') + }, + errors: { + header: gettext('Errors') + } + }; + + me.callParent(); + me.reload(); + } +}); + +Ext.define('PVE.node.ZFSList', { + extend: 'Ext.grid.Panel', + xtype: 'pveZFSList', + + stateful: true, + stateId: 'grid-node-zfs', + columns: [ + { + text: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + header: gettext('Size'), + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + }, + { + header: gettext('Free'), + renderer: Proxmox.Utils.format_size, + dataIndex: 'free' + }, + { + header: gettext('Allocated'), + renderer: Proxmox.Utils.format_size, + dataIndex: 'alloc' + }, + { + header: gettext('Fragmentation'), + renderer: function(value) { + return value.toString() + '%'; + }, + dataIndex: 'frag' + }, + { + header: gettext('Health'), + renderer: PVE.Utils.render_zfs_health, + dataIndex: 'health' + }, + { + header: gettext('Deduplication'), + hidden: true, + renderer: function(value) { + return value.toFixed(2).toString() + 'x'; + }, + dataIndex: 'dedup' + } + ], + + rootVisible: false, + useArrows: true, + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + var me = this.up('panel'); + me.reload(); + } + }, + { + text: gettext('Create') + ': ZFS', + handler: function() { + var me = this.up('panel'); + var win = Ext.create('PVE.node.CreateZFS', { + nodename: me.nodename + }).show(); + win.on('destroy', function() { me.reload(); }); + } + }, + { + text: gettext('Detail'), + itemId: 'detailbtn', + disabled: true, + handler: function() { + var me = this.up('panel'); + var selection = me.getSelection(); + if (selection.length < 1) { + return; + } + me.show_detail(selection[0].get('name')); + } + } + ], + + show_detail: function(zpool) { + var me = this; + + var detailsgrid = Ext.create('PVE.node.ZFSStatus', { + layout: 'fit', + nodename: me.nodename, + flex: 0, + zpool: zpool + }); + + var devicetree = Ext.create('PVE.node.ZFSDevices', { + title: gettext('Devices'), + nodename: me.nodename, + flex: 1, + zpool: zpool + }); + + + var win = Ext.create('Ext.window.Window', { + modal: true, + width: 800, + height: 400, + resizable: true, + layout: 'fit', + title: gettext('Status') + ': ' + zpool, + items:[{ + xtype: 'panel', + region: 'center', + layout: { + type: 'vbox', + align: 'stretch' + }, + items: [detailsgrid, devicetree], + tbar: [{ + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + + devicetree.reload(); + detailsgrid.reload(); + } + }] + }] + }).show(); + }, + + set_button_status: function() { + var me = this; + var selection = me.getSelection(); + me.down('#detailbtn').setDisabled(selection.length === 0); + }, + + reload: function() { + var me = this; + me.store.load(); + me.store.sort(); + }, + + listeners: { + activate: function() { + var me = this; + me.reload(); + }, + selectionchange: function() { + this.set_button_status(); + }, + itemdblclick: function(grid, record) { + var me = this; + me.show_detail(record.get('name')); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + store: { + fields: ['name', 'size', 'free', 'alloc', 'dedup', 'frag', 'health'], + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + '/disks/zfs' + }, + sorters: 'name' + } + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore(), true); + me.reload(); + } +}); + +Ext.define('PVE.node.StatusView', { + extend: 'PVE.panel.StatusView', + alias: 'widget.pveNodeStatus', + + height: 300, + bodyPadding: '20 15 20 15', + + layout: { + type: 'table', + columns: 2, + tableAttrs: { + style: { + width: '100%' + } + } + }, + + defaults: { + xtype: 'pveInfoWidget', + padding: '0 15 5 15' + }, + + items: [ + { + itemId: 'cpu', + iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon', + title: gettext('CPU usage'), + valueField: 'cpu', + maxField: 'cpuinfo', + renderer: PVE.Utils.render_node_cpu_usage + }, + { + itemId: 'wait', + iconCls: 'fa fa-fw fa-clock-o', + title: gettext('IO delay'), + valueField: 'wait', + rowspan: 2 + }, + { + itemId: 'load', + iconCls: 'fa fa-fw fa-tasks', + title: gettext('Load average'), + printBar: false, + textField: 'loadavg' + }, + { + xtype: 'box', + colspan: 2, + padding: '0 0 20 0' + }, + { + iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon', + itemId: 'memory', + title: gettext('RAM usage'), + valueField: 'memory', + maxField: 'memory', + renderer: PVE.Utils.render_node_size_usage + }, + { + itemId: 'ksm', + printBar: false, + title: gettext('KSM sharing'), + textField: 'ksm', + renderer: function(record) { + return PVE.Utils.render_size(record.shared); + }, + padding: '0 15 10 15' + }, + { + iconCls: 'fa fa-fw fa-hdd-o', + itemId: 'rootfs', + title: gettext('HD space') + '(root)', + valueField: 'rootfs', + maxField: 'rootfs', + renderer: PVE.Utils.render_node_size_usage + }, + { + iconCls: 'fa fa-fw fa-refresh', + itemId: 'swap', + printSize: true, + title: gettext('SWAP usage'), + valueField: 'swap', + maxField: 'swap', + renderer: PVE.Utils.render_node_size_usage + }, + { + xtype: 'box', + colspan: 2, + padding: '0 0 20 0' + }, + { + itemId: 'cpus', + colspan: 2, + printBar: false, + title: gettext('CPU(s)'), + textField: 'cpuinfo', + renderer: function(cpuinfo) { + return cpuinfo.cpus + " x " + cpuinfo.model + " (" + + cpuinfo.sockets.toString() + " " + + (cpuinfo.sockets > 1 ? + gettext('Sockets') : + gettext('Socket') + ) + ")"; + }, + value: '' + }, + { + itemId: 'kversion', + colspan: 2, + title: gettext('Kernel Version'), + printBar: false, + textField: 'kversion', + value: '' + }, + { + itemId: 'version', + colspan: 2, + printBar: false, + title: gettext('PVE Manager Version'), + textField: 'pveversion', + value: '' + } + ], + + updateTitle: function() { + var me = this; + var uptime = Proxmox.Utils.render_uptime(me.getRecordValue('uptime')); + me.setTitle(me.pveSelNode.data.node + ' (' + gettext('Uptime') + ': ' + uptime + ')'); + } + +}); +Ext.define('PVE.node.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeSummary', + + scrollable: true, + bodyPadding: 5, + + showVersions: function() { + var me = this; + + // Note: we use simply text/html here, because ExtJS grid has problems + // with cut&paste + + var nodename = me.pveSelNode.data.node; + + var view = Ext.createWidget('component', { + autoScroll: true, + padding: 5, + style: { + 'background-color': '#23272a', + 'white-space': 'pre', + 'font-family': 'monospace' + } + }); + + var win = Ext.create('Ext.window.Window', { + title: gettext('Package versions'), + width: 600, + height: 400, + layout: 'fit', + modal: true, + items: [ view ] + }); + + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: "/nodes/" + nodename + "/apt/versions", + method: 'GET', + failure: function(response, opts) { + win.close(); + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + win.show(); + var text = ''; + + Ext.Array.each(response.result.data, function(rec) { + var version = "not correctly installed"; + var pkg = rec.Package; + if (rec.OldVersion && rec.CurrentState === 'Installed') { + version = rec.OldVersion; + } + if (rec.RunningKernel) { + text += pkg + ': ' + version + ' (running kernel: ' + + rec.RunningKernel + ')\n'; + } else if (rec.ManagerVersion) { + text += pkg + ': ' + version + ' (running version: ' + + rec.ManagerVersion + ')\n'; + } else { + text += pkg + ': ' + version + '\n'; + } + }); + + view.update(Ext.htmlEncode(text)); + } + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + if (!me.statusStore) { + throw "no status storage specified"; + } + + var rstore = me.statusStore; + + var version_btn = new Ext.Button({ + text: gettext('Package versions'), + handler: function(){ + Proxmox.Utils.checked_command(function() { me.showVersions(); }); + } + }); + + var rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/rrddata", + model: 'pve-rrd-node' + }); + + Ext.apply(me, { + tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' } ], + items: [ + { + xtype: 'container', + layout: 'column', + defaults: { + minHeight: 320, + padding: 5, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1 + }, + 'width >= 1900': { + columnWidth: 0.5 + } + } + }, + items: [ + { + xtype: 'pveNodeStatus', + rstore: rstore, + width: 770, + pveSelNode: me.pveSelNode + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('CPU usage'), + fields: ['cpu','iowait'], + fieldTitles: [gettext('CPU usage'), gettext('IO delay')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Server load'), + fields: ['loadavg'], + fieldTitles: [gettext('Load average')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Memory usage'), + fields: ['memtotal','memused'], + fieldTitles: [gettext('Total'), gettext('RAM usage')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Network traffic'), + fields: ['netin','netout'], + store: rrdstore + } + ] + } + ], + listeners: { + activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); }, + destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); } + } + }); + + me.callParent(); + } +}); +/*global Blob*/ +Ext.define('PVE.node.SubscriptionKeyEdit', { + extend: 'Proxmox.window.Edit', + title: gettext('Upload Subscription Key'), + width: 300, + items: { + xtype: 'textfield', + name: 'key', + value: '', + fieldLabel: gettext('Subscription Key') + }, + initComponent : function() { + var me = this; + + me.callParent(); + + me.load(); + } +}); + +Ext.define('PVE.node.Subscription', { + extend: 'Proxmox.grid.ObjectGrid', + + alias: ['widget.pveNodeSubscription'], + + onlineHelp: 'getting_help', + + viewConfig: { + enableTextSelection: true + }, + + showReport: function() { + var me = this; + var nodename = me.pveSelNode.data.node; + + var getReportFileName = function() { + var now = Ext.Date.format(new Date(), 'D-d-F-Y-G-i'); + return me.nodename + '-report-' + now + '.txt'; + }; + + var view = Ext.createWidget('component', { + itemId: 'system-report-view', + scrollable: true, + style: { + 'background-color': '#23272a', + 'white-space': 'pre', + 'font-family': 'monospace', + padding: '5px' + } + }); + + var reportWindow = Ext.create('Ext.window.Window', { + title: gettext('System Report'), + width: 1024, + height: 600, + layout: 'fit', + modal: true, + buttons: [ + '->', + { + text: gettext('Download'), + handler: function() { + var fileContent = reportWindow.getComponent('system-report-view').html; + var fileName = getReportFileName(); + + // Internet Explorer + if (window.navigator.msSaveOrOpenBlob) { + navigator.msSaveOrOpenBlob(new Blob([fileContent]), fileName); + } else { + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + + encodeURIComponent(fileContent)); + element.setAttribute('download', fileName); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + } + } + ], + items: view + }); + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/nodes/' + me.nodename + '/report', + method: 'GET', + waitMsgTarget: me, + failure: function(response) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response) { + var report = Ext.htmlEncode(response.result.data); + reportWindow.show(); + view.update(report); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var reload = function() { + me.rstore.load(); + }; + + var baseurl = '/nodes/' + me.nodename + '/subscription'; + + var render_status = function(value) { + + var message = me.getObjectValue('message'); + + if (message) { + return value + ": " + message; + } + return value; + }; + + var rows = { + productname: { + header: gettext('Type') + }, + key: { + header: gettext('Subscription Key') + }, + status: { + header: gettext('Status'), + renderer: render_status + }, + message: { + visible: false + }, + serverid: { + header: gettext('Server ID') + }, + sockets: { + header: gettext('Sockets') + }, + checktime: { + header: gettext('Last checked'), + renderer: Proxmox.Utils.render_timestamp + }, + nextduedate: { + header: gettext('Next due date') + } + }; + + Ext.apply(me, { + url: '/api2/json' + baseurl, + cwidth1: 170, + tbar: [ + { + text: gettext('Upload Subscription Key'), + handler: function() { + var win = Ext.create('PVE.node.SubscriptionKeyEdit', { + url: '/api2/extjs/' + baseurl + }); + win.show(); + win.on('destroy', reload); + } + }, + { + text: gettext('Check'), + handler: function() { + Proxmox.Utils.API2Request({ + params: { force: 1 }, + url: baseurl, + method: 'POST', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: reload + }); + } + }, + { + text: gettext('System Report'), + handler: function() { + Proxmox.Utils.checked_command(function (){ me.showReport(); }); + } + } + ], + rows: rows, + listeners: { + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.node.CertificateView', { + extend: 'Ext.container.Container', + xtype: 'pveCertificatesView', + + onlineHelp: 'sysadmin_certificate_management', + + mixins: ['Proxmox.Mixin.CBind' ], + + items: [ + { + xtype: 'pveCertView', + border: 0, + cbind: { + nodename: '{nodename}' + } + }, + { + xtype: 'pveACMEView', + border: 0, + cbind: { + nodename: '{nodename}' + } + } + ] + +}); + +Ext.define('PVE.node.CertificateViewer', { + extend: 'Proxmox.window.Edit', + + title: gettext('Certificate'), + + fieldDefaults: { + labelWidth: 120 + }, + width: 800, + resizable: true, + + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Name'), + name: 'filename' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Fingerprint'), + name: 'fingerprint' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Issuer'), + name: 'issuer' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Subject'), + name: 'subject' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Valid Since'), + renderer: Proxmox.Utils.render_timestamp, + name: 'notbefore' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Expires'), + renderer: Proxmox.Utils.render_timestamp, + name: 'notafter' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Subject Alternative Names'), + name: 'san', + renderer: PVE.Utils.render_san + }, + { + xtype: 'textarea', + editable: false, + grow: true, + growMax: 200, + fieldLabel: gettext('Certificate'), + name: 'pem' + } + ], + + initComponent: function() { + var me = this; + + if (!me.cert) { + throw "no cert given"; + } + + if (!me.nodename) { + throw "no nodename given"; + } + + me.url = '/nodes/' + me.nodename + '/certificates/info'; + me.callParent(); + + // hide OK/Reset button, because we just want to show data + me.down('toolbar[dock=bottom]').setVisible(false); + + me.load({ + success: function(response) { + if (Ext.isArray(response.result.data)) { + Ext.Array.each(response.result.data, function(item) { + if (item.filename === me.cert) { + me.setValues(item); + return false; + } + }); + } + } + }); + } +}); + +Ext.define('PVE.node.CertUpload', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCertUpload', + + title: gettext('Upload Custom Certificate'), + resizable: false, + isCreate: true, + submitText: gettext('Upload'), + method: 'POST', + width: 600, + + apiCallDone: function(success, response, options) { + if (!success) { + return; + } + + var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); + Ext.getBody().mask(txt, ['pve-static-mask']); + // reload after 10 seconds automatically + Ext.defer(function() { + window.location.reload(true); + }, 10000); + }, + + items: [ + { + fieldLabel: gettext('Private Key (Optional)'), + labelAlign: 'top', + emptyText: gettext('No change'), + name: 'key', + xtype: 'textarea' + }, + { + xtype: 'filebutton', + text: gettext('From File'), + listeners: { + change: function(btn, e, value) { + var me = this.up('form'); + e = e.event; + Ext.Array.each(e.target.files, function(file) { + PVE.Utils.loadSSHKeyFromFile(file, function(res) { + me.down('field[name=key]').setValue(res); + }); + }); + btn.reset(); + } + } + }, + { + xtype: 'box', + autoEl: 'hr' + }, + { + fieldLabel: gettext('Certificate Chain'), + labelAlign: 'top', + allowBlank: false, + name: 'certificates', + xtype: 'textarea' + }, + { + xtype: 'filebutton', + text: gettext('From File'), + listeners: { + change: function(btn, e, value) { + var me = this.up('form'); + e = e.event; + Ext.Array.each(e.target.files, function(file) { + PVE.Utils.loadSSHKeyFromFile(file, function(res) { + me.down('field[name=certificates]').setValue(res); + }); + }); + btn.reset(); + } + } + }, + { + xtype: 'hidden', + name: 'restart', + value: '1' + }, + { + xtype: 'hidden', + name: 'force', + value: '1' + } + ], + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + me.url = '/nodes/' + me.nodename + '/certificates/custom'; + + me.callParent(); + } +}); + +Ext.define('pve-certificate', { + extend: 'Ext.data.Model', + + fields: [ 'filename', 'fingerprint', 'issuer', 'notafter', 'notbefore', 'subject', 'san' ], + idProperty: 'filename' +}); + +Ext.define('PVE.node.Certificates', { + extend: 'Ext.grid.Panel', + xtype: 'pveCertView', + + tbar: [ + { + xtype: 'button', + text: gettext('Upload Custom Certificate'), + handler: function() { + var me = this.up('grid'); + var win = Ext.create('PVE.node.CertUpload', { + nodename: me.nodename + }); + win.show(); + win.on('destroy', me.reload, me); + } + }, + { + xtype: 'button', + itemId: 'deletebtn', + text: gettext('Delete Custom Certificate'), + handler: function() { + var me = this.up('grid'); + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/certificates/custom?restart=1', + method: 'DELETE', + success: function(response, opt) { + var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); + Ext.getBody().mask(txt, ['pve-static-mask']); + // reload after 10 seconds automatically + Ext.defer(function() { + window.location.reload(true); + }, 10000); + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }, + '-', + { + xtype: 'proxmoxButton', + itemId: 'viewbtn', + disabled: true, + text: gettext('View Certificate'), + handler: function() { + var me = this.up('grid'); + me.view_certificate(); + } + } + ], + + columns: [ + { + header: gettext('File'), + width: 150, + dataIndex: 'filename' + }, + { + header: gettext('Issuer'), + flex: 1, + dataIndex: 'issuer' + }, + { + header: gettext('Subject'), + flex: 1, + dataIndex: 'subject' + }, + { + header: gettext('Valid Since'), + width: 150, + dataIndex: 'notbefore', + renderer: Proxmox.Utils.render_timestamp + }, + { + header: gettext('Expires'), + width: 150, + dataIndex: 'notafter', + renderer: Proxmox.Utils.render_timestamp + }, + { + header: gettext('Subject Alternative Names'), + flex: 1, + dataIndex: 'san', + renderer: PVE.Utils.render_san + }, + { + header: gettext('Fingerprint'), + dataIndex: 'fingerprint', + hidden: true + }, + { + header: gettext('PEM'), + dataIndex: 'pem', + hidden: true + } + ], + + reload: function() { + var me = this; + me.rstore.load(); + }, + + set_button_status: function() { + var me = this; + var rec = me.rstore.getById('pveproxy-ssl.pem'); + + me.down('#deletebtn').setDisabled(!rec); + }, + + view_certificate: function() { + var me = this; + var selection = me.getSelection(); + if (!selection || selection.length < 1) { + return; + } + var win = Ext.create('PVE.node.CertificateViewer', { + cert: selection[0].data.filename, + nodename : me.nodename + }); + win.show(); + }, + + listeners: { + itemdblclick: 'view_certificate' + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + me.rstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'certs-' + me.nodename, + model: 'pve-certificate', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/certificates/info' + } + }); + + me.store = { + type: 'diff', + rstore: me.rstore + }; + + me.callParent(); + + me.mon(me.rstore, 'load', me.set_button_status, me); + me.rstore.startUpdate(); + } +}); +Ext.define('PVE.node.ACMEEditor', { + extend: 'Proxmox.window.Edit', + xtype: 'pveACMEEditor', + + subject: gettext('Domains'), + items: [ + { + xtype: 'inputpanel', + items: [ + { + xtype: 'textarea', + fieldLabel: gettext('Domains'), + emptyText: "domain1.example.com\ndomain2.example.com", + name: 'domains' + } + ], + onGetValues: function(values) { + if (!values.domains) { + return { + 'delete': 'acme' + }; + } + var domains = values.domains.split(/\n/).join(';'); + return { + 'acme': 'domains=' + domains + }; + } + } + ], + + initComponent: function() { + var me = this; + me.callParent(); + + me.load({ + success: function(response, opts) { + var res = PVE.Parser.parseACME(response.result.data.acme); + if (res) { + res.domains = res.domains.join(' '); + me.setValues(res); + } + } + }); + } +}); + +Ext.define('PVE.node.ACMEAccountCreate', { + extend: 'Proxmox.window.Edit', + + width: 400, + title: gettext('Register Account'), + isCreate: true, + method: 'POST', + submitText: gettext('Register'), + url: '/cluster/acme/account', + showTaskViewer: true, + + items: [ + { + xtype: 'proxmoxComboGrid', + name: 'directory', + allowBlank: false, + valueField: 'url', + displayField: 'name', + fieldLabel: gettext('ACME Directory'), + store: { + autoLoad: true, + fields: ['name', 'url'], + idProperty: ['name'], + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/acme/directories' + }, + sorters: { + property: 'name', + order: 'ASC' + } + }, + listConfig: { + columns: [ + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + header: gettext('URL'), + dataIndex: 'url', + flex: 1 + } + ] + }, + listeners: { + change: function(combogrid, value) { + var me = this; + if (!value) { + return; + } + + var disp = me.up('window').down('#tos_url_display'); + var field = me.up('window').down('#tos_url'); + var checkbox = me.up('window').down('#tos_checkbox'); + + disp.setValue(gettext('Loading')); + field.setValue(undefined); + checkbox.setValue(undefined); + + Proxmox.Utils.API2Request({ + url: '/cluster/acme/tos', + method: 'GET', + params: { + directory: value + }, + success: function(response, opt) { + me.up('window').down('#tos_url').setValue(response.result.data); + me.up('window').down('#tos_url_display').setValue(response.result.data); + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + } + }, + { + xtype: 'displayfield', + itemId: 'tos_url_display', + fieldLabel: gettext('Terms of Service'), + renderer: PVE.Utils.render_optional_url, + name: 'tos_url_display' + }, + { + xtype: 'hidden', + itemId: 'tos_url', + name: 'tos_url' + }, + { + xtype: 'proxmoxcheckbox', + itemId: 'tos_checkbox', + fieldLabel: gettext('Accept TOS'), + submitValue: false, + validateValue: function(value) { + if (value && this.checked) { + return true; + } + return false; + } + }, + { + xtype: 'textfield', + name: 'contact', + vtype: 'email', + allowBlank: false, + fieldLabel: gettext('E-Mail') + } + ] + +}); + +Ext.define('PVE.node.ACMEAccountView', { + extend: 'Proxmox.window.Edit', + + width: 600, + fieldDefaults: { + labelWidth: 140 + }, + + title: gettext('Account'), + + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('E-Mail'), + name: 'email' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Created'), + name: 'createdAt' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Status'), + name: 'status' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Directory'), + renderer: PVE.Utils.render_optional_url, + name: 'directory' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Terms of Services'), + renderer: PVE.Utils.render_optional_url, + name: 'tos' + } + ], + + initComponent: function() { + var me = this; + + if (!me.accountname) { + throw "no account name defined"; + } + + me.url = '/cluster/acme/account/' + me.accountname; + + me.callParent(); + + // hide OK/Reset button, because we just want to show data + me.down('toolbar[dock=bottom]').setVisible(false); + + me.load({ + success: function(response) { + var data = response.result.data; + data.email = data.account.contact[0]; + data.createdAt = data.account.createdAt; + data.status = data.account.status; + me.setValues(data); + } + }); + } +}); + +Ext.define('PVE.node.ACME', { + extend: 'Proxmox.grid.ObjectGrid', + xtype: 'pveACMEView', + + margin: '10 0 0 0', + title: 'ACME', + + tbar: [ + { + xtype: 'button', + itemId: 'edit', + text: gettext('Edit Domains'), + handler: function() { + this.up('grid').run_editor(); + } + }, + { + xtype: 'button', + itemId: 'createaccount', + text: gettext('Register Account'), + handler: function() { + var me = this.up('grid'); + var win = Ext.create('PVE.node.ACMEAccountCreate', { + taskDone: function() { + me.load_account(); + me.reload(); + } + }); + win.show(); + } + }, + { + xtype: 'button', + itemId: 'viewaccount', + text: gettext('View Account'), + handler: function() { + var me = this.up('grid'); + var win = Ext.create('PVE.node.ACMEAccountView', { + accountname: 'default' + }); + win.show(); + } + }, + { + xtype: 'button', + itemId: 'order', + text: gettext('Order Certificate'), + handler: function() { + var me = this.up('grid'); + + Proxmox.Utils.API2Request({ + method: 'POST', + params: { + force: 1 + }, + url: '/nodes/' + me.nodename + '/certificates/acme/certificate', + success: function(response, opt) { + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: response.result.data, + taskDone: function(success) { + me.certificate_order_finished(success); + } + }); + win.show(); + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + } + ], + + certificate_order_finished: function(success) { + if (!success) { + return; + } + var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); + Ext.getBody().mask(txt, ['pve-static-mask']); + // reload after 10 seconds automatically + Ext.defer(function() { + window.location.reload(true); + }, 10000); + }, + + set_button_status: function() { + var me = this; + + var account = !!me.account; + var acmeObj = PVE.Parser.parseACME(me.getObjectValue('acme')); + var domains = acmeObj ? acmeObj.domains.length : 0; + + var order = me.down('#order'); + order.setVisible(account); + order.setDisabled(!account || !domains); + + me.down('#createaccount').setVisible(!account); + me.down('#viewaccount').setVisible(account); + }, + + load_account: function() { + var me = this; + + // for now we only use the 'default' account + Proxmox.Utils.API2Request({ + url: '/cluster/acme/account/default', + success: function(response, opt) { + me.account = response.result.data; + me.set_button_status(); + }, + failure: function(response, opt) { + me.account = undefined; + me.set_button_status(); + } + }); + }, + + run_editor: function() { + var me = this; + var win = Ext.create(me.rows.acme.editor, me.editorConfig); + win.show(); + win.on('destroy', me.reload, me); + }, + + listeners: { + itemdblclick: 'run_editor' + }, + + // account data gets loaded here + account: undefined, + + disableSelection: true, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + me.url = '/api2/json/nodes/' + me.nodename + '/config'; + + me.editorConfig = { + url: '/api2/extjs/nodes/' + me.nodename + '/config' + }; + /*jslint confusion: true*/ + /*acme is a string above*/ + me.rows = { + acme: { + defaultValue: '', + header: gettext('Domains'), + editor: 'PVE.node.ACMEEditor', + renderer: function(value) { + var acmeObj = PVE.Parser.parseACME(value); + if (acmeObj) { + return acmeObj.domains.join('
'); + } + return Proxmox.Utils.noneText; + } + } + }; + /*jslint confusion: false*/ + + me.callParent(); + me.mon(me.rstore, 'load', me.set_button_status, me); + me.rstore.startUpdate(); + me.load_account(); + } +}); +Ext.define('PVE.node.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.node.Config', + + onlineHelp: 'chapter_system_administration', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + nodename + "/status", + interval: 1000 + }); + + var node_command = function(cmd) { + Proxmox.Utils.API2Request({ + params: { command: cmd }, + url: '/nodes/' + nodename + '/status', + method: 'POST', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + var actionBtn = Ext.create('Ext.Button', { + text: gettext('Bulk Actions'), + iconCls: 'fa fa-fw fa-ellipsis-v', + disabled: !caps.nodes['Sys.PowerMgmt'], + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Bulk Start'), + iconCls: 'fa fa-fw fa-play', + handler: function() { + var win = Ext.create('PVE.window.BulkAction', { + nodename: nodename, + title: gettext('Bulk Start'), + btnText: gettext('Start'), + action: 'startall' + }); + win.show(); + } + }, + { + text: gettext('Bulk Stop'), + iconCls: 'fa fa-fw fa-stop', + handler: function() { + var win = Ext.create('PVE.window.BulkAction', { + nodename: nodename, + title: gettext('Bulk Stop'), + btnText: gettext('Stop'), + action: 'stopall' + }); + win.show(); + } + }, + { + text: gettext('Bulk Migrate'), + iconCls: 'fa fa-fw fa-send-o', + handler: function() { + var win = Ext.create('PVE.window.BulkAction', { + nodename: nodename, + title: gettext('Bulk Migrate'), + btnText: gettext('Migrate'), + action: 'migrateall' + }); + win.show(); + } + } + ] + }) + }); + + var restartBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Reboot'), + disabled: !caps.nodes['Sys.PowerMgmt'], + dangerous: true, + confirmMsg: Ext.String.format(gettext("Reboot node '{0}'?"), nodename), + handler: function() { + node_command('reboot'); + }, + iconCls: 'fa fa-undo' + }); + + var shutdownBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Shutdown'), + disabled: !caps.nodes['Sys.PowerMgmt'], + dangerous: true, + confirmMsg: Ext.String.format(gettext("Shutdown node '{0}'?"), nodename), + handler: function() { + node_command('shutdown'); + }, + iconCls: 'fa fa-power-off' + }); + + var shellBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.nodes['Sys.Console'], + text: gettext('Shell'), + consoleType: 'shell', + nodename: nodename + }); + + me.items = []; + + Ext.apply(me, { + title: gettext('Node') + " '" + nodename + "'", + hstateid: 'nodetab', + defaults: { statusStore: me.statusStore }, + tbar: [ restartBtn, shutdownBtn, shellBtn, actionBtn] + }); + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + title: gettext('Summary'), + iconCls: 'fa fa-book', + itemId: 'summary', + xtype: 'pveNodeSummary' + }, + { + title: gettext('Notes'), + iconCls: 'fa fa-sticky-note-o', + itemId: 'notes', + xtype: 'pveNotesView' + } + ); + } + + if (caps.nodes['Sys.Console']) { + me.items.push( + { + title: gettext('Shell'), + iconCls: 'fa fa-terminal', + itemId: 'jsconsole', + xtype: 'pveNoVncConsole', + consoleType: 'shell', + xtermjs: true, + nodename: nodename + } + ); + } + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + title: gettext('System'), + iconCls: 'fa fa-cogs', + itemId: 'services', + expandedOnInit: true, + startOnlyServices: { + 'pveproxy': true, + 'pvedaemon': true, + 'pve-cluster': true + }, + nodename: nodename, + onlineHelp: 'pve_service_daemons', + xtype: 'proxmoxNodeServiceView' + }, + { + title: gettext('Network'), + iconCls: 'fa fa-exchange', + itemId: 'network', + groups: ['services'], + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + xtype: 'proxmoxNodeNetworkView' + }, + { + title: gettext('Certificates'), + iconCls: 'fa fa-certificate', + itemId: 'certificates', + groups: ['services'], + nodename: nodename, + xtype: 'pveCertificatesView' + }, + { + title: gettext('DNS'), + iconCls: 'fa fa-globe', + groups: ['services'], + itemId: 'dns', + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + xtype: 'proxmoxNodeDNSView' + }, + { + title: gettext('Hosts'), + iconCls: 'fa fa-globe', + groups: ['services'], + itemId: 'hosts', + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + xtype: 'proxmoxNodeHostsView' + }, + { + title: gettext('Time'), + itemId: 'time', + groups: ['services'], + nodename: nodename, + xtype: 'proxmoxNodeTimeView', + iconCls: 'fa fa-clock-o' + }); + } + + if (caps.nodes['Sys.Syslog']) { + me.items.push({ + title: 'Syslog', + iconCls: 'fa fa-list', + groups: ['services'], + disabled: !caps.nodes['Sys.Syslog'], + itemId: 'syslog', + xtype: 'proxmoxJournalView', + url: "/api2/extjs/nodes/" + nodename + "/journal" + }); + + if (caps.nodes['Sys.Modify']) { + me.items.push({ + title: gettext('Updates'), + iconCls: 'fa fa-refresh', + disabled: !caps.nodes['Sys.Console'], + // do we want to link to system updates instead? + itemId: 'apt', + xtype: 'proxmoxNodeAPT', + upgradeBtn: { + xtype: 'pveConsoleButton', + disabled: Proxmox.UserName !== 'root@pam', + text: gettext('Upgrade'), + consoleType: 'upgrade', + nodename: nodename + }, + nodename: nodename + }); + } + } + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + xtype: 'pveFirewallRules', + iconCls: 'fa fa-shield', + title: gettext('Firewall'), + allow_iface: true, + base_url: '/nodes/' + nodename + '/firewall/rules', + list_refs_url: '/cluster/firewall/refs', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + title: gettext('Options'), + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_host_specific_configuration', + groups: ['firewall'], + base_url: '/nodes/' + nodename + '/firewall/options', + fwtype: 'node', + itemId: 'firewall-options' + }); + } + + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + title: gettext('Disks'), + itemId: 'storage', + expandedOnInit: true, + iconCls: 'fa fa-hdd-o', + xtype: 'pveNodeDiskList' + }, + { + title: 'LVM', + itemId: 'lvm', + onlineHelp: 'chapter_lvm', + iconCls: 'fa fa-square', + groups: ['storage'], + xtype: 'pveLVMList' + }, + { + title: 'LVM-Thin', + itemId: 'lvmthin', + onlineHelp: 'chapter_lvm', + iconCls: 'fa fa-square-o', + groups: ['storage'], + xtype: 'pveLVMThinList' + }, + { + title: Proxmox.Utils.directoryText, + itemId: 'directory', + onlineHelp: 'chapter_storage', + iconCls: 'fa fa-folder', + groups: ['storage'], + xtype: 'pveDirectoryList' + }, + { + title: 'ZFS', + itemId: 'zfs', + onlineHelp: 'chapter_zfs', + iconCls: 'fa fa-th-large', + groups: ['storage'], + xtype: 'pveZFSList' + }, + { + title: 'Ceph', + itemId: 'ceph', + iconCls: 'fa fa-ceph', + xtype: 'pveNodeCephStatus' + }, + { + xtype: 'pveReplicaView', + iconCls: 'fa fa-retweet', + title: gettext('Replication'), + itemId: 'replication' + }, + { + xtype: 'pveNodeCephConfigCrush', + title: gettext('Configuration'), + iconCls: 'fa fa-gear', + groups: ['ceph'], + itemId: 'ceph-config' + }, + { + xtype: 'pveNodeCephMonMgr', + title: gettext('Monitor'), + iconCls: 'fa fa-tv', + groups: ['ceph'], + itemId: 'ceph-monlist' + }, + { + xtype: 'pveNodeCephOsdTree', + title: 'OSD', + iconCls: 'fa fa-hdd-o', + groups: ['ceph'], + itemId: 'ceph-osdtree' + }, + { + xtype: 'pveNodeCephFSPanel', + title: 'CephFS', + iconCls: 'fa fa-folder', + groups: ['ceph'], + nodename: nodename, + itemId: 'ceph-cephfspanel' + }, + { + xtype: 'pveNodeCephPoolList', + title: 'Pools', + iconCls: 'fa fa-sitemap', + groups: ['ceph'], + itemId: 'ceph-pools' + } + ); + } + + if (caps.nodes['Sys.Syslog']) { + me.items.push( + { + xtype: 'proxmoxLogView', + title: gettext('Log'), + iconCls: 'fa fa-list', + groups: ['firewall'], + onlineHelp: 'chapter_pve_firewall', + url: '/api2/extjs/nodes/' + nodename + '/firewall/log', + itemId: 'firewall-fwlog' + }, + { + title: gettext('Log'), + itemId: 'ceph-log', + iconCls: 'fa fa-list', + groups: ['ceph'], + onlineHelp: 'chapter_pveceph', + xtype: 'cephLogView', + url: "/api2/extjs/nodes/" + nodename + "/ceph/log", + nodename: nodename + }); + } + + me.items.push( + { + title: gettext('Task History'), + iconCls: 'fa fa-list', + itemId: 'tasks', + nodename: nodename, + xtype: 'proxmoxNodeTasks' + }, + { + title: gettext('Subscription'), + iconCls: 'fa fa-support', + itemId: 'support', + xtype: 'pveNodeSubscription', + nodename: nodename + } + ); + + me.callParent(); + + me.mon(me.statusStore, 'load', function(s, records, success) { + var uptimerec = s.data.get('uptime'); + var powermgmt = uptimerec ? uptimerec.data.value : false; + if (!caps.nodes['Sys.PowerMgmt']) { + powermgmt = false; + } + restartBtn.setDisabled(!powermgmt); + shutdownBtn.setDisabled(!powermgmt); + shellBtn.setDisabled(!powermgmt); + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.window.Migrate', { + extend: 'Ext.window.Window', + + vmtype: undefined, + nodename: undefined, + vmid: undefined, + + viewModel: { + data: { + vmid: undefined, + nodename: undefined, + vmtype: undefined, + running: false, + qemu: { + onlineHelp: 'qm_migration', + commonName: 'VM' + }, + lxc: { + onlineHelp: 'pct_migration', + commonName: 'CT' + }, + migration: { + possible: true, + preconditions: [], + 'with-local-disks': 0, + mode: undefined, + allowedNodes: undefined + } + + }, + + formulas: { + setMigrationMode: function(get) { + if (get('running')){ + if (get('vmtype') === 'qemu') { + return gettext('Online'); + } else { + return gettext('Restart Mode'); + } + } else { + return gettext('Offline'); + } + }, + setStorageselectorHidden: function(get) { + if (get('migration.with-local-disks') && get('running')) { + return false; + } else { + return true; + } + } + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'panel[reference=formPanel]': { + validityChange: function(panel, isValid) { + this.getViewModel().set('migration.possible', isValid); + this.checkMigratePreconditions(); + } + } + }, + + init: function(view) { + var me = this, + vm = view.getViewModel(); + + if (!view.nodename) { + throw "missing custom view config: nodename"; + } + vm.set('nodename', view.nodename); + + if (!view.vmid) { + throw "missing custom view config: vmid"; + } + vm.set('vmid', view.vmid); + + if (!view.vmtype) { + throw "missing custom view config: vmtype"; + } + vm.set('vmtype', view.vmtype); + + + view.setTitle( + Ext.String.format('{0} {1}{2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid) + ); + me.lookup('proxmoxHelpButton').setHelpConfig({ + onlineHelp: vm.get(view.vmtype).onlineHelp + }); + me.checkMigratePreconditions(); + me.lookup('formPanel').isValid(); + + }, + + onTargetChange: function (nodeSelector) { + //Always display the storages of the currently seleceted migration target + this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value); + this.checkMigratePreconditions(); + }, + + startMigration: function() { + var me = this, + view = me.getView(), + vm = me.getViewModel(); + + var values = me.lookup('formPanel').getValues(); + var params = { + target: values.target + }; + + if (vm.get('migration.mode')) { + params[vm.get('migration.mode')] = 1; + } + if (vm.get('migration.with-local-disks')) { + params['with-local-disks'] = 1; + } + //only submit targetstorage if vm is running, storage migration to different storage is only possible online + if (vm.get('migration.with-local-disks') && vm.get('running')) { + params.targetstorage = values.targetstorage; + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate', + waitMsgTarget: view, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var extraTitle = Ext.String.format(' ({0} ---> {1})', vm.get('nodename'), params.target); + + Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + extraTitle: extraTitle + }).show(); + + view.close(); + } + }); + + }, + + checkMigratePreconditions: function() { + var me = this, + vm = me.getViewModel(); + + + var vmrec = PVE.data.ResourceStore.findRecord('vmid', vm.get('vmid'), + 0, false, false, true); + if (vmrec && vmrec.data && vmrec.data.running) { + vm.set('running', true); + } + + if (vm.get('vmtype') === 'qemu') { + me.checkQemuPreconditions(); + } else { + me.checkLxcPreconditions(); + } + me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')]; + + // Only allow nodes where the local storage is available in case of offline migration + // where storage migration is not possible + me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes'); + + me.lookup('formPanel').isValid(); + + }, + + checkQemuPreconditions: function() { + var me = this, + vm = me.getViewModel(), + migrateStats; + + if (vm.get('running')) { + vm.set('migration.mode', 'online'); + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate', + method: 'GET', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + migrateStats = response.result.data; + if (migrateStats.running) { + vm.set('running', true); + } + // Get migration object from viewmodel to prevent + // to many bind callbacks + var migration = vm.get('migration'); + migration.preconditions = []; + + if (migrateStats.allowed_nodes) { + migration.allowedNodes = migrateStats.allowed_nodes; + var target = me.lookup('pveNodeSelector').value; + if (target.length && !migrateStats.allowed_nodes.includes(target)) { + let disallowed = migrateStats.not_allowed_nodes[target]; + let missing_storages = disallowed.unavailable_storages.join(', '); + + migration.possible = false; + migration.preconditions.push({ + text: 'Storage (' + missing_storages + ') not available on selected target. ' + + 'Start VM to use live storage migration or select other target node', + severity: 'error' + }); + } + } + + if (migrateStats.local_resources.length) { + migration.possible = false; + migration.preconditions.push({ + text: 'Can\'t migrate VM with local resources: '+ migrateStats.local_resources.join(', '), + severity: 'error' + }); + } + + if (migrateStats.local_disks.length) { + + migrateStats.local_disks.forEach(function (disk) { + if (disk.cdrom && disk.cdrom === 1) { + migration.possible = false; + migration.preconditions.push({ + text: "Can't migrate VM with local CD/DVD", + severity: 'error' + }); + + } else if (!disk.referenced_in_config) { + migration.possible = false; + migration.preconditions.push({ + text: 'Found not referenced/unused disk via storage: '+ disk.volid, + severity: 'error' + }); + } else { + migration['with-local-disks'] = 1; + migration.preconditions.push({ + text:'Migration with local disk might take long: ' + disk.volid + +' (' + PVE.Utils.render_size(disk.size) + ')', + severity: 'warning' + }); + } + }); + + } + + vm.set('migration', migration); + + } + }); + }, + checkLxcPreconditions: function() { + var me = this, + vm = me.getViewModel(); + if (vm.get('running')) { + vm.set('migration.mode', 'restart'); + } + } + + + }, + + width: 600, + modal: true, + layout: { + type: 'vbox', + align: 'stretch' + }, + border: false, + items: [ + { + xtype: 'form', + reference: 'formPanel', + bodyPadding: 10, + border: false, + layout: { + type: 'column' + }, + items: [ + { + xtype: 'container', + columnWidth: 0.5, + items: [{ + xtype: 'displayfield', + name: 'source', + fieldLabel: gettext('Source node'), + bind: { + value: '{nodename}' + } + }, + { + xtype: 'displayfield', + reference: 'migrationMode', + fieldLabel: gettext('Mode'), + bind: { + value: '{setMigrationMode}' + } + }] + }, + { + xtype: 'container', + columnWidth: 0.5, + items: [{ + xtype: 'pveNodeSelector', + reference: 'pveNodeSelector', + name: 'target', + fieldLabel: gettext('Target node'), + allowBlank: false, + disallowedNodes: undefined, + onlineValidator: true, + listeners: { + change: 'onTargetChange' + } + }, + { + xtype: 'pveStorageSelector', + reference: 'pveDiskStorageSelector', + name: 'targetstorage', + fieldLabel: gettext('Target storage'), + storageContent: 'images', + bind: { + hidden: '{setStorageselectorHidden}' + } + }] + } + ] + }, + { + xtype: 'gridpanel', + reference: 'preconditionGrid', + selectable: false, + flex: 1, + columns: [{ + text: '', + dataIndex: 'severity', + renderer: function(v) { + switch (v) { + case 'warning': + return ' '; + case 'error': + return ''; + default: + return v; + } + }, + width: 35 + }, + { + text: 'Info', + dataIndex: 'text', + cellWrap: true, + flex: 1 + }], + bind: { + hidden: '{!migration.preconditions.length}', + store: { + fields: ['severity','text'], + data: '{migration.preconditions}' + } + } + } + + ], + buttons: [ + { + xtype: 'proxmoxHelpButton', + reference: 'proxmoxHelpButton', + onlineHelp: 'pct_migration', + listenToGlobalEvent: false, + hidden: false + }, + '->', + { + xtype: 'button', + reference: 'submitButton', + text: gettext('Migrate'), + handler: 'startMigration', + bind: { + disabled: '{!migration.possible}' + } + } + ] +}); +Ext.define('PVE.window.BulkAction', { + extend: 'Ext.window.Window', + + resizable: true, + width: 800, + modal: true, + layout: { + type: 'fit' + }, + border: false, + + // the action to be set + // currently there are + // startall + // migrateall + // stopall + action: undefined, + + submit: function(params) { + var me = this; + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/' + me.action, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + me.hide(); + win.on('destroy', function() { + me.close(); + }); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.action) { + throw "no action specified"; + } + + if (!me.btnText) { + throw "no button text specified"; + } + + if (!me.title) { + throw "no title specified"; + } + + var items = []; + + if (me.action === 'migrateall') { + /*jslint confusion: true*/ + /*value is string and number*/ + items.push( + { + xtype: 'pveNodeSelector', + name: 'target', + disallowedNodes: [me.nodename], + fieldLabel: gettext('Target node'), + allowBlank: false, + onlineValidator: true + }, + { + xtype: 'proxmoxintegerfield', + name: 'maxworkers', + minValue: 1, + maxValue: 100, + value: 1, + fieldLabel: gettext('Parallel jobs'), + allowBlank: false + }, + { + itemId: 'lxcwarning', + xtype: 'displayfield', + userCls: 'pve-hint', + value: 'Warning: Running CTs will be migrated in Restart Mode.', + hidden: true // only visible if running container chosen + } + ); + /*jslint confusion: false*/ + } else if (me.action === 'startall') { + items.push({ + xtype: 'hiddenfield', + name: 'force', + value: 1 + }); + } + + items.push({ + xtype: 'vmselector', + itemId: 'vms', + name: 'vms', + flex: 1, + height: 300, + selectAll: true, + allowBlank: false, + nodename: me.nodename, + action: me.action, + listeners: { + selectionchange: function(vmselector, records) { + if (me.action == 'migrateall') { + var showWarning = records.some(function(item) { + return (item.data.type == 'lxc' && + item.data.status == 'running'); + }); + me.down('#lxcwarning').setVisible(showWarning); + } + } + } + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + layout: { + type: 'vbox', + align: 'stretch' + }, + fieldDefaults: { + labelWidth: 300, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn = Ext.create('Ext.Button', { + text: me.btnText, + handler: function() { + form.isValid(); + me.submit(form.getValues()); + } + }); + + Ext.apply(me, { + items: [ me.formPanel ], + buttons: [ submitBtn ] + }); + + me.callParent(); + + form.on('validitychange', function() { + var valid = form.isValid(); + submitBtn.setDisabled(!valid); + }); + form.isValid(); + } +}); +Ext.define('PVE.window.Clone', { + extend: 'Ext.window.Window', + + resizable: false, + + isTemplate: false, + + onlineHelp: 'qm_copy_and_clone', + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'panel[reference=cloneform]': { + validitychange: 'disableSubmit' + } + }, + disableSubmit: function(form) { + this.lookupReference('submitBtn').setDisabled(!form.isValid()); + } + }, + + statics: { + // display a snapshot selector only if needed + wrap: function(nodename, vmid, isTemplate, guestType) { + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/' + guestType + '/' + vmid +'/snapshot', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts) { + var snapshotList = response.result.data; + var hasSnapshots = snapshotList.length === 1 && + snapshotList[0].name === 'current' ? false : true; + + Ext.create('PVE.window.Clone', { + nodename: nodename, + guestType: guestType, + vmid: vmid, + isTemplate: isTemplate, + hasSnapshots: hasSnapshots + }).show(); + } + }); + } + }, + + create_clone: function(values) { + var me = this; + + var params = { newid: values.newvmid }; + + if (values.snapname && values.snapname !== 'current') { + params.snapname = values.snapname; + } + + if (values.pool) { + params.pool = values.pool; + } + + if (values.name) { + if (me.guestType === 'lxc') { + params.hostname = values.name; + } else { + params.name = values.name; + } + } + + if (values.target) { + params.target = values.target; + } + + if (values.clonemode === 'copy') { + params.full = 1; + if (values.hdstorage) { + params.storage = values.hdstorage; + if (values.diskformat && me.guestType !== 'lxc') { + params.format = values.diskformat; + } + } + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/clone', + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + me.close(); + } + }); + + }, + + // disable the Storage selector when clone mode is linked clone + updateVisibility: function() { + var me = this; + var clonemode = me.lookupReference('clonemodesel').getValue(); + var disksel = me.lookup('diskselector'); + disksel.setDisabled(clonemode === 'clone'); + }, + + // add to the list of valid nodes each node where + // all the VM disks are available + verifyFeature: function() { + var me = this; + + var snapname = me.lookupReference('snapshotsel').getValue(); + var clonemode = me.lookupReference('clonemodesel').getValue(); + + var params = { feature: clonemode }; + if (snapname !== 'current') { + params.snapname = snapname; + } + + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/feature', + params: params, + method: 'GET', + failure: function(response, opts) { + me.lookupReference('submitBtn').setDisabled(true); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var res = response.result.data; + + me.lookupReference('targetsel').allowedNodes = res.nodes; + me.lookupReference('targetsel').validate(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.snapname) { + me.snapname = 'current'; + } + + if (!me.guestType) { + throw "no Guest Type specified"; + } + + var titletext = me.guestType === 'lxc' ? 'CT' : 'VM'; + if (me.isTemplate) { + titletext += ' Template'; + } + me.title = "Clone " + titletext + " " + me.vmid; + + var col1 = []; + var col2 = []; + + col1.push({ + xtype: 'pveNodeSelector', + name: 'target', + reference: 'targetsel', + fieldLabel: gettext('Target node'), + selectCurNode: true, + allowBlank: false, + onlineValidator: true, + listeners: { + change: function(f, value) { + me.lookupReference('hdstorage').setTargetNode(value); + } + } + }); + + var modelist = [['copy', gettext('Full Clone')]]; + if (me.isTemplate) { + modelist.push(['clone', gettext('Linked Clone')]); + } + + col1.push({ + xtype: 'pveGuestIDSelector', + name: 'newvmid', + guestType: me.guestType, + value: '', + loadNextFreeID: true, + validateExists: false + }, + { + xtype: 'textfield', + name: 'name', + allowBlank: true, + fieldLabel: me.guestType === 'lxc' ? gettext('Hostname') : gettext('Name') + }, + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true + } + ); + + col2.push({ + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Mode'), + name: 'clonemode', + reference: 'clonemodesel', + allowBlank: false, + hidden: !me.isTemplate, + value: me.isTemplate ? 'clone' : 'copy', + comboItems: modelist, + listeners: { + change: function(t, value) { + me.updateVisibility(); + me.verifyFeature(); + } + } + }, + { + xtype: 'PVE.form.SnapshotSelector', + name: 'snapname', + reference: 'snapshotsel', + fieldLabel: gettext('Snapshot'), + nodename: me.nodename, + guestType: me.guestType, + vmid: me.vmid, + hidden: me.isTemplate || !me.hasSnapshots ? true : false, + disabled: false, + allowBlank: false, + value : me.snapname, + listeners: { + change: function(f, value) { + me.verifyFeature(); + } + } + }, + { + xtype: 'pveDiskStorageSelector', + reference: 'diskselector', + nodename: me.nodename, + autoSelect: false, + hideSize: true, + hideSelection: true, + storageLabel: gettext('Target Storage'), + allowBlank: true, + storageContent: me.guestType === 'qemu' ? 'images' : 'rootdir', + emptyText: gettext('Same as source'), + disabled: me.isTemplate ? true : false // because default mode is clone for templates + }); + + var formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + reference: 'cloneform', + border: false, + layout: 'column', + defaultType: 'container', + columns: 2, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [ + { + columnWidth: 0.5, + padding: '0 10 0 0', + layout: 'anchor', + items: col1 + }, + { + columnWidth: 0.5, + padding: '0 0 0 10', + layout: 'anchor', + items: col2 + } + ] + }); + + Ext.apply(me, { + modal: true, + width: 600, + height: 250, + border: false, + layout: 'fit', + buttons: [ { + xtype: 'proxmoxHelpButton', + listenToGlobalEvent: false, + hidden: false, + onlineHelp: me.onlineHelp + }, + '->', + { + reference: 'submitBtn', + text: gettext('Clone'), + disabled: true, + handler: function() { + var cloneForm = me.lookupReference('cloneform'); + if (cloneForm.isValid()) { + me.create_clone(cloneForm.getValues()); + } + } + } ], + items: [ formPanel ] + }); + + me.callParent(); + + me.verifyFeature(); + } +}); +Ext.define('PVE.qemu.Monitor', { + extend: 'Ext.panel.Panel', + + alias: 'widget.pveQemuMonitor', + + maxLines: 500, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var history = []; + var histNum = -1; + var lines = []; + + var textbox = Ext.createWidget('panel', { + region: 'center', + xtype: 'panel', + autoScroll: true, + border: true, + margins: '5 5 5 5', + bodyStyle: 'font-family: monospace;' + }); + + var scrollToEnd = function() { + var el = textbox.getTargetEl(); + var dom = Ext.getDom(el); + + var clientHeight = dom.clientHeight; + // BrowserBug: clientHeight reports 0 in IE9 StrictMode + // Instead we are using offsetHeight and hardcoding borders + if (Ext.isIE9 && Ext.isStrict) { + clientHeight = dom.offsetHeight + 2; + } + dom.scrollTop = dom.scrollHeight - clientHeight; + }; + + var refresh = function() { + textbox.update('
' + lines.join('\n') + '
'); + scrollToEnd(); + }; + + var addLine = function(line) { + lines.push(line); + if (lines.length > me.maxLines) { + lines.shift(); + } + }; + + var executeCmd = function(cmd) { + addLine("# " + Ext.htmlEncode(cmd)); + if (cmd) { + history.unshift(cmd); + if (history.length > 20) { + history.splice(20); + } + } + histNum = -1; + + refresh(); + Proxmox.Utils.API2Request({ + params: { command: cmd }, + url: '/nodes/' + nodename + '/qemu/' + vmid + "/monitor", + method: 'POST', + waitMsgTarget: me, + success: function(response, opts) { + var res = response.result.data; + Ext.Array.each(res.split('\n'), function(line) { + addLine(Ext.htmlEncode(line)); + }); + refresh(); + }, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + Ext.apply(me, { + layout: { type: 'border' }, + border: false, + items: [ + textbox, + { + region: 'south', + margins:'0 5 5 5', + border: false, + xtype: 'textfield', + name: 'cmd', + value: '', + fieldStyle: 'font-family: monospace;', + allowBlank: true, + listeners: { + afterrender: function(f) { + f.focus(false); + addLine("Type 'help' for help."); + refresh(); + }, + specialkey: function(f, e) { + var key = e.getKey(); + switch (key) { + case e.ENTER: + var cmd = f.getValue(); + f.setValue(''); + executeCmd(cmd); + break; + case e.PAGE_UP: + textbox.scrollBy(0, -0.9*textbox.getHeight(), false); + break; + case e.PAGE_DOWN: + textbox.scrollBy(0, 0.9*textbox.getHeight(), false); + break; + case e.UP: + if (histNum + 1 < history.length) { + f.setValue(history[++histNum]); + } + e.preventDefault(); + break; + case e.DOWN: + if (histNum > 0) { + f.setValue(history[--histNum]); + } + e.preventDefault(); + break; + default: + break; + } + } + } + } + ], + listeners: { + show: function() { + var field = me.query('textfield[name="cmd"]')[0]; + field.focus(false, true); + } + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.qemu.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveQemuSummary', + + scrollable: true, + bodyPadding: 5, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + if (!me.workspace) { + throw "no workspace specified"; + } + + if (!me.statusStore) { + throw "no status storage specified"; + } + + var template = !!me.pveSelNode.data.template; + var rstore = me.statusStore; + + var width = template ? 1 : 0.5; + var items = [ + { + xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView', + responsiveConfig: { + 'width < 1900': { + columnWidth: width + }, + 'width >= 1900': { + columnWidth: width / 2 + } + }, + itemId: 'gueststatus', + pveSelNode: me.pveSelNode, + rstore: rstore + }, + { + xtype: 'pveNotesView', + maxHeight: 330, + itemId: 'notesview', + pveSelNode: me.pveSelNode, + responsiveConfig: { + 'width < 1900': { + columnWidth: width + }, + 'width >= 1900': { + columnWidth: width / 2 + } + } + } + ]; + + var rrdstore; + if (!template) { + + rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/rrddata", + model: 'pve-rrd-guest' + }); + + items.push( + { + xtype: 'proxmoxRRDChart', + title: gettext('CPU usage'), + pveSelNode: me.pveSelNode, + fields: ['cpu'], + fieldTitles: [gettext('CPU usage')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Memory usage'), + pveSelNode: me.pveSelNode, + fields: ['maxmem', 'mem'], + fieldTitles: [gettext('Total'), gettext('RAM usage')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Network traffic'), + pveSelNode: me.pveSelNode, + fields: ['netin','netout'], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Disk IO'), + pveSelNode: me.pveSelNode, + fields: ['diskread','diskwrite'], + store: rrdstore + } + ); + + } + + Ext.apply(me, { + tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ], + items: [ + { + xtype: 'container', + layout: { + type: 'column' + }, + defaults: { + minHeight: 330, + padding: 5, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1 + }, + 'width >= 1900': { + columnWidth: 0.5 + } + } + }, + items: items + } + ] + }); + + me.callParent(); + if (!template) { + rrdstore.startUpdate(); + me.on('destroy', rrdstore.stopUpdate); + } + } +}); +Ext.define('PVE.qemu.OSTypeInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuOSTypePanel', + onlineHelp: 'qm_os_settings', + insideWizard: false, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'combobox[name=osbase]': { + change: 'onOSBaseChange' + }, + 'combobox[name=ostype]': { + afterrender: 'onOSTypeChange', + change: 'onOSTypeChange' + } + }, + onOSBaseChange: function(field, value) { + this.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]); + }, + onOSTypeChange: function(field) { + var me = this, ostype = field.getValue(); + if (!me.getView().insideWizard) { + return; + } + var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype); + + me.setWidget('pveBusSelector', targetValues.busType); + me.setWidget('pveNetworkCardSelector', targetValues.networkCard); + var scsihw = targetValues.scsihw || '__default__'; + this.getViewModel().set('current.scsihw', scsihw); + }, + setWidget: function(widget, newValue) { + // changing a widget is safe only if ComponentQuery.query returns us + // a single value array + var widgets = Ext.ComponentQuery.query('pveQemuCreateWizard ' + widget); + if (widgets.length === 1) { + widgets[0].setValue(newValue); + } else { + throw 'non unique widget :' + widget + ' in Wizard'; + } + } + }, + + initComponent : function() { + var me = this; + + /*jslint confusion: true */ + me.items = [ + { + xtype: 'displayfield', + value: gettext('Guest OS') + ':', + hidden: !me.insideWizard + }, + { + xtype: 'combobox', + submitValue: false, + name: 'osbase', + fieldLabel: gettext('Type'), + editable: false, + queryMode: 'local', + value: 'Linux', + store: Object.keys(PVE.Utils.kvm_ostypes) + }, + { + xtype: 'combobox', + name: 'ostype', + reference: 'ostype', + fieldLabel: gettext('Version'), + value: 'l26', + allowBlank : false, + editable: false, + queryMode: 'local', + valueField: 'val', + displayField: 'desc', + store: { + fields: ['desc', 'val'], + data: PVE.Utils.kvm_ostypes.Linux, + listeners: { + datachanged: function (store) { + var ostype = me.lookup('ostype'); + var old_val = ostype.getValue(); + if (!me.insideWizard && old_val && store.find('val', old_val) != -1) { + ostype.setValue(old_val); + } else { + ostype.setValue(store.getAt(0)); + } + } + } + } + } + ]; + /*jslint confusion: false */ + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.OSTypeEdit', { + extend: 'Proxmox.window.Edit', + + subject: 'OS Type', + + items: [{ xtype: 'pveQemuOSTypePanel' }], + + initComponent : function() { + var me = this; + + me.callParent(); + + me.load({ + success: function(response, options) { + var value = response.result.data.ostype || 'other'; + var osinfo = PVE.Utils.get_kvm_osinfo(value); + me.setValues({ ostype: value, osbase: osinfo.base }); + } + }); + } +}); +/* + * This class holds performance *recommended* settings for the PVE Qemu wizards + * the *mandatory* settings are set in the PVE::QemuServer + * config_to_command sub + * We store this here until we get the data from the API server +*/ + +// this is how you would add an hypothetic FreeBSD > 10 entry +// +//virtio-blk is stable but virtIO net still +// problematic as of 10.3 +// see https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=165059 +// addOS({ +// parent: 'generic', // inherits defaults +// pveOS: 'freebsd10', // must match a radiofield in OSTypeEdit.js +// busType: 'virtio' // must match a pveBusController value +// // networkCard muss match a pveNetworkCardSelector + + +Ext.define('PVE.qemu.OSDefaults', { + singleton: true, // will also force creation when loaded + + constructor: function() { + var me = this; + + var addOS = function(settings) { + if (me.hasOwnProperty(settings.parent)) { + var child = Ext.clone(me[settings.parent]); + me[settings.pveOS] = Ext.apply(child, settings); + + } else { + throw("Could not find your genitor"); + } + }; + + // default values + me.generic = { + busType: 'ide', + networkCard: 'e1000', + busPriority: { + ide: 4, + sata: 3, + scsi: 2, + virtio: 1 + }, + scsihw: 'virtio-scsi-pci' + }; + + // virtio-net is in kernel since 2.6.25 + // virtio-scsi since 3.2 but backported in RHEL with 2.6 kernel + addOS({ + pveOS: 'l26', + parent : 'generic', + busType: 'scsi', + busPriority: { + scsi: 4, + virtio: 3, + sata: 2, + ide: 1 + }, + networkCard: 'virtio' + }); + + // recommandation from http://wiki.qemu.org/Windows2000 + addOS({ + pveOS: 'w2k', + parent : 'generic', + networkCard: 'rtl8139', + scsihw: '' + }); + // https://pve.proxmox.com/wiki/Windows_XP_Guest_Notes + addOS({ + pveOS: 'wxp', + parent : 'w2k' + }); + + me.getDefaults = function(ostype) { + if (PVE.qemu.OSDefaults[ostype]) { + return PVE.qemu.OSDefaults[ostype]; + } else { + return PVE.qemu.OSDefaults.generic; + } + }; + } +}); +Ext.define('PVE.qemu.ProcessorInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuProcessorPanel', + onlineHelp: 'qm_cpu', + + insideWizard: false, + + controller: { + xclass: 'Ext.app.ViewController', + + updateCores: function() { + var me = this.getView(); + var sockets = me.down('field[name=sockets]').getValue(); + var cores = me.down('field[name=cores]').getValue(); + me.down('field[name=totalcores]').setValue(sockets*cores); + var vcpus = me.down('field[name=vcpus]'); + vcpus.setMaxValue(sockets*cores); + vcpus.setEmptyText(sockets*cores); + vcpus.validate(); + }, + + control: { + 'field[name=sockets]': { + change: 'updateCores' + }, + 'field[name=cores]': { + change: 'updateCores' + } + } + }, + + onGetValues: function(values) { + var me = this; + + if (Array.isArray(values['delete'])) { + values['delete'] = values['delete'].join(','); + } + + PVE.Utils.delete_if_default(values, 'cpulimit', '0', 0); + PVE.Utils.delete_if_default(values, 'cpuunits', '1024', 0); + + // build the cpu options: + me.cpu.cputype = values.cputype; + + if (values.flags) { + me.cpu.flags = values.flags; + } else { + delete me.cpu.flags; + } + + delete values.cputype; + delete values.flags; + var cpustring = PVE.Parser.printQemuCpu(me.cpu); + + // remove cputype delete request: + var del = values['delete']; + delete values['delete']; + if (del) { + del = del.split(','); + Ext.Array.remove(del, 'cputype'); + } else { + del = []; + } + + if (cpustring) { + values.cpu = cpustring; + } else { + del.push('cpu'); + } + + var delarr = del.join(','); + if (delarr) { + values['delete'] = delarr; + } + + return values; + }, + + cpu: {}, + + column1: [ + { + xtype: 'proxmoxintegerfield', + name: 'sockets', + minValue: 1, + maxValue: 4, + value: '1', + fieldLabel: gettext('Sockets'), + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + name: 'cores', + minValue: 1, + maxValue: 128, + value: '1', + fieldLabel: gettext('Cores'), + allowBlank: false + } + ], + + column2: [ + { + xtype: 'CPUModelSelector', + name: 'cputype', + value: '__default__', + fieldLabel: gettext('Type') + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Total cores'), + name: 'totalcores', + value: '1' + } + ], + + advancedColumn1: [ + { + xtype: 'proxmoxintegerfield', + name: 'vcpus', + minValue: 1, + maxValue: 1, + value: '', + fieldLabel: gettext('VCPUs'), + deleteEmpty: true, + allowBlank: true, + emptyText: '1' + }, + { + xtype: 'numberfield', + name: 'cpulimit', + minValue: 0, + maxValue: 128, // api maximum + value: '', + step: 1, + fieldLabel: gettext('CPU limit'), + allowBlank: true, + emptyText: gettext('unlimited') + } + ], + + advancedColumn2: [ + { + xtype: 'proxmoxintegerfield', + name: 'cpuunits', + fieldLabel: gettext('CPU units'), + minValue: 8, + maxValue: 500000, + value: '1024', + deleteEmpty: true, + allowBlank: true + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enable NUMA'), + name: 'numa', + uncheckedValue: 0 + } + ], + advancedColumnB: [ + { + xtype: 'label', + text: 'Extra CPU Flags:' + }, + { + xtype: 'vmcpuflagselector', + name: 'flags' + } + ] +}); + +Ext.define('PVE.qemu.ProcessorEdit', { + extend: 'Proxmox.window.Edit', + + width: 700, + + initComponent : function() { + var me = this; + + var ipanel = Ext.create('PVE.qemu.ProcessorInputPanel'); + + Ext.apply(me, { + subject: gettext('Processors'), + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var data = response.result.data; + var value = data.cpu; + if (value) { + var cpu = PVE.Parser.parseQemuCpu(value); + ipanel.cpu = cpu; + data.cputype = cpu.cputype; + if (cpu.flags) { + data.flags = cpu.flags; + } + } + me.setValues(data); + } + }); + } +}); +Ext.define('PVE.qemu.BootOrderPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuBootOrderPanel', + vmconfig: {}, // store loaded vm config + + bootdisk: undefined, + selection: [], + list: [], + comboboxes: [], + + isBootDisk: function(value) { + return PVE.Utils.bus_match.test(value); + }, + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + var order = me.vmconfig.boot || 'cdn'; + me.bootdisk = me.vmconfig.bootdisk || undefined; + + // get the first 3 characters + // ignore the rest (there should never be more than 3) + me.selection = order.split('').slice(0,3); + + // build bootdev list + me.list = []; + Ext.Object.each(me.vmconfig, function(key, value) { + if (me.isBootDisk(key) && + !(/media=cdrom/).test(value)) { + me.list.push([key, "Disk '" + key + "'"]); + } + }); + + me.list.push(['d', 'CD-ROM']); + me.list.push(['n', gettext('Network')]); + me.list.push(['__none__', Proxmox.Utils.noneText]); + + me.recomputeList(); + + me.comboboxes.forEach(function(box) { + box.resetOriginalValue(); + }); + }, + + onGetValues: function(values) { + var me = this; + var order = me.selection.join(''); + var res = { boot: order }; + + if (me.bootdisk && order.indexOf('c') !== -1) { + res.bootdisk = me.bootdisk; + } else { + res['delete'] = 'bootdisk'; + } + + return res; + }, + + recomputeSelection: function(combobox, newVal, oldVal) { + var me = this.up('#inputpanel'); + me.selection = []; + me.comboboxes.forEach(function(item) { + var val = item.getValue(); + + // when selecting an already selected item, + // switch it around + if ((val === newVal || (me.isBootDisk(val) && me.isBootDisk(newVal))) && + item.name !== combobox.name && + newVal !== '__none__') { + // swap items + val = oldVal; + } + + // push 'c','d' or 'n' in the array + if (me.isBootDisk(val)) { + me.selection.push('c'); + me.bootdisk = val; + } else if (val === 'd' || + val === 'n') { + me.selection.push(val); + } + }); + + me.recomputeList(); + }, + + recomputeList: function(){ + var me = this; + // set the correct values in the kvcomboboxes + var cnt = 0; + me.comboboxes.forEach(function(item) { + if (cnt === 0) { + // never show 'none' on first combobox + item.store.loadData(me.list.slice(0, me.list.length-1)); + } else { + item.store.loadData(me.list); + } + item.suspendEvent('change'); + if (cnt < me.selection.length) { + item.setValue((me.selection[cnt] !== 'c')?me.selection[cnt]:me.bootdisk); + } else if (cnt === 0){ + item.setValue(''); + } else { + item.setValue('__none__'); + } + cnt++; + item.resumeEvent('change'); + item.validate(); + }); + }, + + initComponent : function() { + var me = this; + + // this has to be done here, because of + // the way our inputPanel class handles items + me.comboboxes = [ + Ext.createWidget('proxmoxKVComboBox', { + fieldLabel: gettext('Boot device') + " 1", + labelWidth: 120, + name: 'bd1', + allowBlank: false, + listeners: { + change: me.recomputeSelection + } + }), + Ext.createWidget('proxmoxKVComboBox', { + fieldLabel: gettext('Boot device') + " 2", + labelWidth: 120, + name: 'bd2', + allowBlank: false, + listeners: { + change: me.recomputeSelection + } + }), + Ext.createWidget('proxmoxKVComboBox', { + fieldLabel: gettext('Boot device') + " 3", + labelWidth: 120, + name: 'bd3', + allowBlank: false, + listeners: { + change: me.recomputeSelection + } + }) + ]; + Ext.apply(me, { items: me.comboboxes }); + me.callParent(); + } +}); + +Ext.define('PVE.qemu.BootOrderEdit', { + extend: 'Proxmox.window.Edit', + + items: [{ + xtype: 'pveQemuBootOrderPanel', + itemId: 'inputpanel' + }], + + subject: gettext('Boot Order'), + + initComponent : function() { + var me = this; + me.callParent(); + me.load({ + success: function(response, options) { + me.down('#inputpanel').setVMConfig(response.result.data); + } + }); + } +}); +Ext.define('PVE.qemu.MemoryInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuMemoryPanel', + onlineHelp: 'qm_memory', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + var res = {}; + + res.memory = values.memory; + res.balloon = values.balloon; + + if (!values.ballooning) { + res.balloon = 0; + res['delete'] = 'shares'; + } else if (values.memory === values.balloon) { + delete res.balloon; + res['delete'] = 'balloon,shares'; + } else if (Ext.isDefined(values.shares) && (values.shares !== "")) { + res.shares = values.shares; + } else { + res['delete'] = "shares"; + } + + return res; + }, + + initComponent: function() { + var me = this; + var labelWidth = 160; + + me.items= [ + { + xtype: 'pveMemoryField', + labelWidth: labelWidth, + fieldLabel: gettext('Memory') + ' (MiB)', + name: 'memory', + minValue: 1, + step: 32, + hotplug: me.hotplug, + listeners: { + change: function(f, value, old) { + var bf = me.down('field[name=balloon]'); + var balloon = bf.getValue(); + bf.setMaxValue(value); + if (balloon === old) { + bf.setValue(value); + } + bf.validate(); + } + } + } + ]; + + me.advancedItems= [ + { + xtype: 'pveMemoryField', + name: 'balloon', + minValue: 1, + step: 32, + fieldLabel: gettext('Minimum memory') + ' (MiB)', + hotplug: me.hotplug, + labelWidth: labelWidth, + allowBlank: false, + listeners: { + change: function(f, value) { + var memory = me.down('field[name=memory]').getValue(); + var shares = me.down('field[name=shares]'); + shares.setDisabled(value === memory); + } + } + }, + { + xtype: 'proxmoxintegerfield', + name: 'shares', + disabled: true, + minValue: 0, + maxValue: 50000, + value: '', + step: 10, + fieldLabel: gettext('Shares'), + labelWidth: labelWidth, + allowBlank: true, + emptyText: Proxmox.Utils.defaultText + ' (1000)', + submitEmptyText: false + }, + { + xtype: 'proxmoxcheckbox', + labelWidth: labelWidth, + value: '1', + name: 'ballooning', + fieldLabel: gettext('Ballooning Device'), + listeners: { + change: function(f, value) { + var bf = me.down('field[name=balloon]'); + var shares = me.down('field[name=shares]'); + var memory = me.down('field[name=memory]'); + bf.setDisabled(!value); + shares.setDisabled(!value || (bf.getValue() === memory.getValue())); + } + } + } + ]; + + if (me.insideWizard) { + me.column1 = me.items; + me.items = undefined; + me.advancedColumn1 = me.advancedItems; + me.advancedItems = undefined; + } + me.callParent(); + } + +}); + +Ext.define('PVE.qemu.MemoryEdit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + var memoryhotplug; + if(me.hotplug) { + Ext.each(me.hotplug.split(','), function(el) { + if (el === 'memory') { + memoryhotplug = 1; + } + }); + } + + var ipanel = Ext.create('PVE.qemu.MemoryInputPanel', { + hotplug: memoryhotplug + }); + + Ext.apply(me, { + subject: gettext('Memory'), + items: [ ipanel ], + // uncomment the following to use the async configiguration API + // backgroundDelay: 5, + width: 400 + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var data = response.result.data; + + var values = { + ballooning: data.balloon === 0 ? '0' : '1', + shares: data.shares, + memory: data.memory || '512', + balloon: data.balloon > 0 ? data.balloon : (data.memory || '512') + }; + + ipanel.setValues(values); + } + }); + } +}); +Ext.define('PVE.qemu.NetworkInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuNetworkInputPanel', + onlineHelp: 'qm_network_device', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + me.network.model = values.model; + if (values.nonetwork) { + return {}; + } else { + me.network.bridge = values.bridge; + me.network.tag = values.tag; + me.network.firewall = values.firewall; + } + me.network.macaddr = values.macaddr; + me.network.disconnect = values.disconnect; + me.network.queues = values.queues; + + if (values.rate) { + me.network.rate = values.rate; + } else { + delete me.network.rate; + } + + var params = {}; + + params[me.confid] = PVE.Parser.printQemuNetwork(me.network); + + return params; + }, + + setNetwork: function(confid, data) { + var me = this; + + me.confid = confid; + + if (data) { + data.networkmode = data.bridge ? 'bridge' : 'nat'; + } else { + data = {}; + data.networkmode = 'bridge'; + } + me.network = data; + + me.setValues(me.network); + }, + + setNodename: function(nodename) { + var me = this; + + me.bridgesel.setNodename(nodename); + }, + + initComponent : function() { + var me = this; + + me.network = {}; + me.confid = 'net0'; + + me.column1 = []; + me.column2 = []; + + me.bridgesel = Ext.create('PVE.form.BridgeSelector', { + name: 'bridge', + fieldLabel: gettext('Bridge'), + nodename: me.nodename, + autoSelect: true, + allowBlank: false + }); + + me.column1 = [ + me.bridgesel, + { + xtype: 'pveVlanField', + name: 'tag', + value: '' + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Firewall'), + name: 'firewall', + checked: (me.insideWizard || me.isCreate) + } + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Disconnect'), + name: 'disconnect' + } + ]; + + if (me.insideWizard) { + me.column1.unshift({ + xtype: 'checkbox', + name: 'nonetwork', + inputValue: 'none', + boxLabel: gettext('No network device'), + listeners: { + change: function(cb, value) { + var fields = [ + 'disconnect', + 'bridge', + 'tag', + 'firewall', + 'model', + 'macaddr', + 'rate', + 'queues' + ]; + fields.forEach(function(fieldname) { + me.down('field[name='+fieldname+']').setDisabled(value); + }); + me.down('field[name=bridge]').validate(); + } + } + }); + me.column2.unshift({ + xtype: 'displayfield' + }); + } + + me.column2.push( + { + xtype: 'pveNetworkCardSelector', + name: 'model', + fieldLabel: gettext('Model'), + value: PVE.qemu.OSDefaults.generic.networkCard, + allowBlank: false + }, + { + xtype: 'textfield', + name: 'macaddr', + fieldLabel: gettext('MAC address'), + vtype: 'MacAddress', + allowBlank: true, + emptyText: 'auto' + }); + me.advancedColumn2 = [ + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Rate limit') + ' (MB/s)', + minValue: 0, + maxValue: 10*1024, + value: '', + emptyText: 'unlimited', + allowBlank: true + }, + { + xtype: 'proxmoxintegerfield', + name: 'queues', + fieldLabel: 'Multiqueue', + minValue: 1, + maxValue: 8, + value: '', + allowBlank: true + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.NetworkEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.isCreate = me.confid ? false : true; + + var ipanel = Ext.create('PVE.qemu.NetworkInputPanel', { + confid: me.confid, + nodename: nodename, + isCreate: me.isCreate + }); + + Ext.applyIf(me, { + subject: gettext('Network Device'), + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var i, confid; + me.vmconfig = response.result.data; + if (!me.isCreate) { + var value = me.vmconfig[me.confid]; + var network = PVE.Parser.parseQemuNetwork(me.confid, value); + if (!network) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse network options'); + me.close(); + return; + } + ipanel.setNetwork(me.confid, network); + } else { + for (i = 0; i < 100; i++) { + confid = 'net' + i.toString(); + if (!Ext.isDefined(me.vmconfig[confid])) { + me.confid = confid; + break; + } + } + ipanel.setNetwork(me.confid); + } + } + }); + } +}); +Ext.define('PVE.qemu.Smbios1InputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.PVE.qemu.Smbios1InputPanel', + + insideWizard: false, + + smbios1: {}, + + onGetValues: function(values) { + var me = this; + + var params = { + smbios1: PVE.Parser.printQemuSmbios1(values) + }; + + return params; + }, + + setSmbios1: function(data) { + var me = this; + + me.smbios1 = data; + + me.setValues(me.smbios1); + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: 'UUID', + regex: /^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$/, + name: 'uuid' + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Manufacturer'), + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'manufacturer' + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Product'), + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'product' + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Version'), + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'version' + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Serial'), + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'serial' + }, + { + xtype: 'textareafield', + fieldLabel: 'SKU', + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'sku' + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Family'), + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'family' + } + ] +}); + +Ext.define('PVE.qemu.Smbios1Edit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + var ipanel = Ext.create('PVE.qemu.Smbios1InputPanel', {}); + + Ext.applyIf(me, { + subject: gettext('SMBIOS settings (type1)'), + width: 450, + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var i, confid; + me.vmconfig = response.result.data; + var value = me.vmconfig.smbios1; + if (value) { + var data = PVE.Parser.parseQemuSmbios1(value); + if (!data) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse smbios options'); + me.close(); + return; + } + ipanel.setSmbios1(data); + } + } + }); + } +}); +Ext.define('PVE.qemu.CDInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuCDInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + var confid = me.confid || (values.controller + values.deviceid); + + me.drive.media = 'cdrom'; + if (values.mediaType === 'iso') { + me.drive.file = values.cdimage; + } else if (values.mediaType === 'cdrom') { + me.drive.file = 'cdrom'; + } else { + me.drive.file = 'none'; + } + + var params = {}; + + params[confid] = PVE.Parser.printQemuDrive(me.drive); + + return params; + }, + + setVMConfig: function(vmconfig) { + var me = this; + + if (me.bussel) { + me.bussel.setVMConfig(vmconfig, 'cdrom'); + } + }, + + setDrive: function(drive) { + var me = this; + + var values = {}; + if (drive.file === 'cdrom') { + values.mediaType = 'cdrom'; + } else if (drive.file === 'none') { + values.mediaType = 'none'; + } else { + values.mediaType = 'iso'; + var match = drive.file.match(/^([^:]+):/); + if (match) { + values.cdstorage = match[1]; + values.cdimage = drive.file; + } + } + + me.drive = drive; + + me.setValues(values); + }, + + setNodename: function(nodename) { + var me = this; + + me.cdstoragesel.setNodename(nodename); + me.cdfilesel.setStorage(undefined, nodename); + }, + + initComponent : function() { + var me = this; + + me.drive = {}; + + var items = []; + + if (!me.confid) { + me.bussel = Ext.create('PVE.form.ControllerSelector', { + noVirtIO: true + }); + items.push(me.bussel); + } + + items.push({ + xtype: 'radiofield', + name: 'mediaType', + inputValue: 'iso', + boxLabel: gettext('Use CD/DVD disc image file (iso)'), + checked: true, + listeners: { + change: function(f, value) { + if (!me.rendered) { + return; + } + me.down('field[name=cdstorage]').setDisabled(!value); + me.down('field[name=cdimage]').setDisabled(!value); + me.down('field[name=cdimage]').validate(); + } + } + }); + + me.cdfilesel = Ext.create('PVE.form.FileSelector', { + name: 'cdimage', + nodename: me.nodename, + storageContent: 'iso', + fieldLabel: gettext('ISO image'), + labelAlign: 'right', + allowBlank: false + }); + + me.cdstoragesel = Ext.create('PVE.form.StorageSelector', { + name: 'cdstorage', + nodename: me.nodename, + fieldLabel: gettext('Storage'), + labelAlign: 'right', + storageContent: 'iso', + allowBlank: false, + autoSelect: me.insideWizard, + listeners: { + change: function(f, value) { + me.cdfilesel.setStorage(value); + } + } + }); + + items.push(me.cdstoragesel); + items.push(me.cdfilesel); + + items.push({ + xtype: 'radiofield', + name: 'mediaType', + inputValue: 'cdrom', + boxLabel: gettext('Use physical CD/DVD Drive') + }); + + items.push({ + xtype: 'radiofield', + name: 'mediaType', + inputValue: 'none', + boxLabel: gettext('Do not use any media') + }); + + me.items = items; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.CDEdit', { + extend: 'Proxmox.window.Edit', + + width: 400, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.isCreate = me.confid ? false : true; + + var ipanel = Ext.create('PVE.qemu.CDInputPanel', { + confid: me.confid, + nodename: nodename + }); + + Ext.applyIf(me, { + subject: 'CD/DVD Drive', + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var value = response.result.data[me.confid]; + var drive = PVE.Parser.parseQemuDrive(me.confid, value); + if (!drive) { + Ext.Msg.alert('Error', 'Unable to parse drive options'); + me.close(); + return; + } + ipanel.setDrive(drive); + } + } + }); + } +}); +/*jslint confusion: true */ +/* 'change' property is assigned a string and then a function */ +Ext.define('PVE.qemu.HDInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuHDInputPanel', + onlineHelp: 'qm_hard_disk', + + insideWizard: false, + + unused: false, // ADD usused disk imaged + + vmconfig: {}, // used to select usused disks + + controller: { + + xclass: 'Ext.app.ViewController', + + onControllerChange: function(field) { + var value = field.getValue(); + + var allowIOthread = value.match(/^(virtio|scsi)/); + this.lookup('iothread').setDisabled(!allowIOthread); + if (!allowIOthread) { + this.lookup('iothread').setValue(false); + } + + var virtio = value.match(/^virtio/); + this.lookup('discard').setDisabled(virtio); + this.lookup('ssd').setDisabled(virtio); + if (virtio) { + this.lookup('discard').setValue(false); + this.lookup('ssd').setValue(false); + } + + this.lookup('scsiController').setVisible(value.match(/^scsi/)); + }, + + control: { + 'field[name=controller]': { + change: 'onControllerChange', + afterrender: 'onControllerChange' + }, + 'field[name=iothread]' : { + change: function(f, value) { + if (!this.getView().insideWizard) { + return; + } + var vmScsiType = value ? 'virtio-scsi-single': 'virtio-scsi-pci'; + this.lookupReference('scsiController').setValue(vmScsiType); + } + } + } + }, + + onGetValues: function(values) { + var me = this; + + var params = {}; + var confid = me.confid || (values.controller + values.deviceid); + + if (me.unused) { + me.drive.file = me.vmconfig[values.unusedId]; + confid = values.controller + values.deviceid; + } else if (me.isCreate) { + if (values.hdimage) { + me.drive.file = values.hdimage; + } else { + me.drive.file = values.hdstorage + ":" + values.disksize; + } + me.drive.format = values.diskformat; + } + + if (values.nobackup) { + me.drive.backup = 'no'; + } else { + delete me.drive.backup; + } + + if (values.noreplicate) { + me.drive.replicate = 'no'; + } else { + delete me.drive.replicate; + } + + if (values.discard) { + me.drive.discard = 'on'; + } else { + delete me.drive.discard; + } + + if (values.ssd) { + me.drive.ssd = 'on'; + } else { + delete me.drive.ssd; + } + + if (values.iothread) { + me.drive.iothread = 'on'; + } else { + delete me.drive.iothread; + } + + if (values.cache) { + me.drive.cache = values.cache; + } else { + delete me.drive.cache; + } + + var names = ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr']; + Ext.Array.each(names, function(name) { + if (values[name]) { + me.drive[name] = values[name]; + } else { + delete me.drive[name]; + } + var burst_name = name + '_max'; + if (values[burst_name] && values[name]) { + me.drive[burst_name] = values[burst_name]; + } else { + delete me.drive[burst_name]; + } + }); + + + params[confid] = PVE.Parser.printQemuDrive(me.drive); + + return params; + }, + + setVMConfig: function(vmconfig) { + var me = this; + + me.vmconfig = vmconfig; + + if (me.bussel) { + me.bussel.setVMConfig(vmconfig); + me.scsiController.setValue(vmconfig.scsihw); + } + if (me.unusedDisks) { + var disklist = []; + Ext.Object.each(vmconfig, function(key, value) { + if (key.match(/^unused\d+$/)) { + disklist.push([key, value]); + } + }); + me.unusedDisks.store.loadData(disklist); + me.unusedDisks.setValue(me.confid); + } + }, + + setDrive: function(drive) { + var me = this; + + me.drive = drive; + + var values = {}; + var match = drive.file.match(/^([^:]+):/); + if (match) { + values.hdstorage = match[1]; + } + + values.hdimage = drive.file; + values.nobackup = !PVE.Parser.parseBoolean(drive.backup, 1); + values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1); + values.diskformat = drive.format || 'raw'; + values.cache = drive.cache || '__default__'; + values.discard = (drive.discard === 'on'); + values.ssd = PVE.Parser.parseBoolean(drive.ssd); + values.iothread = PVE.Parser.parseBoolean(drive.iothread); + + values.mbps_rd = drive.mbps_rd; + values.mbps_wr = drive.mbps_wr; + values.iops_rd = drive.iops_rd; + values.iops_wr = drive.iops_wr; + values.mbps_rd_max = drive.mbps_rd_max; + values.mbps_wr_max = drive.mbps_wr_max; + values.iops_rd_max = drive.iops_rd_max; + values.iops_wr_max = drive.iops_wr_max; + + me.setValues(values); + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + initComponent : function() { + var me = this; + + var labelWidth = 140; + + me.drive = {}; + + me.column1 = []; + me.column2 = []; + + me.advancedColumn1 = []; + me.advancedColumn2 = []; + + if (!me.confid || me.unused) { + me.bussel = Ext.create('PVE.form.ControllerSelector', { + vmconfig: me.insideWizard ? {ide2: 'cdrom'} : {} + }); + me.column1.push(me.bussel); + + me.scsiController = Ext.create('Ext.form.field.Display', { + fieldLabel: gettext('SCSI Controller'), + reference: 'scsiController', + bind: me.insideWizard ? { + value: '{current.scsihw}' + } : undefined, + renderer: PVE.Utils.render_scsihw, + submitValue: false, + hidden: true + }); + me.column1.push(me.scsiController); + } + + if (me.unused) { + me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', { + name: 'unusedId', + fieldLabel: gettext('Disk image'), + matchFieldWidth: false, + listConfig: { + width: 350 + }, + data: [], + allowBlank: false + }); + me.column1.push(me.unusedDisks); + } else if (me.isCreate) { + me.column1.push({ + xtype: 'pveDiskStorageSelector', + storageContent: 'images', + name: 'disk', + nodename: me.nodename, + autoSelect: me.insideWizard + }); + } else { + me.column1.push({ + xtype: 'textfield', + disabled: true, + submitValue: false, + fieldLabel: gettext('Disk image'), + name: 'hdimage' + }); + } + + me.column2.push( + { + xtype: 'CacheTypeSelector', + name: 'cache', + value: '__default__', + fieldLabel: gettext('Cache') + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Discard'), + disabled: me.confid && me.confid.match(/^virtio/), + reference: 'discard', + name: 'discard' + } + ); + + me.advancedColumn1.push( + { + xtype: 'proxmoxcheckbox', + disabled: me.confid && me.confid.match(/^virtio/), + fieldLabel: gettext('SSD emulation'), + labelWidth: labelWidth, + name: 'ssd', + reference: 'ssd' + }, + { + xtype: 'proxmoxcheckbox', + disabled: me.confid && !me.confid.match(/^(virtio|scsi)/), + fieldLabel: 'IO thread', + labelWidth: labelWidth, + reference: 'iothread', + name: 'iothread' + }, + { + xtype: 'numberfield', + name: 'mbps_rd', + minValue: 1, + step: 1, + fieldLabel: gettext('Read limit') + ' (MB/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited') + }, + { + xtype: 'numberfield', + name: 'mbps_wr', + minValue: 1, + step: 1, + fieldLabel: gettext('Write limit') + ' (MB/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited') + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_rd', + minValue: 10, + step: 10, + fieldLabel: gettext('Read limit') + ' (ops/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited') + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_wr', + minValue: 10, + step: 10, + fieldLabel: gettext('Write limit') + ' (ops/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited') + } + ); + + me.advancedColumn2.push( + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('No backup'), + labelWidth: labelWidth, + name: 'nobackup' + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Skip replication'), + labelWidth: labelWidth, + name: 'noreplicate' + }, + { + xtype: 'numberfield', + name: 'mbps_rd_max', + minValue: 1, + step: 1, + fieldLabel: gettext('Read max burst') + ' (MB)', + labelWidth: labelWidth, + emptyText: gettext('default') + }, + { + xtype: 'numberfield', + name: 'mbps_wr_max', + minValue: 1, + step: 1, + fieldLabel: gettext('Write max burst') + ' (MB)', + labelWidth: labelWidth, + emptyText: gettext('default') + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_rd_max', + minValue: 10, + step: 10, + fieldLabel: gettext('Read max burst') + ' (ops)', + labelWidth: labelWidth, + emptyText: gettext('default') + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_wr_max', + minValue: 10, + step: 10, + fieldLabel: gettext('Write max burst') + ' (ops)', + labelWidth: labelWidth, + emptyText: gettext('default') + } + ); + + me.callParent(); + } +}); +/*jslint confusion: false */ + +Ext.define('PVE.qemu.HDEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + backgroundDelay: 5, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var unused = me.confid && me.confid.match(/^unused\d+$/); + + me.isCreate = me.confid ? unused : true; + + var ipanel = Ext.create('PVE.qemu.HDInputPanel', { + confid: me.confid, + nodename: nodename, + unused: unused, + isCreate: me.isCreate + }); + + var subject; + if (unused) { + me.subject = gettext('Unused Disk'); + } else if (me.isCreate) { + me.subject = gettext('Hard Disk'); + } else { + me.subject = gettext('Hard Disk') + ' (' + me.confid + ')'; + } + + me.items = [ ipanel ]; + + me.callParent(); + /*jslint confusion: true*/ + /* 'data' is assigned an empty array in same file, and here we + * use it like an object + */ + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var value = response.result.data[me.confid]; + var drive = PVE.Parser.parseQemuDrive(me.confid, value); + if (!drive) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options'); + me.close(); + return; + } + ipanel.setDrive(drive); + me.isValid(); // trigger validation + } + } + }); + /*jslint confusion: false*/ + } +}); +Ext.define('PVE.window.HDResize', { + extend: 'Ext.window.Window', + + resizable: false, + + resize_disk: function(disk, size) { + var me = this; + var params = { disk: disk, size: '+' + size + 'G' }; + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/resize', + waitMsgTarget: me, + method: 'PUT', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + me.close(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + var items = [ + { + xtype: 'displayfield', + name: 'disk', + value: me.disk, + fieldLabel: gettext('Disk'), + vtype: 'StorageId', + allowBlank: false + } + ]; + + me.hdsizesel = Ext.createWidget('numberfield', { + name: 'size', + minValue: 0, + maxValue: 128*1024, + decimalPrecision: 3, + value: '0', + fieldLabel: gettext('Size Increment') + ' (GiB)', + allowBlank: false + }); + + items.push(me.hdsizesel); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 140, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + me.title = gettext('Resize disk'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Resize disk'), + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.resize_disk(me.disk, values.size); + } + } + }); + + Ext.apply(me, { + modal: true, + width: 250, + height: 150, + border: false, + layout: 'fit', + buttons: [ submitBtn ], + items: [ me.formPanel ] + }); + + + me.callParent(); + + if (!me.disk) { + return; + } + + } +}); +Ext.define('PVE.window.HDMove', { + extend: 'Ext.window.Window', + + resizable: false, + + + move_disk: function(disk, storage, format, delete_disk) { + var me = this; + var qemu = (me.type === 'qemu'); + var params = {}; + params.storage = storage; + params[qemu ? 'disk':'volume'] = disk; + + if (format && qemu) { + params.format = format; + } + + if (delete_disk) { + params['delete'] = 1; + } + + var url = '/nodes/' + me.nodename + '/' + me.type + '/' + me.vmid + '/'; + url += qemu ? 'move_disk' : 'move_volume'; + + Proxmox.Utils.API2Request({ + params: params, + url: url, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + win.on('destroy', function() { me.close(); }); + } + }); + + }, + + initComponent : function() { + var me = this; + + var diskarray = []; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.type) { + me.type = 'qemu'; + } + + var qemu = (me.type === 'qemu'); + + var items = [ + { + xtype: 'displayfield', + name: qemu ? 'disk' : 'volume', + value: me.disk, + fieldLabel: qemu ? gettext('Disk') : gettext('Mount Point'), + vtype: 'StorageId', + allowBlank: false + } + ]; + + items.push({ + xtype: 'pveDiskStorageSelector', + storageLabel: gettext('Target Storage'), + nodename: me.nodename, + storageContent: qemu ? 'images' : 'rootdir', + hideSize: true + }); + + items.push({ + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Delete source'), + name: 'deleteDisk', + uncheckedValue: 0, + checked: false + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + me.title = qemu ? gettext("Move disk") : gettext('Move Volume'); + submitBtn = Ext.create('Ext.Button', { + text: me.title, + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.move_disk(me.disk, values.hdstorage, values.diskformat, + values.deleteDisk); + } + } + }); + + Ext.apply(me, { + modal: true, + width: 350, + border: false, + layout: 'fit', + buttons: [ submitBtn ], + items: [ me.formPanel ] + }); + + + me.callParent(); + + me.mon(me.formPanel, 'validitychange', function(fp, isValid) { + submitBtn.setDisabled(!isValid); + }); + + me.formPanel.isValid(); + } +}); +Ext.define('PVE.qemu.EFIDiskInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveEFIDiskInputPanel', + + insideWizard: false, + + unused: false, // ADD usused disk imaged + + vmconfig: {}, // used to select usused disks + + onGetValues: function(values) { + var me = this; + + var confid = 'efidisk0'; + + if (values.hdimage) { + me.drive.file = values.hdimage; + } else { + // we use 1 here, because for efi the size gets overridden from the backend + me.drive.file = values.hdstorage + ":1"; + } + + me.drive.format = values.diskformat; + var params = {}; + params[confid] = PVE.Parser.printQemuDrive(me.drive); + return params; + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + initComponent : function() { + var me = this; + + me.drive = {}; + + me.items= []; + + me.items.push({ + xtype: 'pveDiskStorageSelector', + name: 'efidisk0', + storageContent: 'images', + nodename: me.nodename, + hideSize: true + }); + me.callParent(); + } +}); + +Ext.define('PVE.qemu.EFIDiskEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + subject: gettext('EFI Disk'), + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.items = [{ + xtype: 'pveEFIDiskInputPanel', + onlineHelp: 'qm_bios_and_uefi', + confid: me.confid, + nodename: nodename, + isCreate: true + }]; + + me.callParent(); + } +}); +Ext.define('PVE.qemu.DisplayInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveDisplayInputPanel', + + onGetValues: function(values) { + var ret = PVE.Parser.printPropertyString(values, 'type'); + if (ret === '') { + return { + 'delete': 'vga' + }; + } + return { + vga: ret + }; + }, + + items: [{ + name: 'type', + xtype: 'proxmoxKVComboBox', + value: '__default__', + deleteEmpty: false, + fieldLabel: gettext('Graphic card'), + comboItems: PVE.Utils.kvm_vga_driver_array(), + validator: function() { + var v = this.getValue(); + var cfg = this.up('proxmoxWindowEdit').vmconfig || {}; + + if (v.match(/^serial\d+$/) && (!cfg[v] || cfg[v] !== 'socket')) { + var fmt = gettext("Serial interface '{0}' is not correctly configured."); + return Ext.String.format(fmt, v); + } + return true; + }, + listeners: { + change: function(cb, val) { + var me = this.up('panel'); + if (!val) { + return; + } + var disable = false; + var emptyText = Proxmox.Utils.defaultText; + switch (val) { + case "cirrus": + emptyText = "4"; + break; + case "std": + emptyText = "16"; + break; + case "qxl": + case "qxl2": + case "qxl3": + case "qxl4": + emptyText = "16"; + break; + case "vmware": + emptyText = "16"; + break; + case "none": + case "serial0": + case "serial1": + case "serial2": + case "serial3": + emptyText = 'N/A'; + disable = true; + break; + case "virtio": + emptyText = "256"; + break; + default: + break; + } + var memoryfield = me.down('field[name=memory]'); + memoryfield.setEmptyText(emptyText); + memoryfield.setDisabled(disable); + } + } + },{ + xtype: 'proxmoxintegerfield', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Memory') + ' (MiB)', + minValue: 4, + maxValue: 512, + step: 4, + name: 'memory' + }] +}); + +Ext.define('PVE.qemu.DisplayEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + subject: gettext('Display'), + width: 350, + + items: [{ + xtype: 'pveDisplayInputPanel' + }], + + initComponent : function() { + var me = this; + + me.callParent(); + + me.load({ + success: function(response) { + me.vmconfig = response.result.data; + var vga = me.vmconfig.vga || '__default__'; + me.setValues(PVE.Parser.parsePropertyString(vga, 'type')); + } + }); + } +}); +Ext.define('PVE.qemu.KeyboardEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.applyIf(me, { + subject: gettext('Keyboard Layout'), + items: { + xtype: 'VNCKeyboardSelector', + name: 'keyboard', + value: '__default__', + fieldLabel: gettext('Keyboard Layout') + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('PVE.qemu.HardwareView', { + extend: 'Proxmox.grid.PendingObjectGrid', + alias: ['widget.PVE.qemu.HardwareView'], + + onlineHelp: 'qm_virtual_machines_settings', + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = rows[key] || {}; + var iconCls = rowdef.iconCls; + var icon = ''; + var txt = (rowdef.header || key); + + metaData.tdAttr = "valign=middle"; + + if (rowdef.tdCls) { + metaData.tdCls = rowdef.tdCls; + if (rowdef.tdCls == 'pve-itype-icon-storage') { + var value = me.getObjectValue(key, '', false); + if (value === '') { + value = me.getObjectValue(key, '', true); + } + if (value.match(/vm-.*-cloudinit/)) { + metaData.tdCls = 'pve-itype-icon-cloud'; + return rowdef.cloudheader; + } else if (value.match(/media=cdrom/)) { + metaData.tdCls = 'pve-itype-icon-cdrom'; + return rowdef.cdheader; + } + } + } else if (iconCls) { + icon = ""; + metaData.tdCls += " pve-itype-fa"; + } + return icon + txt; + }, + + initComponent : function() { + var me = this; + var i, confid; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + var diskCap = caps.vms['VM.Config.Disk']; + + /*jslint confusion: true */ + var rows = { + memory: { + header: gettext('Memory'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined, + never_delete: true, + defaultValue: '512', + tdCls: 'pve-itype-icon-memory', + group: 2, + multiKey: ['memory', 'balloon', 'shares'], + renderer: function(value, metaData, record, ri, ci, store, pending) { + var res = ''; + + var max = me.getObjectValue('memory', 512, pending); + var balloon = me.getObjectValue('balloon', undefined, pending); + var shares = me.getObjectValue('shares', undefined, pending); + + res = Proxmox.Utils.format_size(max*1024*1024); + + if (balloon !== undefined && balloon > 0) { + res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res; + + if (shares) { + res += ' [shares=' + shares +']'; + } + } else if (balloon === 0) { + res += ' [balloon=0]'; + } + return res; + } + }, + sockets: { + header: gettext('Processors'), + never_delete: true, + editor: (caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']) ? + 'PVE.qemu.ProcessorEdit' : undefined, + tdCls: 'pve-itype-icon-processor', + group: 3, + defaultValue: '1', + multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'], + renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) { + + var sockets = me.getObjectValue('sockets', 1, pending); + var model = me.getObjectValue('cpu', undefined, pending); + var cores = me.getObjectValue('cores', 1, pending); + var numa = me.getObjectValue('numa', undefined, pending); + var vcpus = me.getObjectValue('vcpus', undefined, pending); + var cpulimit = me.getObjectValue('cpulimit', undefined, pending); + var cpuunits = me.getObjectValue('cpuunits', undefined, pending); + + var res = Ext.String.format('{0} ({1} sockets, {2} cores)', + sockets*cores, sockets, cores); + + if (model) { + res += ' [' + model + ']'; + } + + if (numa) { + res += ' [numa=' + numa +']'; + } + + if (vcpus) { + res += ' [vcpus=' + vcpus +']'; + } + + if (cpulimit) { + res += ' [cpulimit=' + cpulimit +']'; + } + + if (cpuunits) { + res += ' [cpuunits=' + cpuunits +']'; + } + + return res; + } + }, + bios: { + header: 'BIOS', + group: 4, + never_delete: true, + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined, + defaultValue: '', + iconCls: 'microchip', + renderer: PVE.Utils.render_qemu_bios + }, + vga: { + header: gettext('Display'), + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined, + never_delete: true, + tdCls: 'pve-itype-icon-display', + group:5, + defaultValue: '', + renderer: PVE.Utils.render_kvm_vga_driver + }, + machine: { + header: gettext('Machine'), + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Machine'), + width: 350, + items: [{ + xtype: 'proxmoxKVComboBox', + name: 'machine', + value: '__default__', + fieldLabel: gettext('Machine'), + comboItems: [ + ['__default__', PVE.Utils.render_qemu_machine('')], + ['q35', 'q35'] + ] + }]} : undefined, + iconCls: 'cogs', + never_delete: true, + group: 6, + defaultValue: '', + renderer: PVE.Utils.render_qemu_machine + }, + scsihw: { + header: gettext('SCSI Controller'), + iconCls: 'database', + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined, + renderer: PVE.Utils.render_scsihw, + group: 7, + never_delete: true, + defaultValue: '' + }, + cores: { + visible: false + }, + cpu: { + visible: false + }, + numa: { + visible: false + }, + balloon: { + visible: false + }, + hotplug: { + visible: false + }, + vcpus: { + visible: false + }, + cpuunits: { + visible: false + }, + cpulimit: { + visible: false + }, + shares: { + visible: false + } + }; + /*jslint confusion: false */ + + PVE.Utils.forEachBus(undefined, function(type, id) { + var confid = type + id; + rows[confid] = { + group: 10, + tdCls: 'pve-itype-icon-storage', + editor: 'PVE.qemu.HDEdit', + never_delete: caps.vms['VM.Config.Disk'] ? false : true, + header: gettext('Hard Disk') + ' (' + confid +')', + cdheader: gettext('CD/DVD Drive') + ' (' + confid +')', + cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')' + }; + }); + for (i = 0; i < 32; i++) { + confid = "net" + i.toString(); + rows[confid] = { + group: 15, + order: i, + tdCls: 'pve-itype-icon-network', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined, + never_delete: caps.vms['VM.Config.Network'] ? false : true, + header: gettext('Network Device') + ' (' + confid +')' + }; + } + rows.efidisk0 = { + group: 20, + tdCls: 'pve-itype-icon-storage', + editor: null, + never_delete: caps.vms['VM.Config.Disk'] ? false : true, + header: gettext('EFI Disk') + }; + for (i = 0; i < 5; i++) { + confid = "usb" + i.toString(); + rows[confid] = { + group: 25, + order: i, + tdCls: 'pve-itype-icon-usb', + editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined, + never_delete: caps.nodes['Sys.Console'] ? false : true, + header: gettext('USB Device') + ' (' + confid + ')' + }; + } + for (i = 0; i < 4; i++) { + confid = "hostpci" + i.toString(); + rows[confid] = { + group: 30, + order: i, + tdCls: 'pve-itype-icon-pci', + never_delete: caps.nodes['Sys.Console'] ? false : true, + editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined, + header: gettext('PCI Device') + ' (' + confid + ')' + }; + } + for (i = 0; i < 4; i++) { + confid = "serial" + i.toString(); + rows[confid] = { + group: 35, + order: i, + tdCls: 'pve-itype-icon-serial', + never_delete: caps.nodes['Sys.Console'] ? false : true, + header: gettext('Serial Port') + ' (' + confid + ')' + }; + } + for (i = 0; i < 256; i++) { + rows["unused" + i.toString()] = { + group: 99, + order: i, + tdCls: 'pve-itype-icon-storage', + editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined, + header: gettext('Unused Disk') + ' ' + i.toString() + }; + } + + var sorterFn = function(rec1, rec2) { + var v1 = rec1.data.key; + var v2 = rec2.data.key; + var g1 = rows[v1].group || 0; + var g2 = rows[v2].group || 0; + var order1 = rows[v1].order || 0; + var order2 = rows[v2].order || 0; + + if ((g1 - g2) !== 0) { + return g1 - g2; + } + + if ((order1 - order2) !== 0) { + return order1 - order2; + } + + if (v1 > v2) { + return 1; + } else if (v1 < v2) { + return -1; + } else { + return 0; + } + }; + + var reload = function() { + me.rstore.load(); + }; + + var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config'; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rowdef = rows[rec.data.key]; + if (!rowdef.editor) { + return; + } + + var editor = rowdef.editor; + if (rowdef.tdCls == 'pve-itype-icon-storage') { + var value = me.getObjectValue(rec.data.key, '', true); + if (value.match(/vm-.*-cloudinit/)) { + return; + } else if (value.match(/media=cdrom/)) { + editor = 'PVE.qemu.CDEdit'; + } else if (!diskCap) { + return; + } + } + + var win; + + if (Ext.isString(editor)) { + win = Ext.create(editor, { + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + baseurl + }); + } else { + var config = Ext.apply({ + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + baseurl + }, rowdef.editor); + win = Ext.createWidget(rowdef.editor.xtype, config); + win.load(); + } + + win.show(); + win.on('destroy', reload); + }; + + var run_resize = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.HDResize', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid + }); + + win.show(); + + win.on('destroy', reload); + }; + + var run_move = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.HDMove', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid + }); + + win.show(); + + win.on('destroy', reload); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + selModel: sm, + disabled: true, + handler: run_editor + }); + + var resize_btn = new Proxmox.button.Button({ + text: gettext('Resize disk'), + selModel: sm, + disabled: true, + handler: run_resize + }); + + var move_btn = new Proxmox.button.Button({ + text: gettext('Move disk'), + selModel: sm, + disabled: true, + handler: run_move + }); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + defaultText: gettext('Remove'), + altText: gettext('Detach'), + selModel: sm, + disabled: true, + dangerous: true, + RESTMethod: 'PUT', + confirmMsg: function(rec) { + var warn = gettext('Are you sure you want to remove entry {0}'); + if (this.text === this.altText) { + warn = gettext('Are you sure you want to detach entry {0}'); + } + + var entry = rec.data.key; + var msg = Ext.String.format(warn, "'" + + me.renderKey(entry, {}, rec) + "'"); + + if (entry.match(/^unused\d+$/)) { + msg += " " + gettext('This will permanently erase all data.'); + } + + return msg; + }, + handler: function(b, e, rec) { + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: b.RESTMethod, + params: { + 'delete': rec.data.key + }, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + if (b.RESTMethod === 'POST') { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + listeners: { + destroy: function () { + me.reload(); + } + } + }); + win.show(); + } + } + }); + }, + listeners: { + render: function(btn) { + // hack: calculate an optimal button width on first display + // to prevent the whole toolbar to move when we switch + // between the "Remove" and "Detach" labels + var def = btn.getSize().width; + + btn.setText(btn.altText); + var alt = btn.getSize().width; + + btn.setText(btn.defaultText); + + var optimal = alt > def ? alt : def; + btn.setSize({ width: optimal }); + } + } + }); + + var revert_btn = new Proxmox.button.Button({ + text: gettext('Revert'), + selModel: sm, + disabled: true, + handler: function(b, e, rec) { + var rowdef = me.rows[rec.data.key] || {}; + var keys = rowdef.multiKey || [ rec.data.key ]; + var revert = keys.join(','); + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'revert': revert + }, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert('Error',response.htmlStatus); + } + }); + } + }); + + var efidisk_menuitem = Ext.create('Ext.menu.Item',{ + text: gettext('EFI Disk'), + iconCls: 'pve-itype-icon-storage', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + + var rstoredata = me.rstore.getData().map; + // check if ovmf is configured + if (rstoredata.bios && rstoredata.bios.data.value === 'ovmf') { + var win = Ext.create('PVE.qemu.EFIDiskEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } else { + Ext.Msg.alert('Error',gettext('Please select OVMF(UEFI) as BIOS first.')); + } + + } + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + // disable button when we have an efidisk already + // disable is ok in this case, because you can instantly + // see that there is already one + efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined); + // en/disable usb add button + var usbcount = 0; + var pcicount = 0; + var hasCloudInit = false; + me.rstore.getData().items.forEach(function(item){ + if (/^usb\d+/.test(item.id)) { + usbcount++; + } else if (/^hostpci\d+/.test(item.id)) { + pcicount++; + } + if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) { + hasCloudInit = true; + } + }); + + // heuristic only for disabling some stuff, the backend has the final word. + var noSysConsolePerm = !caps.nodes['Sys.Console']; + + me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5)); + me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4)); + me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit); + + if (!rec) { + remove_btn.disable(); + edit_btn.disable(); + resize_btn.disable(); + move_btn.disable(); + revert_btn.disable(); + return; + } + var key = rec.data.key; + var value = rec.data.value; + var rowdef = rows[key]; + + var pending = rec.data['delete'] || me.hasPendingChanges(key); + var isCDRom = (value && !!value.toString().match(/media=cdrom/)); + var isUnusedDisk = key.match(/^unused\d+/); + var isUsedDisk = !isUnusedDisk && + rowdef.tdCls == 'pve-itype-icon-storage' && + !isCDRom; + + var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/)); + + var isEfi = (key === 'efidisk0'); + + remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true) || (isUnusedDisk && !diskCap)); + remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText); + remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT'; + + edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit || (!isCDRom && !diskCap)); + + resize_btn.setDisabled(pending || !isUsedDisk || !diskCap); + + move_btn.setDisabled(pending || !isUsedDisk || !diskCap); + + revert_btn.setDisabled(!pending); + + }; + + Ext.apply(me, { + url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending', + interval: 5000, + selModel: sm, + run_editor: run_editor, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Hard Disk'), + iconCls: 'pve-itype-icon-storage', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + var win = Ext.create('PVE.qemu.HDEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('CD/DVD Drive'), + iconCls: 'pve-itype-icon-cdrom', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + var win = Ext.create('PVE.qemu.CDEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('Network Device'), + iconCls: 'pve-itype-icon-network', + disabled: !caps.vms['VM.Config.Network'], + handler: function() { + var win = Ext.create('PVE.qemu.NetworkEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode, + isCreate: true + }); + win.on('destroy', reload); + win.show(); + } + }, + efidisk_menuitem, + { + text: gettext('USB Device'), + itemId: 'addusb', + iconCls: 'pve-itype-icon-usb', + disabled: !caps.nodes['Sys.Console'], + handler: function() { + var win = Ext.create('PVE.qemu.USBEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('PCI Device'), + itemId: 'addpci', + iconCls: 'pve-itype-icon-pci', + disabled: !caps.nodes['Sys.Console'], + handler: function() { + var win = Ext.create('PVE.qemu.PCIEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('Serial Port'), + itemId: 'addserial', + iconCls: 'pve-itype-icon-serial', + disabled: !caps.vms['VM.Config.Options'], + handler: function() { + var win = Ext.create('PVE.qemu.SerialEdit', { + url: '/api2/extjs/' + baseurl + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('CloudInit Drive'), + itemId: 'addci', + iconCls: 'pve-itype-icon-cloud', + disabled: !caps.nodes['Sys.Console'], + handler: function() { + var win = Ext.create('PVE.qemu.CIDriveEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + } + ] + }) + }, + remove_btn, + edit_btn, + resize_btn, + move_btn, + revert_btn + ], + rows: rows, + sorterFn: sorterFn, + listeners: { + itemdblclick: run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + + me.mon(me.rstore, 'refresh', function() { + set_button_status(); + }); + } +}); +Ext.define('PVE.qemu.ScsiHwEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.applyIf(me, { + subject: gettext('SCSI Controller Type'), + items: { + xtype: 'pveScsiHwSelector', + name: 'scsihw', + value: '__default__', + fieldLabel: gettext('Type') + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('PVE.qemu.BiosEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveQemuBiosEdit', + + initComponent : function() { + var me = this; + + var EFIHint = Ext.createWidget({ + xtype: 'displayfield', //submitValue is false, so we don't get submitted + userCls: 'pve-hint', + value: 'You need to add an EFI disk for storing the ' + + 'EFI settings. See the online help for details.', + hidden: true + }); + + Ext.applyIf(me, { + subject: 'BIOS', + items: [ { + xtype: 'pveQemuBiosSelector', + onlineHelp: 'qm_bios_and_uefi', + name: 'bios', + value: '__default__', + fieldLabel: 'BIOS', + listeners: { + 'change' : function(field, newValue) { + if (newValue == 'ovmf') { + Proxmox.Utils.API2Request({ + url : me.url, + method : 'GET', + failure : function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success : function(response, opts) { + var vmConfig = response.result.data; + // there can be only one + if (!vmConfig.efidisk0) { + EFIHint.setVisible(true); + } + } + }); + } else { + if (EFIHint.isVisible()) { + EFIHint.setVisible(false); + } + } + } + } + }, + EFIHint + ] }); + + me.callParent(); + + me.load(); + + } +}); +/*jslint confusion: true */ +Ext.define('PVE.qemu.Options', { + extend: 'Proxmox.grid.PendingObjectGrid', + alias: ['widget.PVE.qemu.Options'], + + onlineHelp: 'qm_options', + + initComponent : function() { + var me = this; + var i; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + var rows = { + name: { + required: true, + defaultValue: me.pveSelNode.data.name, + header: gettext('Name'), + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Name'), + items: { + xtype: 'inputpanel', + items:{ + xtype: 'textfield', + name: 'name', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Name'), + allowBlank: true + }, + onGetValues: function(values) { + var params = values; + if (values.name === undefined || + values.name === null || + values.name === '') { + params = { 'delete':'name'}; + } + return params; + } + } + } : undefined + }, + onboot: { + header: gettext('Start at boot'), + defaultValue: '', + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Start at boot'), + items: { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Start at boot') + } + } : undefined + }, + startup: { + header: gettext('Start/Shutdown order'), + defaultValue: '', + renderer: PVE.Utils.render_kvm_startup, + editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ? + { + xtype: 'pveWindowStartupEdit', + onlineHelp: 'qm_startup_and_shutdown' + } : undefined + }, + ostype: { + header: gettext('OS Type'), + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.OSTypeEdit' : undefined, + renderer: PVE.Utils.render_kvm_ostype, + defaultValue: 'other' + }, + bootdisk: { + visible: false + }, + boot: { + header: gettext('Boot Order'), + defaultValue: 'cdn', + editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.BootOrderEdit' : undefined, + multiKey: ['boot', 'bootdisk'], + renderer: function(order, metaData, record, rowIndex, colIndex, store, pending) { + var i; + var text = ''; + var bootdisk = me.getObjectValue('bootdisk', undefined, pending); + order = order || 'cdn'; + for (i = 0; i < order.length; i++) { + var sel = order.substring(i, i + 1); + if (text) { + text += ', '; + } + if (sel === 'c') { + if (bootdisk) { + text += "Disk '" + bootdisk + "'"; + } else { + text += "Disk"; + } + } else if (sel === 'n') { + text += 'Network'; + } else if (sel === 'a') { + text += 'Floppy'; + } else if (sel === 'd') { + text += 'CD-ROM'; + } else { + text += sel; + } + } + return text; + } + }, + tablet: { + header: gettext('Use tablet for pointer'), + defaultValue: true, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Use tablet for pointer'), + items: { + xtype: 'proxmoxcheckbox', + name: 'tablet', + checked: true, + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + hotplug: { + header: gettext('Hotplug'), + defaultValue: 'disk,network,usb', + renderer: PVE.Utils.render_hotplug_features, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Hotplug'), + items: { + xtype: 'pveHotplugFeatureSelector', + name: 'hotplug', + value: '', + multiSelect: true, + fieldLabel: gettext('Hotplug'), + allowBlank: true + } + } : undefined + }, + acpi: { + header: gettext('ACPI support'), + defaultValue: true, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('ACPI support'), + items: { + xtype: 'proxmoxcheckbox', + name: 'acpi', + checked: true, + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + kvm: { + header: gettext('KVM hardware virtualization'), + defaultValue: true, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('KVM hardware virtualization'), + items: { + xtype: 'proxmoxcheckbox', + name: 'kvm', + checked: true, + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + freeze: { + header: gettext('Freeze CPU at startup'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.PowerMgmt'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Freeze CPU at startup'), + items: { + xtype: 'proxmoxcheckbox', + name: 'freeze', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + labelWidth: 140, + fieldLabel: gettext('Freeze CPU at startup') + } + } : undefined + }, + localtime: { + header: gettext('Use local time for RTC'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Use local time for RTC'), + items: { + xtype: 'proxmoxcheckbox', + name: 'localtime', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + labelWidth: 140, + fieldLabel: gettext('Use local time for RTC') + } + } : undefined + }, + startdate: { + header: gettext('RTC start date'), + defaultValue: 'now', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('RTC start date'), + items: { + xtype: 'proxmoxtextfield', + name: 'startdate', + deleteEmpty: true, + value: 'now', + fieldLabel: gettext('RTC start date'), + vtype: 'QemuStartDate', + allowBlank: true + } + } : undefined + }, + smbios1: { + header: gettext('SMBIOS settings (type1)'), + defaultValue: '', + renderer: Ext.String.htmlEncode, + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.Smbios1Edit' : undefined + }, + agent: { + header: gettext('Qemu Agent'), + defaultValue: false, + renderer: PVE.Utils.render_qga_features, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Qemu Agent'), + items: { + xtype: 'pveAgentFeatureSelector', + name: 'agent' + } + } : undefined + }, + protection: { + header: gettext('Protection'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Protection'), + items: { + xtype: 'proxmoxcheckbox', + name: 'protection', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + hookscript: { + header: gettext('Hookscript') + } + }; + + var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config'; + + var edit_btn = new Ext.Button({ + text: gettext('Edit'), + disabled: true, + handler: function() { me.run_editor(); } + }); + + var revert_btn = new Proxmox.button.Button({ + text: gettext('Revert'), + disabled: true, + handler: function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rowdef = me.rows[rec.data.key] || {}; + var keys = rowdef.multiKey || [ rec.data.key ]; + var revert = keys.join(','); + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'revert': revert + }, + callback: function() { + me.reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert('Error',response.htmlStatus); + } + }); + } + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + + var key = rec.data.key; + var pending = rec.data['delete'] || me.hasPendingChanges(key); + var rowdef = rows[key]; + + edit_btn.setDisabled(!rowdef.editor); + revert_btn.setDisabled(!pending); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/pending", + interval: 5000, + cwidth1: 250, + tbar: [ edit_btn, revert_btn ], + rows: rows, + editorConfig: { + url: "/api2/extjs/" + baseurl + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + me.rstore.on('datachanged', function() { + set_button_status(); + }); + } +}); + +Ext.define('PVE.window.Snapshot', { + extend: 'Ext.window.Window', + + resizable: false, + + // needed for finding the reference to submitbutton + // because we do not have a controller + referenceHolder: true, + defaultButton: 'submitbutton', + defaultFocus: 'field', + + take_snapshot: function(snapname, descr, vmstate) { + var me = this; + var params = { snapname: snapname, vmstate: vmstate ? 1 : 0 }; + if (descr) { + params.description = descr; + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot", + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + me.close(); + } + }); + }, + + update_snapshot: function(snapname, descr) { + var me = this; + Proxmox.Utils.API2Request({ + params: { description: descr }, + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" + + snapname + '/config', + waitMsgTarget: me, + method: 'PUT', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + me.close(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + var summarystore = Ext.create('Ext.data.Store', { + model: 'KeyValue', + sorters: [ + { + property : 'key', + direction: 'ASC' + } + ] + }); + + var items = [ + { + xtype: me.snapname ? 'displayfield' : 'textfield', + name: 'snapname', + value: me.snapname, + fieldLabel: gettext('Name'), + vtype: 'ConfigId', + allowBlank: false + } + ]; + + if (me.snapname) { + items.push({ + xtype: 'displayfield', + name: 'snaptime', + renderer: PVE.Utils.render_timestamp_human_readable, + fieldLabel: gettext('Timestamp') + }); + } else { + items.push({ + xtype: 'proxmoxcheckbox', + name: 'vmstate', + uncheckedValue: 0, + defaultValue: 0, + checked: 1, + fieldLabel: gettext('Include RAM') + }); + } + + items.push({ + xtype: 'textareafield', + grow: true, + name: 'description', + fieldLabel: gettext('Description') + }); + + if (me.snapname) { + items.push({ + title: gettext('Settings'), + xtype: 'grid', + height: 200, + store: summarystore, + columns: [ + {header: gettext('Key'), width: 150, dataIndex: 'key'}, + {header: gettext('Value'), flex: 1, dataIndex: 'value'} + ] + }); + } + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + if (me.snapname) { + me.title = gettext('Edit') + ': ' + gettext('Snapshot'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Update'), + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.update_snapshot(me.snapname, values.description); + } + } + }); + } else { + me.title ="VM " + me.vmid + ': ' + gettext('Take Snapshot'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Take Snapshot'), + reference: 'submitbutton', + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.take_snapshot(values.snapname, values.description, values.vmstate); + } + } + }); + } + + Ext.apply(me, { + modal: true, + width: 450, + border: false, + layout: 'fit', + buttons: [ submitBtn ], + items: [ me.formPanel ] + }); + + if (me.snapname) { + Ext.apply(me, { + width: 620, + height: 420 + }); + } + + me.callParent(); + + if (!me.snapname) { + return; + } + + // else load data + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" + + me.snapname + '/config', + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.close(); + }, + success: function(response, options) { + var data = response.result.data; + var kvarray = []; + Ext.Object.each(data, function(key, value) { + if (key === 'description' || key === 'snaptime') { + return; + } + kvarray.push({ key: key, value: value }); + }); + + summarystore.suspendEvents(); + summarystore.add(kvarray); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh', summarystore); + + form.findField('snaptime').setValue(data.snaptime); + form.findField('description').setValue(data.description); + } + }); + } +}); +Ext.define('PVE.qemu.SnapshotTree', { + extend: 'Ext.tree.Panel', + alias: ['widget.pveQemuSnapshotTree'], + + load_delay: 3000, + + old_digest: 'invalid', + + stateful: true, + stateId: 'grid-qemu-snapshots', + + sorterFn: function(rec1, rec2) { + var v1 = rec1.data.snaptime; + var v2 = rec2.data.snaptime; + + if (rec1.data.name === 'current') { + return 1; + } + if (rec2.data.name === 'current') { + return -1; + } + + return (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)); + }, + + reload: function(repeat) { + var me = this; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot', + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + me.load_task.delay(me.load_delay); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + var digest = 'invalid'; + var idhash = {}; + var root = { name: '__root', expanded: true, children: [] }; + Ext.Array.each(response.result.data, function(item) { + item.leaf = true; + item.children = []; + if (item.name === 'current') { + digest = item.digest + item.running; + if (item.running) { + item.iconCls = 'fa fa-fw fa-desktop x-fa-tree-running'; + } else { + item.iconCls = 'fa fa-fw fa-desktop x-fa-tree'; + } + } else { + item.iconCls = 'fa fa-fw fa-history x-fa-tree'; + } + idhash[item.name] = item; + }); + + if (digest !== me.old_digest) { + me.old_digest = digest; + + Ext.Array.each(response.result.data, function(item) { + if (item.parent && idhash[item.parent]) { + var parent_item = idhash[item.parent]; + parent_item.children.push(item); + parent_item.leaf = false; + parent_item.expanded = true; + parent_item.expandable = false; + } else { + root.children.push(item); + } + }); + + me.setRootNode(root); + } + + me.load_task.delay(me.load_delay); + } + }); + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/feature', + params: { feature: 'snapshot' }, + method: 'GET', + success: function(response, options) { + var res = response.result.data; + if (res.hasFeature) { + var snpBtns = Ext.ComponentQuery.query('#snapshotBtn'); + snpBtns.forEach(function(item){ + item.enable(); + }); + } + } + }); + + + }, + + listeners: { + beforestatesave: function(grid, state, eopts) { + // extjs cannot serialize functions, + // so a the sorter with only the sorterFn will + // not be a valid sorter when restoring the state + delete state.storeState.sorters; + } + }, + + initComponent: function() { + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + me.vmid = me.pveSelNode.data.vmid; + if (!me.vmid) { + throw "no VM ID specified"; + } + + me.load_task = new Ext.util.DelayedTask(me.reload, me); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var valid_snapshot = function(record) { + return record && record.data && record.data.name && + record.data.name !== 'current'; + }; + + var valid_snapshot_rollback = function(record) { + return record && record.data && record.data.name && + record.data.name !== 'current' && !record.data.snapstate; + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (valid_snapshot(rec)) { + var win = Ext.create('PVE.window.Snapshot', { + snapname: rec.data.name, + nodename: me.nodename, + vmid: me.vmid + }); + win.show(); + me.mon(win, 'close', me.reload, me); + } + }; + + var editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: valid_snapshot, + handler: run_editor + }); + + var rollbackBtn = new Proxmox.button.Button({ + text: gettext('Rollback'), + disabled: true, + selModel: sm, + enableFn: valid_snapshot_rollback, + confirmMsg: function(rec) { + return Proxmox.Utils.format_task_description('qmrollback', me.vmid) + + " '" + rec.data.name + "'"; + }, + handler: function(btn, event) { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var snapname = rec.data.name; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname + '/rollback', + method: 'POST', + waitMsgTarget: me, + callback: function() { + me.reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + } + }); + } + }); + + var removeBtn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.name + "'"); + return msg; + }, + enableFn: valid_snapshot, + handler: function(btn, event) { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var snapname = rec.data.name; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname, + method: 'DELETE', + waitMsgTarget: me, + callback: function() { + me.reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + } + }); + } + }); + + var snapshotBtn = Ext.create('Ext.Button', { + itemId: 'snapshotBtn', + text: gettext('Take Snapshot'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.window.Snapshot', { + nodename: me.nodename, + vmid: me.vmid + }); + win.show(); + } + }); + + Ext.apply(me, { + layout: 'fit', + rootVisible: false, + animate: false, + sortableColumns: false, + selModel: sm, + tbar: [ snapshotBtn, rollbackBtn, removeBtn, editBtn ], + fields: [ + 'name', 'description', 'snapstate', 'vmstate', 'running', + { name: 'snaptime', type: 'date', dateFormat: 'timestamp' } + ], + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'name', + width: 200, + renderer: function(value, metaData, record) { + if (value === 'current') { + return "NOW"; + } else { + return value; + } + } + }, + { + text: gettext('RAM'), + align: 'center', + resizable: false, + dataIndex: 'vmstate', + width: 50, + renderer: function(value, metaData, record) { + if (record.data.name !== 'current') { + return Proxmox.Utils.format_boolean(value); + } + } + }, + { + text: gettext('Date') + "/" + gettext("Status"), + dataIndex: 'snaptime', + width: 150, + renderer: function(value, metaData, record) { + if (record.data.snapstate) { + return record.data.snapstate; + } + if (value) { + return Ext.Date.format(value,'Y-m-d H:i:s'); + } + } + }, + { + text: gettext('Description'), + dataIndex: 'description', + flex: 1, + renderer: function(value, metaData, record) { + if (record.data.name === 'current') { + return gettext("You are here!"); + } else { + return Ext.String.htmlEncode(value); + } + } + } + ], + columnLines: true, // will work in 4.1? + listeners: { + activate: me.reload, + destroy: me.load_task.cancel, + itemdblclick: run_editor + } + }); + + me.callParent(); + + me.store.sorters.add(new Ext.util.Sorter({ + sorterFn: me.sorterFn + })); + } +}); + +Ext.define('PVE.qemu.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.qemu.Config', + + onlineHelp: 'chapter_virtual_machines', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var template = !!me.pveSelNode.data.template; + + var running = !!me.pveSelNode.data.uptime; + + var caps = Ext.state.Manager.get('GuiCap'); + + var base_url = '/nodes/' + nodename + "/qemu/" + vmid; + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json' + base_url + '/status/current', + interval: 1000 + }); + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: base_url + '/status/' + cmd, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + var resumeBtn = Ext.create('Ext.Button', { + text: gettext('Resume'), + disabled: !caps.vms['VM.PowerMgmt'], + hidden: true, + handler: function() { + vm_command('resume'); + }, + iconCls: 'fa fa-play' + }); + + var startBtn = Ext.create('Ext.Button', { + text: gettext('Start'), + disabled: !caps.vms['VM.PowerMgmt'] || running, + hidden: template, + handler: function() { + vm_command('start'); + }, + iconCls: 'fa fa-play' + }); + + var migrateBtn = Ext.create('Ext.Button', { + text: gettext('Migrate'), + disabled: !caps.vms['VM.Migrate'], + hidden: PVE.data.ResourceStore.getNodes().length < 2, + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'qemu', + nodename: nodename, + vmid: vmid + }); + win.show(); + }, + iconCls: 'fa fa-send-o' + }); + + var moreBtn = Ext.create('Proxmox.button.Button', { + text: gettext('More'), + menu: { items: [ + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: caps.vms['VM.Clone'] ? false : true, + handler: function() { + PVE.window.Clone.wrap(nodename, vmid, template, 'qemu'); + } + }, + { + text: gettext('Convert to template'), + disabled: template, + xtype: 'pveMenuItem', + iconCls: 'fa fa-fw fa-file-o', + hidden: caps.vms['VM.Allocate'] ? false : true, + confirmMsg: Proxmox.Utils.format_task_description('qmtemplate', vmid), + handler: function() { + Proxmox.Utils.API2Request({ + url: base_url + '/template', + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + } + }, + { + iconCls: 'fa fa-heartbeat ', + hidden: !caps.nodes['Sys.Console'], + text: gettext('Manage HA'), + handler: function() { + var ha = me.pveSelNode.data.hastate; + Ext.create('PVE.ha.VMResourceEdit', { + vmid: vmid, + isCreate: (!ha || ha === 'unmanaged') + }).show(); + } + }, + { + text: gettext('Remove'), + itemId: 'removeBtn', + disabled: !caps.vms['VM.Allocate'], + handler: function() { + Ext.create('PVE.window.SafeDestroy', { + url: base_url, + item: { type: 'VM', id: vmid } + }).show(); + }, + iconCls: 'fa fa-trash-o' + } + ]} + }); + + var shutdownBtn = Ext.create('PVE.button.Split', { + text: gettext('Shutdown'), + disabled: !caps.vms['VM.PowerMgmt'] || !running, + hidden: template, + confirmMsg: Proxmox.Utils.format_task_description('qmshutdown', vmid), + handler: function() { + vm_command('shutdown'); + }, + menu: { + items: [{ + text: gettext('Pause'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('qmpause', vmid), + handler: function() { + vm_command("suspend"); + }, + iconCls: 'fa fa-pause' + },{ + text: gettext('Hibernate'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('qmsuspend', vmid), + tooltip: gettext('Suspend to disk'), + handler: function() { + vm_command("suspend", { todisk: 1 }); + }, + iconCls: 'fa fa-download' + },{ + text: gettext('Stop'), + disabled: !caps.vms['VM.PowerMgmt'], + dangerous: true, + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'), + confirmMsg: Proxmox.Utils.format_task_description('qmstop', vmid), + handler: function() { + vm_command("stop", { timeout: 30 }); + }, + iconCls: 'fa fa-stop' + },{ + text: gettext('Reset'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('qmreset', vmid), + handler: function() { + vm_command("reset"); + }, + iconCls: 'fa fa-bolt' + }] + }, + iconCls: 'fa fa-power-off' + }); + + var vm = me.pveSelNode.data; + + var consoleBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.vms['VM.Console'], + hidden: template, + consoleType: 'kvm', + consoleName: vm.name, + nodename: nodename, + vmid: vmid + }); + + var statusTxt = Ext.create('Ext.toolbar.TextItem', { + data: { + lock: undefined + }, + tpl: [ + '', + ' ({lock})', + '' + ] + }); + + Ext.apply(me, { + title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm.text, nodename), + hstateid: 'kvmtab', + tbarSpacing: false, + tbar: [ statusTxt, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn ], + defaults: { statusStore: me.statusStore }, + items: [ + { + title: gettext('Summary'), + xtype: 'pveQemuSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + } + ] + }); + + if (caps.vms['VM.Console'] && !template) { + me.items.push({ + title: gettext('Console'), + itemId: 'console', + iconCls: 'fa fa-terminal', + xtype: 'pveNoVncConsole', + vmid: vmid, + consoleType: 'kvm', + nodename: nodename + }); + } + + me.items.push( + { + title: gettext('Hardware'), + itemId: 'hardware', + iconCls: 'fa fa-desktop', + xtype: 'PVE.qemu.HardwareView' + }, + { + title: 'Cloud-Init', + itemId: 'cloudinit', + iconCls: 'fa fa-cloud', + xtype: 'pveCiPanel' + }, + { + title: gettext('Options'), + iconCls: 'fa fa-gear', + itemId: 'options', + xtype: 'PVE.qemu.Options' + }, + { + title: gettext('Task History'), + itemId: 'tasks', + xtype: 'proxmoxNodeTasks', + iconCls: 'fa fa-list', + nodename: nodename, + vmidFilter: vmid + } + ); + + if (caps.vms['VM.Monitor'] && !template) { + me.items.push({ + title: gettext('Monitor'), + iconCls: 'fa fa-eye', + itemId: 'monitor', + xtype: 'pveQemuMonitor' + }); + } + + if (caps.vms['VM.Backup']) { + me.items.push({ + title: gettext('Backup'), + iconCls: 'fa fa-floppy-o', + xtype: 'pveBackupView', + itemId: 'backup' + }, + { + title: gettext('Replication'), + iconCls: 'fa fa-retweet', + xtype: 'pveReplicaView', + itemId: 'replication' + }); + } + + if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback']) && !template) { + me.items.push({ + title: gettext('Snapshots'), + iconCls: 'fa fa-history', + xtype: 'pveQemuSnapshotTree', + itemId: 'snapshot' + }); + } + + if (caps.vms['VM.Console']) { + me.items.push( + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + iconCls: 'fa fa-shield', + allow_iface: true, + base_url: base_url + '/firewall/rules', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + groups: ['firewall'], + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_vm_container_configuration', + title: gettext('Options'), + base_url: base_url + '/firewall/options', + fwtype: 'vm', + itemId: 'firewall-options' + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: base_url + '/firewall/aliases', + itemId: 'firewall-aliases' + }, + { + xtype: 'pveIPSet', + title: gettext('IPSet'), + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: base_url + '/firewall/ipset', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall-ipset' + }, + { + title: gettext('Log'), + groups: ['firewall'], + iconCls: 'fa fa-list', + onlineHelp: 'chapter_pve_firewall', + itemId: 'firewall-fwlog', + xtype: 'proxmoxLogView', + url: '/api2/extjs' + base_url + '/firewall/log' + } + ); + } + + if (caps.vms['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: '/vms/' + vmid + }); + } + + me.callParent(); + + me.mon(me.statusStore, 'load', function(s, records, success) { + var status; + var qmpstatus; + var spice = false; + var xtermjs = false; + var lock; + + if (!success) { + status = qmpstatus = 'unknown'; + } else { + var rec = s.data.get('status'); + status = rec ? rec.data.value : 'unknown'; + rec = s.data.get('qmpstatus'); + qmpstatus = rec ? rec.data.value : 'unknown'; + rec = s.data.get('template'); + template = rec.data.value || false; + rec = s.data.get('lock'); + lock = rec ? rec.data.value : undefined; + + spice = s.data.get('spice') ? true : false; + xtermjs = s.data.get('serial') ? true : false; + + } + + if (template) { + return; + } + + var resume = (['prelaunch', 'paused', 'suspended'].indexOf(qmpstatus) !== -1); + + if (resume || lock === 'suspended') { + startBtn.setVisible(false); + resumeBtn.setVisible(true); + } else { + startBtn.setVisible(true); + resumeBtn.setVisible(false); + } + + consoleBtn.setEnableSpice(spice); + consoleBtn.setEnableXtermJS(xtermjs); + + statusTxt.update({ lock: lock }); + + startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template); + shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running'); + me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped'); + consoleBtn.setDisabled(template); + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.qemu.CreateWizard', { + extend: 'PVE.window.Wizard', + alias: 'widget.pveQemuCreateWizard', + mixins: ['Proxmox.Mixin.CBind'], + + viewModel: { + data: { + nodename: '', + current: { + scsihw: '' + } + } + }, + + cbindData: { + nodename: undefined + }, + + subject: gettext('Virtual Machine'), + + items: [ + { + xtype: 'inputpanel', + title: gettext('General'), + onlineHelp: 'qm_general_settings', + column1: [ + { + xtype: 'pveNodeSelector', + name: 'nodename', + cbind: { + selectCurNode: '{!nodename}', + preferredValue: '{nodename}' + }, + bind: { + value: '{nodename}' + }, + fieldLabel: gettext('Node'), + allowBlank: false, + onlineValidator: true + }, + { + xtype: 'pveGuestIDSelector', + name: 'vmid', + guestType: 'qemu', + value: '', + loadNextFreeID: true, + validateExists: false + }, + { + xtype: 'textfield', + name: 'name', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Name'), + allowBlank: true + } + ], + column2: [ + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true + } + ], + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Start at boot') + } + ], + advancedColumn2: [ + { + xtype: 'textfield', + name: 'order', + defaultValue: '', + emptyText: 'any', + labelWidth: 120, + fieldLabel: gettext('Start/Shutdown order') + }, + { + xtype: 'textfield', + name: 'up', + defaultValue: '', + emptyText: 'default', + labelWidth: 120, + fieldLabel: gettext('Startup delay') + }, + { + xtype: 'textfield', + name: 'down', + defaultValue: '', + emptyText: 'default', + labelWidth: 120, + fieldLabel: gettext('Shutdown timeout') + } + ], + onGetValues: function(values) { + + ['name', 'pool', 'onboot', 'agent'].forEach(function(field) { + if (!values[field]) { + delete values[field]; + } + }); + + var res = PVE.Parser.printStartup({ + order: values.order, + up: values.up, + down: values.down + }); + + if (res) { + values.startup = res; + } + + delete values.order; + delete values.up; + delete values.down; + + return values; + } + }, + { + xtype: 'container', + layout: 'hbox', + defaults: { + flex: 1, + padding: '0 10' + }, + title: gettext('OS'), + items: [ + { + xtype: 'pveQemuCDInputPanel', + bind: { + nodename: '{nodename}' + }, + confid: 'ide2', + insideWizard: true + }, + { + xtype: 'pveQemuOSTypePanel', + insideWizard: true + } + ] + }, + { + xtype: 'pveQemuSystemPanel', + title: gettext('System'), + isCreate: true, + insideWizard: true + }, + { + xtype: 'pveQemuHDInputPanel', + bind: { + nodename: '{nodename}' + }, + title: gettext('Hard Disk'), + isCreate: true, + insideWizard: true + }, + { + xtype: 'pveQemuProcessorPanel', + insideWizard: true, + title: gettext('CPU') + }, + { + xtype: 'pveQemuMemoryPanel', + insideWizard: true, + title: gettext('Memory') + }, + { + xtype: 'pveQemuNetworkInputPanel', + bind: { + nodename: '{nodename}' + }, + title: gettext('Network'), + insideWizard: true + }, + { + title: gettext('Confirm'), + layout: 'fit', + items: [ + { + xtype: 'grid', + store: { + model: 'KeyValue', + sorters: [{ + property : 'key', + direction: 'ASC' + }] + }, + columns: [ + {header: 'Key', width: 150, dataIndex: 'key'}, + {header: 'Value', flex: 1, dataIndex: 'value'} + ] + } + ], + dockedItems: [ + { + xtype: 'proxmoxcheckbox', + name: 'start', + dock: 'bottom', + margin: '5 0 0 0', + boxLabel: gettext('Start after created') + } + ], + listeners: { + show: function(panel) { + var kv = this.up('window').getValues(); + var data = []; + Ext.Object.each(kv, function(key, value) { + if (key === 'delete') { // ignore + return; + } + data.push({ key: key, value: value }); + }); + + var summarystore = panel.down('grid').getStore(); + summarystore.suspendEvents(); + summarystore.removeAll(); + summarystore.add(data); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh'); + + } + }, + onSubmit: function() { + var wizard = this.up('window'); + var kv = wizard.getValues(); + delete kv['delete']; + + var nodename = kv.nodename; + delete kv.nodename; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/qemu', + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function(response){ + wizard.close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + } + ] +}); + + + + +Ext.define('PVE.qemu.USBInputPanel', { + extend: 'Proxmox.panel.InputPanel', + mixins: ['Proxmox.Mixin.CBind' ], + + autoComplete: false, + onlineHelp: 'qm_usb_passthrough', + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'field[name=usb]': { + change: function(field, newValue, oldValue) { + var hwidfield = this.lookupReference('hwid'); + var portfield = this.lookupReference('port'); + var usb3field = this.lookupReference('usb3'); + if (field.inputValue === 'hostdevice') { + hwidfield.setDisabled(!newValue); + } else if(field.inputValue === 'port') { + portfield.setDisabled(!newValue); + } else if(field.inputValue === 'spice') { + usb3field.setDisabled(newValue); + } + } + }, + 'pveUSBSelector': { + change: function(field, newValue, oldValue) { + var usbval = field.getUSBValue(); + var usb3field = this.lookupReference('usb3'); + var usb3 = /usb3/.test(usbval); + if(usb3 && !usb3field.isDisabled()) { + usb3field.savedVal = usb3field.getValue(); + usb3field.setValue(true); + usb3field.setDisabled(true); + } else if(!usb3 && usb3field.isDisabled()){ + var val = (usb3field.savedVal === undefined)?usb3field.originalValue:usb3field.savedVal; + usb3field.setValue(val); + usb3field.setDisabled(false); + } + } + } + } + }, + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + }, + + onGetValues: function(values) { + var me = this; + if(!me.confid) { + var i; + for (i = 0; i < 6; i++) { + if (!me.vmconfig['usb' + i.toString()]) { + me.confid = 'usb' + i.toString(); + break; + } + } + } + var val = ""; + var type = me.down('radiofield').getGroupValue(); + switch (type) { + case 'spice': + val = 'spice'; break; + case 'hostdevice': + case 'port': + val = me.down('pveUSBSelector[name=' + type + ']').getUSBValue(); + if (!/usb3/.test(val) && me.down('field[name=usb3]').getValue() === true) { + val += ',usb3=1'; + } + break; + default: + throw "invalid type selected"; + } + + values[me.confid] = val; + return values; + }, + + items: [ + { + xtype: 'fieldcontainer', + defaultType: 'radiofield', + items:[ + { + name: 'usb', + inputValue: 'spice', + boxLabel: gettext('Spice Port'), + submitValue: false, + checked: true + }, + { + name: 'usb', + inputValue: 'hostdevice', + boxLabel: gettext('Use USB Vendor/Device ID'), + submitValue: false + }, + { + xtype: 'pveUSBSelector', + disabled: true, + type: 'device', + name: 'hostdevice', + cbind: { pveSelNode: '{pveSelNode}' }, + editable: true, + reference: 'hwid', + allowBlank: false, + fieldLabel: 'Choose Device', + labelAlign: 'right', + submitValue: false + }, + { + name: 'usb', + inputValue: 'port', + boxLabel: gettext('Use USB Port'), + submitValue: false + }, + { + xtype: 'pveUSBSelector', + disabled: true, + name: 'port', + cbind: { pveSelNode: '{pveSelNode}' }, + editable: true, + type: 'port', + reference: 'port', + allowBlank: false, + fieldLabel: gettext('Choose Port'), + labelAlign: 'right', + submitValue: false + }, + { + xtype: 'checkbox', + name: 'usb3', + submitValue: false, + reference: 'usb3', + fieldLabel: gettext('Use USB3') + } + ] + } + ] +}); + +Ext.define('PVE.qemu.USBEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('USB Device'), + + + initComponent : function() { + var me = this; + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.USBInputPanel', { + confid: me.confid, + pveSelNode: me.pveSelNode + }); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var data = response.result.data[me.confid].split(','); + var port, hostdevice, usb3 = false; + var type = 'spice'; + var i; + for (i = 0; i < data.length; i++) { + if (/^(host=)?(0x)?[a-zA-Z0-9]{4}\:(0x)?[a-zA-Z0-9]{4}$/.test(data[i])) { + hostdevice = data[i]; + hostdevice = hostdevice.replace('host=', '').replace('0x',''); + type = 'hostdevice'; + } else if (/^(host=)?(\d+)\-(\d+(\.\d+)*)$/.test(data[i])) { + port = data[i]; + port = port.replace('host=',''); + type = 'port'; + } + + if (/^usb3=(1|on|true)$/.test(data[i])) { + usb3 = true; + } + } + var values = { + usb : type, + hostdevice: hostdevice, + port: port, + usb3: usb3 + }; + + ipanel.setValues(values); + } + } + }); + } +}); +Ext.define('PVE.qemu.PCIInputPanel', { + extend: 'Proxmox.panel.InputPanel', + + onlineHelp: 'qm_pci_passthrough', + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + + var hostpci = me.vmconfig[me.confid] || ''; + + var values = PVE.Parser.parsePropertyString(hostpci, 'host'); + if (values.host && values.host.length < 6) { // 00:00 format not 00:00.0 + values.host += ".0"; + values.multifunction = true; + } + values['x-vga'] = PVE.Parser.parseBoolean(values['x-vga'], 0); + values.pcie = PVE.Parser.parseBoolean(values.pcie, 0); + values.rombar = PVE.Parser.parseBoolean(values.rombar, 1); + + me.setValues(values); + if (!me.vmconfig.machine || me.vmconfig.machine.indexOf('q35') === -1) { + // machine is not set to some variant of q35, so we disable pcie + var pcie = me.down('field[name=pcie]'); + pcie.setDisabled(true); + pcie.setBoxLabel(gettext('Q35 only')); + } + + if (values.romfile) { + me.down('field[name=romfile]').setVisible(true); + } + }, + + onGetValues: function(values) { + var me = this; + var ret = {}; + if(!me.confid) { + var i; + for (i = 0; i < 5; i++) { + if (!me.vmconfig['hostpci' + i.toString()]) { + me.confid = 'hostpci' + i.toString(); + break; + } + } + } + if (values.multifunction) { + // modify host to skip the '.X' + values.host = values.host.substring(0,5); + delete values.multifunction; + } + + if (values.rombar) { + delete values.rombar; + } else { + values.rombar = 0; + } + + if (!values.romfile) { + delete values.romfile; + } + + ret[me.confid] = PVE.Parser.printPropertyString(values, 'host'); + return ret; + }, + + initComponent: function() { + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + me.column1 = [ + { + xtype: 'pvePCISelector', + fieldLabel: gettext('Device'), + name: 'host', + nodename: me.nodename, + allowBlank: false, + onLoadCallBack: function(store, records, success) { + if (!success || !records.length) { + return; + } + + var first = records[0]; + if (first.data.iommugroup === -1) { + // no iommu groups + var warning = Ext.create('Ext.form.field.Display', { + columnWidth: 1, + padding: '0 0 10 0', + value: 'No IOMMU detected, please activate it.' + + 'See Documentation for further information.', + userCls: 'pve-hint' + }); + me.items.insert(0, warning); + me.updateLayout(); // insert does not trigger that + } + }, + listeners: { + change: function(pcisel, value) { + if (!value) { + return; + } + var pcidev = pcisel.getStore().getById(value); + var mdevfield = me.down('field[name=mdev]'); + mdevfield.setDisabled(!pcidev || !pcidev.data.mdev); + if (!pcidev) { + return; + } + var id = pcidev.data.id.substring(0,5); // 00:00 + var iommu = pcidev.data.iommugroup; + // try to find out if there are more devices + // in that iommu group + if (iommu !== -1) { + var count = 0; + pcisel.getStore().each(function(record) { + if (record.data.iommugroup === iommu && + record.data.id.substring(0,5) !== id) + { + count++; + return false; + } + }); + var warning = me.down('#iommuwarning'); + if (count && !warning) { + warning = Ext.create('Ext.form.field.Display', { + columnWidth: 1, + padding: '0 0 10 0', + itemId: 'iommuwarning', + value: 'The selected Device is not in a seperate' + + 'IOMMU group, make sure this is intended.', + userCls: 'pve-hint' + }); + me.items.insert(0, warning); + me.updateLayout(); // insert does not trigger that + } else if (!count && warning) { + me.remove(warning); + } + } + if (pcidev.data.mdev) { + mdevfield.setPciID(value); + } + } + } + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('All Functions'), + name: 'multifunction' + } + ]; + + me.column2 = [ + { + xtype: 'pveMDevSelector', + name: 'mdev', + disabled: true, + fieldLabel: gettext('MDev Type'), + nodename: me.nodename, + listeners: { + change: function(field, value) { + var mf = me.down('field[name=multifunction]'); + if (!!value) { + mf.setValue(false); + } + mf.setDisabled(!!value); + } + } + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Primary GPU'), + name: 'x-vga' + } + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'ROM-Bar', + name: 'rombar' + }, + { + xtype: 'displayfield', + submitValue: true, + hidden: true, + fieldLabel: 'ROM-File', + name: 'romfile' + } + ]; + + me.advancedColumn2 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'PCI-Express', + name: 'pcie' + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.PCIEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('PCI Device'), + + + initComponent : function() { + var me = this; + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.PCIInputPanel', { + confid: me.confid, + pveSelNode: me.pveSelNode + }); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response) { + ipanel.setVMConfig(response.result.data); + } + }); + } +}); +/*jslint confusion: true */ +Ext.define('PVE.qemu.SerialnputPanel', { + extend: 'Proxmox.panel.InputPanel', + + autoComplete: false, + + setVMConfig: function(vmconfig) { + var me = this, i; + me.vmconfig = vmconfig; + + for (i = 0; i < 4; i++) { + var port = 'serial' + i.toString(); + if (!me.vmconfig[port]) { + me.down('field[name=serialid]').setValue(i); + break; + } + } + + }, + + onGetValues: function(values) { + var me = this; + + var id = 'serial' + values.serialid; + delete values.serialid; + values[id] = 'socket'; + return values; + }, + + items: [ + { + xtype: 'proxmoxintegerfield', + name: 'serialid', + fieldLabel: gettext('Serial Port'), + minValue: 0, + maxValue: 3, + allowBlank: false, + validator: function(id) { + if (!this.rendered) { + return true; + } + var me = this.up('panel'); + if (me.vmconfig !== undefined && Ext.isDefined(me.vmconfig['serial' + id])) { + return "This device is already in use."; + } + return true; + } + } + ] +}); + +Ext.define('PVE.qemu.SerialEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('Serial Port'), + + initComponent : function() { + var me = this; + + // for now create of (socket) serial port only + me.isCreate = true; + + var ipanel = Ext.create('PVE.qemu.SerialnputPanel', {}); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + } + }); + } +}); +Ext.define('PVE.window.IPInfo', { + extend: 'Ext.window.Window', + width: 600, + title: gettext('Guest Agent Network Information'), + height: 300, + layout: { + type: 'fit' + }, + modal: true, + items: [ + { + xtype: 'grid', + emptyText: gettext('No network information'), + columns: [ + { + dataIndex: 'name', + text: gettext('Name'), + flex: 3 + }, + { + dataIndex: 'hardware-address', + text: gettext('MAC address'), + width: 140 + }, + { + dataIndex: 'ip-addresses', + text: gettext('IP address'), + align: 'right', + flex: 4, + renderer: function(val) { + if (!Ext.isArray(val)) { + return ''; + } + var ips = []; + val.forEach(function(ip) { + var addr = ip['ip-address']; + var pref = ip.prefix; + if (addr && pref) { + ips.push(addr + '/' + pref); + } + }); + return ips.join('
'); + } + } + ] + } + ] +}); + +Ext.define('PVE.qemu.AgentIPView', { + extend: 'Ext.container.Container', + xtype: 'pveAgentIPView', + + layout: { + type: 'hbox', + align: 'top' + }, + + nics: [], + + items: [ + { + xtype: 'box', + html: ' IPs' + }, + { + xtype: 'container', + flex: 1, + layout: { + type: 'vbox', + align: 'right', + pack: 'end' + }, + items: [ + { + xtype: 'label', + flex: 1, + itemId: 'ipBox', + style: { + 'text-align': 'right' + } + }, + { + xtype: 'button', + itemId: 'moreBtn', + hidden: true, + ui: 'default-toolbar', + handler: function(btn) { + var me = this.up('pveAgentIPView'); + + var win = Ext.create('PVE.window.IPInfo'); + win.down('grid').getStore().setData(me.nics); + win.show(); + }, + text: gettext('More') + } + ] + } + ], + + getDefaultIps: function(nics) { + var me = this; + var ips = []; + nics.forEach(function(nic) { + if (nic['hardware-address'] && + nic['hardware-address'] != '00:00:00:00:00:00') { + + var nic_ips = nic['ip-addresses'] || []; + nic_ips.forEach(function(ip) { + var p = ip['ip-address']; + // show 2 ips at maximum + if (ips.length < 2) { + ips.push(p); + } + }); + } + }); + + return ips; + }, + + startIPStore: function(store, records, success) { + var me = this; + var agentRec = store.getById('agent'); + /*jslint confusion: true*/ + /* value is number and string */ + me.agent = (agentRec && agentRec.data.value === 1); + me.running = (store.getById('status').data.value === 'running'); + /*jslint confusion: false*/ + + var caps = Ext.state.Manager.get('GuiCap'); + + if (!caps.vms['VM.Monitor']) { + var errorText = gettext("Requires '{0}' Privileges"); + me.updateStatus(false, Ext.String.format(errorText, 'VM.Monitor')); + return; + } + + if (me.agent && me.running && me.ipStore.isStopped) { + me.ipStore.startUpdate(); + } else if (me.ipStore.isStopped) { + me.updateStatus(); + } + }, + + updateStatus: function(unsuccessful, defaulttext) { + var me = this; + var text = defaulttext || gettext('No network information'); + var more = false; + if (unsuccessful) { + text = gettext('Guest Agent not running'); + } else if (me.agent && me.running) { + if (Ext.isArray(me.nics) && me.nics.length) { + more = true; + var ips = me.getDefaultIps(me.nics); + if (ips.length !== 0) { + text = ips.join('
'); + } + } else if (me.nics && me.nics.error) { + var msg = gettext('Cannot get info from Guest Agent
Error: {0}'); + text = Ext.String.format(text, me.nics.error.desc); + } + } else if (me.agent) { + text = gettext('Guest Agent not running'); + } else { + text = gettext('No Guest Agent configured'); + } + + var ipBox = me.down('#ipBox'); + ipBox.update(text); + + var moreBtn = me.down('#moreBtn'); + moreBtn.setVisible(more); + }, + + initComponent: function() { + var me = this; + + if (!me.rstore) { + throw 'rstore not given'; + } + + if (!me.pveSelNode) { + throw 'pveSelNode not given'; + } + + var nodename = me.pveSelNode.data.node; + var vmid = me.pveSelNode.data.vmid; + + me.ipStore = Ext.create('Proxmox.data.UpdateStore', { + interval: 10000, + storeid: 'pve-qemu-agent-' + vmid, + method: 'POST', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + nodename + '/qemu/' + vmid + '/agent/network-get-interfaces' + } + }); + + me.callParent(); + + me.mon(me.ipStore, 'load', function(store, records, success) { + if (records && records.length) { + me.nics = records[0].data.result; + } else { + me.nics = undefined; + } + me.updateStatus(!success); + }); + + me.on('destroy', me.ipStore.stopUpdate); + + // if we already have info about the vm, use it immediately + if (me.rstore.getCount()) { + me.startIPStore(me.rstore, me.rstore.getData(), false); + } + + // check if the guest agent is there on every statusstore load + me.mon(me.rstore, 'load', me.startIPStore, me); + } +}); +Ext.define('PVE.qemu.CloudInit', { + extend: 'Proxmox.grid.PendingObjectGrid', + xtype: 'pveCiPanel', + + onlineHelp: 'qm_cloud_init', + + tbar: [ + { + xtype: 'proxmoxButton', + disabled: true, + dangerous: true, + confirmMsg: function(rec) { + var me = this.up('grid'); + var warn = gettext('Are you sure you want to remove entry {0}'); + + var entry = rec.data.key; + var msg = Ext.String.format(warn, "'" + + me.renderKey(entry, {}, rec) + "'"); + + return msg; + }, + enableFn: function(record) { + var me = this.up('grid'); + var caps = Ext.state.Manager.get('GuiCap'); + if (me.rows[record.data.key].never_delete || + !caps.vms['VM.Config.Network']) { + return false; + } + + if (record.data.key === 'cipassword' && !record.data.value) { + return false; + } + return true; + }, + handler: function() { + var me = this.up('grid'); + var records = me.getSelection(); + if (!records || !records.length) { + return; + } + + var id = records[0].data.key; + var match = id.match(/^net(\d+)$/); + if (match) { + id = 'ipconfig' + match[1]; + } + + var params = {}; + params['delete'] = id; + Proxmox.Utils.API2Request({ + url: me.baseurl + '/config', + waitMsgTarget: me, + method: 'PUT', + params: params, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + callback: function() { + me.reload(); + } + }); + }, + text: gettext('Remove') + }, + { + xtype: 'proxmoxButton', + disabled: true, + handler: function() { + var me = this.up('grid'); + me.run_editor(); + }, + text: gettext('Edit') + }, + '-', + { + xtype: 'button', + itemId: 'savebtn', + text: gettext('Regenerate Image'), + handler: function() { + var me = this.up('grid'); + var eject_params = {}; + var insert_params = {}; + var disk = PVE.Parser.parseQemuDrive(me.ciDriveId, me.ciDrive); + var storage = ''; + var stormatch = disk.file.match(/^([^\:]+)\:/); + if (stormatch) { + storage = stormatch[1]; + } + eject_params[me.ciDriveId] = 'none,media=cdrom'; + insert_params[me.ciDriveId] = storage + ':cloudinit'; + + var failure = function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }; + + Proxmox.Utils.API2Request({ + url: me.baseurl + '/config', + waitMsgTarget: me, + method: 'PUT', + params: eject_params, + failure: failure, + callback: function() { + Proxmox.Utils.API2Request({ + url: me.baseurl + '/config', + waitMsgTarget: me, + method: 'PUT', + params: insert_params, + failure: failure, + callback: function() { + me.reload(); + } + }); + } + }); + } + } + ], + + border: false, + + set_button_status: function(rstore, records, success) { + if (!success || records.length < 1) { + return; + } + var me = this; + var found; + records.forEach(function(record) { + if (found) { + return; + } + var id = record.data.key; + var value = record.data.value; + var ciregex = new RegExp("vm-" + me.pveSelNode.data.vmid + "-cloudinit"); + if (id.match(/^(ide|scsi|sata)\d+$/) && ciregex.test(value)) { + found = id; + me.ciDriveId = found; + me.ciDrive = value; + } + }); + + me.down('#savebtn').setDisabled(!found); + me.setDisabled(!found); + if (!found) { + me.getView().mask(gettext('No CloudInit Drive found'), ['pve-static-mask']); + } else { + me.getView().unmask(); + } + }, + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = rows[key] || {}; + + var icon = ""; + if (rowdef.iconCls) { + icon = ' '; + } + return icon + (rowdef.header || key); + }, + + listeners: { + activate: function () { + var me = this; + me.rstore.startUpdate(); + }, + itemdblclick: function() { + var me = this; + me.run_editor(); + } + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + var caps = Ext.state.Manager.get('GuiCap'); + me.baseurl = '/api2/extjs/nodes/' + nodename + '/qemu/' + vmid; + me.url = me.baseurl + '/pending'; + me.editorConfig.url = me.baseurl + '/config'; + me.editorConfig.pveSelNode = me.pveSelNode; + + /*jslint confusion: true*/ + /* editor is string and object */ + me.rows = { + ciuser: { + header: gettext('User'), + iconCls: 'fa fa-user', + never_delete: true, + defaultValue: '', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('User'), + items: [ + { + xtype: 'proxmoxtextfield', + deleteEmpty: true, + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('User'), + name: 'ciuser' + } + ] + } : undefined, + renderer: function(value) { + return value || Proxmox.Utils.defaultText; + } + }, + cipassword: { + header: gettext('Password'), + iconCls: 'fa fa-unlock', + defaultValue: '', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Password'), + items: [ + { + xtype: 'proxmoxtextfield', + inputType: 'password', + deleteEmpty: true, + emptyText: Proxmox.Utils.noneText, + fieldLabel: gettext('Password'), + name: 'cipassword' + } + ] + } : undefined, + renderer: function(value) { + return value || Proxmox.Utils.noneText; + } + }, + searchdomain: { + header: gettext('DNS domain'), + iconCls: 'fa fa-globe', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + never_delete: true, + defaultValue: gettext('use host settings') + }, + nameserver: { + header: gettext('DNS servers'), + iconCls: 'fa fa-globe', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + never_delete: true, + defaultValue: gettext('use host settings') + }, + sshkeys: { + header: gettext('SSH public key'), + iconCls: 'fa fa-key', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.SSHKeyEdit' : undefined, + never_delete: true, + renderer: function(value) { + value = decodeURIComponent(value); + var keys = value.split('\n'); + var text = []; + keys.forEach(function(key) { + if (key.length) { + // First erase all quoted strings (eg. command="foo" + var v = key.replace(/"(?:\\.|[^"\\])*"/g, ''); + // Now try to detect the comment: + var res = v.match(/^\s*(\S+\s+)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)\s+\S+\s+(.*?)\s*$/, ''); + if (res) { + key = Ext.String.htmlEncode(res[2]); + if (res[1]) { + key += ' (' + gettext('with options') + ')'; + } + text.push(key); + return; + } + // Most likely invalid at this point, so just stick to + // the old value. + text.push(Ext.String.htmlEncode(key)); + } + }); + if (text.length) { + return text.join('
'); + } else { + return Proxmox.Utils.noneText; + } + }, + defaultValue: '' + } + }; + var i; + var ipconfig_renderer = function(value, md, record, ri, ci, store, pending) { + var id = record.data.key; + var match = id.match(/^net(\d+)$/); + var val = ''; + if (match) { + val = me.getObjectValue('ipconfig'+match[1], '', pending); + } + return val; + }; + for (i = 0; i < 32; i++) { + // we want to show an entry for every network device + // even if it is empty + me.rows['net' + i.toString()] = { + multiKey: ['ipconfig' + i.toString(), 'net' + i.toString()], + header: gettext('IP Config') + ' (net' + i.toString() +')', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.IPConfigEdit' : undefined, + iconCls: 'fa fa-exchange', + renderer: ipconfig_renderer + }; + me.rows['ipconfig' + i.toString()] = { + visible: false + }; + } + /*jslint confusion: false*/ + + PVE.Utils.forEachBus(['ide', 'scsi', 'sata'], function(type, id) { + me.rows[type+id] = { + visible: false + }; + }); + me.callParent(); + me.mon(me.rstore, 'load', me.set_button_status, me); + } +}); +Ext.define('PVE.qemu.CIDriveInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveCIDriveInputPanel', + + insideWizard: false, + + vmconfig: {}, // used to select usused disks + + onGetValues: function(values) { + var me = this; + + var drive = {}; + var params = {}; + drive.file = values.hdstorage + ":cloudinit"; + drive.format = values.diskformat; + params[values.controller + values.deviceid] = PVE.Parser.printQemuDrive(drive); + return params; + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + setVMConfig: function(config) { + var me = this; + me.down('#drive').setVMConfig(config, 'cdrom'); + }, + + initComponent : function() { + var me = this; + + me.drive = {}; + + me.items = [ + { + xtype: 'pveControllerSelector', + noVirtIO: true, + itemId: 'drive', + fieldLabel: gettext('CloudInit Drive'), + name: 'drive' + }, + { + xtype: 'pveDiskStorageSelector', + itemId: 'storselector', + storageContent: 'images', + nodename: me.nodename, + hideSize: true + } + ]; + me.callParent(); + } +}); + +Ext.define('PVE.qemu.CIDriveEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCIDriveEdit', + + isCreate: true, + subject: gettext('CloudInit Drive'), + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.items = [{ + xtype: 'pveCIDriveInputPanel', + itemId: 'cipanel', + nodename: nodename + }]; + + me.callParent(); + + me.load({ + success: function(response, opts) { + me.down('#cipanel').setVMConfig(response.result.data); + } + }); + } +}); +Ext.define('PVE.qemu.SSHKeyInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveQemuSSHKeyInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + if (values.sshkeys) { + values.sshkeys.trim(); + } + if (!values.sshkeys.length) { + values = {}; + values['delete'] = 'sshkeys'; + return values; + } else { + values.sshkeys = encodeURIComponent(values.sshkeys); + } + return values; + }, + + items: [ + { + xtype: 'textarea', + itemId: 'sshkeys', + name: 'sshkeys', + height: 250 + }, + { + xtype: 'filebutton', + itemId: 'filebutton', + name: 'file', + text: gettext('Load SSH Key File'), + fieldLabel: 'test', + listeners: { + change: function(btn, e, value) { + var me = this.up('inputpanel'); + e = e.event; + Ext.Array.each(e.target.files, function(file) { + PVE.Utils.loadSSHKeyFromFile(file, function(res) { + var keysField = me.down('#sshkeys'); + var old = keysField.getValue(); + keysField.setValue(old + res); + }); + }); + btn.reset(); + } + } + } + ], + + initComponent: function() { + var me = this; + + me.callParent(); + if (!window.FileReader) { + me.down('#filebutton').setVisible(false); + } + + } +}); + +Ext.define('PVE.qemu.SSHKeyEdit', { + extend: 'Proxmox.window.Edit', + + width: 800, + + initComponent : function() { + var me = this; + + var ipanel = Ext.create('PVE.qemu.SSHKeyInputPanel'); + + Ext.apply(me, { + subject: gettext('SSH Keys'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.create) { + me.load({ + success: function(response, options) { + var data = response.result.data; + if (data.sshkeys) { + data.sshkeys = decodeURIComponent(data.sshkeys); + ipanel.setValues(data); + } + } + }); + } + } +}); +Ext.define('PVE.qemu.IPConfigPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveIPConfigPanel', + + insideWizard: false, + + vmconfig: {}, + + onGetValues: function(values) { + var me = this; + + if (values.ipv4mode !== 'static') { + values.ip = values.ipv4mode; + } + + if (values.ipv6mode !== 'static') { + values.ip6 = values.ipv6mode; + } + + var params = {}; + + var cfg = PVE.Parser.printIPConfig(values); + if (cfg === '') { + params['delete'] = [me.confid]; + } else { + params[me.confid] = cfg; + } + return params; + }, + + setVMConfig: function(config) { + var me = this; + me.vmconfig = config; + }, + + setIPConfig: function(confid, data) { + var me = this; + + me.confid = confid; + + if (data.ip === 'dhcp') { + data.ipv4mode = data.ip; + data.ip = ''; + } else { + data.ipv4mode = 'static'; + } + if (data.ip6 === 'dhcp' || data.ip6 === 'auto') { + data.ipv6mode = data.ip6; + data.ip6 = ''; + } else { + data.ipv6mode = 'static'; + } + + me.ipconfig = data; + me.setValues(me.ipconfig); + }, + + initComponent : function() { + var me = this; + + me.ipconfig = {}; + + me.column1 = [ + { + xtype: 'displayfield', + fieldLabel: gettext('Network Device'), + value: me.netid + }, + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: gettext('IPv4') + ':' + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv4mode', + inputValue: 'static', + checked: false, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip]').setDisabled(!value); + me.down('field[name=gw]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: gettext('DHCP'), + name: 'ipv4mode', + inputValue: 'dhcp', + checked: false, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip', + vtype: 'IPCIDRAddress', + value: '', + disabled: true, + fieldLabel: gettext('IPv4/CIDR') + }, + { + xtype: 'textfield', + name: 'gw', + value: '', + vtype: 'IPAddress', + disabled: true, + fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') +')' + } + ]; + + me.column2 = [ + { + xtype: 'displayfield' + }, + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: gettext('IPv6') + ':' + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv6mode', + inputValue: 'static', + checked: false, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip6]').setDisabled(!value); + me.down('field[name=gw6]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: gettext('DHCP'), + name: 'ipv6mode', + inputValue: 'dhcp', + checked: false, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip6', + value: '', + vtype: 'IP6CIDRAddress', + disabled: true, + fieldLabel: gettext('IPv6/CIDR') + }, + { + xtype: 'textfield', + name: 'gw6', + vtype: 'IP6Address', + value: '', + disabled: true, + fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')' + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.IPConfigEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + // convert confid from netX to ipconfigX + var match = me.confid.match(/^net(\d+)$/); + if (match) { + me.netid = me.confid; + me.confid = 'ipconfig' + match[1]; + } + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.isCreate = me.confid ? false : true; + + var ipanel = Ext.create('PVE.qemu.IPConfigPanel', { + confid: me.confid, + netid: me.netid, + nodename: nodename + }); + + Ext.applyIf(me, { + subject: gettext('Network Config'), + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + me.vmconfig = response.result.data; + var ipconfig = {}; + var value = me.vmconfig[me.confid]; + if (value) { + ipconfig = PVE.Parser.parseIPConfig(me.confid, value); + if (!ipconfig) { + Ext.Msg.alert(gettext('Error'), gettext('Unable to parse network configuration')); + me.close(); + return; + } + } + ipanel.setIPConfig(me.confid, ipconfig); + ipanel.setVMConfig(me.vmconfig); + } + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.qemu.SystemInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveQemuSystemPanel', + + onlineHelp: 'qm_system_settings', + + viewModel: { + data: { + efi: false, + addefi: true + }, + + formulas: { + efidisk: function(get) { + return get('efi') && get('addefi'); + } + } + }, + + onGetValues: function(values) { + if (values.vga && values.vga.substr(0,6) === 'serial') { + values['serial' + values.vga.substr(6,1)] = 'socket'; + } + + var efidrive = {}; + if (values.hdimage) { + efidrive.file = values.hdimage; + } else if (values.hdstorage) { + efidrive.file = values.hdstorage + ":1"; + } + + if (values.diskformat) { + efidrive.format = values.diskformat; + } + + delete values.hdimage; + delete values.hdstorage; + delete values.diskformat; + + if (efidrive.file) { + values.efidisk0 = PVE.Parser.printQemuDrive(efidrive); + } + + return values; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + scsihwChange: function(field, value) { + var me = this; + if (me.getView().insideWizard) { + me.getViewModel().set('current.scsihw', value); + } + }, + + biosChange: function(field, value) { + var me = this; + if (me.getView().insideWizard) { + me.getViewModel().set('efi', value === 'ovmf'); + } + }, + + control: { + 'pveScsiHwSelector': { + change: 'scsihwChange' + }, + 'pveQemuBiosSelector': { + change: 'biosChange' + } + } + }, + + column1: [ + { + xtype: 'proxmoxKVComboBox', + value: '__default__', + deleteEmpty: false, + fieldLabel: gettext('Graphic card'), + name: 'vga', + comboItems: PVE.Utils.kvm_vga_driver_array() + }, + { + xtype: 'proxmoxcheckbox', + name: 'agent', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Qemu Agent') + } + ], + + column2: [ + { + xtype: 'pveScsiHwSelector', + name: 'scsihw', + value: '__default__', + bind: { + value: '{current.scsihw}' + }, + fieldLabel: gettext('SCSI Controller') + } + ], + + advancedColumn1: [ + { + xtype: 'pveQemuBiosSelector', + name: 'bios', + value: '__default__', + fieldLabel: 'BIOS' + }, + { + xtype: 'proxmoxcheckbox', + bind: { + value: '{addefi}', + hidden: '{!efi}', + disabled: '{!efi}' + }, + hidden: true, + submitValue: false, + disabled: true, + fieldLabel: gettext('Add EFI Disk') + }, + { + xtype: 'pveDiskStorageSelector', + name: 'efidisk0', + storageContent: 'images', + bind: { + nodename: '{nodename}', + hidden: '{!efi}', + disabled: '{!efidisk}' + }, + autoSelect: false, + disabled: true, + hidden: true, + hideSize: true + } + ], + + advancedColumn2: [ + { + xtype: 'proxmoxKVComboBox', + name: 'machine', + value: '__default__', + fieldLabel: gettext('Machine'), + comboItems: [ + ['__default__', PVE.Utils.render_qemu_machine('')], + ['q35', 'q35'] + ] + } + ] + +}); +Ext.define('PVE.lxc.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveLxcSummary', + + scrollable: true, + bodyPadding: 5, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + if (!me.workspace) { + throw "no workspace specified"; + } + + if (!me.statusStore) { + throw "no status storage specified"; + } + + var template = !!me.pveSelNode.data.template; + var rstore = me.statusStore; + + var width = template ? 1 : 0.5; + var items = [ + { + xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView', + responsiveConfig: { + 'width < 1900': { + columnWidth: width + }, + 'width >= 1900': { + columnWidth: width / 2 + } + }, + itemId: 'gueststatus', + pveSelNode: me.pveSelNode, + rstore: rstore + }, + { + xtype: 'pveNotesView', + maxHeight: 320, + itemId: 'notesview', + pveSelNode: me.pveSelNode, + responsiveConfig: { + 'width < 1900': { + columnWidth: width + }, + 'width >= 1900': { + columnWidth: width / 2 + } + } + } + ]; + + var rrdstore; + if (!template) { + + rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/rrddata", + model: 'pve-rrd-guest' + }); + + items.push( + { + xtype: 'proxmoxRRDChart', + title: gettext('CPU usage'), + pveSelNode: me.pveSelNode, + fields: ['cpu'], + fieldTitles: [gettext('CPU usage')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Memory usage'), + pveSelNode: me.pveSelNode, + fields: ['maxmem', 'mem'], + fieldTitles: [gettext('Total'), gettext('RAM usage')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Network traffic'), + pveSelNode: me.pveSelNode, + fields: ['netin','netout'], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Disk IO'), + pveSelNode: me.pveSelNode, + fields: ['diskread','diskwrite'], + store: rrdstore + } + ); + + } + + Ext.apply(me, { + tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ], + items: [ + { + xtype: 'container', + layout: { + type: 'column' + }, + defaults: { + minHeight: 320, + padding: 5, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1 + }, + 'width >= 1900': { + columnWidth: 0.5 + } + } + }, + items: items + } + ] + }); + + me.callParent(); + if (!template) { + rrdstore.startUpdate(); + me.on('destroy', rrdstore.stopUpdate); + } + } +}); +Ext.define('PVE.lxc.NetworkInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcNetworkInputPanel', + + insideWizard: false, + + onlineHelp: 'pct_container_network', + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + + var bridgesel = me.query("[isFormField][name=bridge]")[0]; + bridgesel.setNodename(nodename); + }, + + onGetValues: function(values) { + var me = this; + + var id; + if (me.isCreate) { + id = values.id; + delete values.id; + } else { + id = me.ifname; + } + + if (!id) { + return {}; + } + + var newdata = {}; + + if (values.ipv6mode !== 'static') { + values.ip6 = values.ipv6mode; + } + if (values.ipv4mode !== 'static') { + values.ip = values.ipv4mode; + } + newdata[id] = PVE.Parser.printLxcNetwork(values); + return newdata; + }, + + initComponent : function() { + var me = this; + + var cdata = {}; + + if (me.insideWizard) { + me.ifname = 'net0'; + cdata.name = 'eth0'; + me.dataCache = {}; + } + cdata.firewall = (me.insideWizard || me.isCreate); + + if (!me.dataCache) { + throw "no dataCache specified"; + } + + if (!me.isCreate) { + if (!me.ifname) { + throw "no interface name specified"; + } + if (!me.dataCache[me.ifname]) { + throw "no such interface '" + me.ifname + "'"; + } + + cdata = PVE.Parser.parseLxcNetwork(me.dataCache[me.ifname]); + } + + var i; + for (i = 0; i < 10; i++) { + if (me.isCreate && !me.dataCache['net'+i.toString()]) { + me.ifname = 'net' + i.toString(); + break; + } + } + + var idselector = { + xtype: 'hidden', + name: 'id', + value: me.ifname + }; + + me.column1 = [ + idselector, + { + xtype: 'textfield', + name: 'name', + fieldLabel: gettext('Name'), + emptyText: '(e.g., eth0)', + allowBlank: false, + value: cdata.name, + validator: function(value) { + var result = ''; + Ext.Object.each(me.dataCache, function(key, netstr) { + if (!key.match(/^net\d+/) || key === me.ifname) { + return; // continue + } + var net = PVE.Parser.parseLxcNetwork(netstr); + if (net.name === value) { + result = "interface name already in use"; + return false; + } + }); + if (result !== '') { + return result; + } + // validator can return bool/string + /*jslint confusion:true*/ + return true; + } + }, + { + xtype: 'textfield', + name: 'hwaddr', + fieldLabel: gettext('MAC address'), + vtype: 'MacAddress', + value: cdata.hwaddr, + allowBlank: true, + emptyText: 'auto' + }, + { + xtype: 'PVE.form.BridgeSelector', + name: 'bridge', + nodename: me.nodename, + fieldLabel: gettext('Bridge'), + value: cdata.bridge, + allowBlank: false + }, + { + xtype: 'pveVlanField', + name: 'tag', + value: cdata.tag + }, + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Rate limit') + ' (MB/s)', + minValue: 0, + maxValue: 10*1024, + value: cdata.rate, + emptyText: 'unlimited', + allowBlank: true + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Firewall'), + name: 'firewall', + value: cdata.firewall + } + ]; + + var dhcp4 = (cdata.ip === 'dhcp'); + if (dhcp4) { + cdata.ip = ''; + cdata.gw = ''; + } + + var auto6 = (cdata.ip6 === 'auto'); + var dhcp6 = (cdata.ip6 === 'dhcp'); + if (auto6 || dhcp6) { + cdata.ip6 = ''; + cdata.gw6 = ''; + } + + me.column2 = [ + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: 'IPv4:' // do not localize + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv4mode', + inputValue: 'static', + checked: !dhcp4, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip]').setDisabled(!value); + me.down('field[name=gw]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: 'DHCP', // do not localize + name: 'ipv4mode', + inputValue: 'dhcp', + checked: dhcp4, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip', + vtype: 'IPCIDRAddress', + value: cdata.ip, + disabled: dhcp4, + fieldLabel: 'IPv4/CIDR' // do not localize + }, + { + xtype: 'textfield', + name: 'gw', + value: cdata.gw, + vtype: 'IPAddress', + disabled: dhcp4, + fieldLabel: gettext('Gateway') + ' (IPv4)', + margin: '0 0 3 0' // override bottom margin to account for the menuseparator + }, + { + xtype: 'menuseparator', + height: '3', + margin: '0' + }, + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: 'IPv6:' // do not localize + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv6mode', + inputValue: 'static', + checked: !(auto6 || dhcp6), + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip6]').setDisabled(!value); + me.down('field[name=gw6]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: 'DHCP', // do not localize + name: 'ipv6mode', + inputValue: 'dhcp', + checked: dhcp6, + margin: '0 0 0 10' + }, + { + xtype: 'radiofield', + boxLabel: 'SLAAC', // do not localize + name: 'ipv6mode', + inputValue: 'auto', + checked: auto6, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip6', + value: cdata.ip6, + vtype: 'IP6CIDRAddress', + disabled: (dhcp6 || auto6), + fieldLabel: 'IPv6/CIDR' // do not localize + }, + { + xtype: 'textfield', + name: 'gw6', + vtype: 'IP6Address', + value: cdata.gw6, + disabled: (dhcp6 || auto6), + fieldLabel: gettext('Gateway') + ' (IPv6)' + } + ]; + + me.callParent(); + } +}); + + +Ext.define('PVE.lxc.NetworkEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent : function() { + var me = this; + + if (!me.dataCache) { + throw "no dataCache specified"; + } + + if (!me.nodename) { + throw "no node name specified"; + } + + var ipanel = Ext.create('PVE.lxc.NetworkInputPanel', { + ifname: me.ifname, + nodename: me.nodename, + dataCache: me.dataCache, + isCreate: me.isCreate + }); + + Ext.apply(me, { + subject: gettext('Network Device') + ' (veth)', + digest: me.dataCache.digest, + items: [ ipanel ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.lxc.NetworkView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveLxcNetworkView', + + onlineHelp: 'pct_container_network', + + dataCache: {}, // used to store result of last load + + stateful: true, + stateId: 'grid-lxc-network', + + load: function() { + var me = this; + + Proxmox.Utils.setErrorMask(me, true); + + Proxmox.Utils.API2Request({ + url: me.url, + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + var result = Ext.decode(response.responseText); + var data = result.data || {}; + me.dataCache = data; + var records = []; + Ext.Object.each(data, function(key, value) { + if (!key.match(/^net\d+/)) { + return; // continue + } + var net = PVE.Parser.parseLxcNetwork(value); + net.id = key; + records.push(net); + }); + me.store.loadData(records); + me.down('button[name=addButton]').setDisabled((records.length >= 10)); + } + }); + }, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + me.url = '/nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var store = new Ext.data.Store({ + model: 'pve-lxc-network', + sorters: [ + { + property : 'id', + direction: 'ASC' + } + ] + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return !!caps.vms['VM.Config.Network']; + }, + confirmMsg: function (rec) { + return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.id + "'"); + }, + handler: function(btn, event, rec) { + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + method: 'PUT', + params: { 'delete': rec.data.id, digest: me.dataCache.digest }, + callback: function() { + me.load(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + if (!caps.vms['VM.Config.Network']) { + return false; + } + + var win = Ext.create('PVE.lxc.NetworkEdit', { + url: me.url, + nodename: nodename, + dataCache: me.dataCache, + ifname: rec.data.id + }); + win.on('destroy', me.load, me); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + selModel: sm, + disabled: true, + enableFn: function(rec) { + if (!caps.vms['VM.Config.Network']) { + return false; + } + return true; + }, + handler: run_editor + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + name: 'addButton', + disabled: !caps.vms['VM.Config.Network'], + handler: function() { + var win = Ext.create('PVE.lxc.NetworkEdit', { + url: me.url, + nodename: nodename, + isCreate: true, + dataCache: me.dataCache + }); + win.on('destroy', me.load, me); + win.show(); + } + }, + remove_btn, + edit_btn + ], + columns: [ + { + header: 'ID', + width: 50, + dataIndex: 'id' + }, + { + header: gettext('Name'), + width: 80, + dataIndex: 'name' + }, + { + header: gettext('Bridge'), + width: 80, + dataIndex: 'bridge' + }, + { + header: gettext('Firewall'), + width: 80, + dataIndex: 'firewall', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('VLAN Tag'), + width: 80, + dataIndex: 'tag' + }, + { + header: gettext('MAC address'), + width: 110, + dataIndex: 'hwaddr' + }, + { + header: gettext('IP address'), + width: 150, + dataIndex: 'ip', + renderer: function(value, metaData, rec) { + if (rec.data.ip && rec.data.ip6) { + return rec.data.ip + "
" + rec.data.ip6; + } else if (rec.data.ip6) { + return rec.data.ip6; + } else { + return rec.data.ip; + } + } + }, + { + header: gettext('Gateway'), + width: 150, + dataIndex: 'gw', + renderer: function(value, metaData, rec) { + if (rec.data.gw && rec.data.gw6) { + return rec.data.gw + "
" + rec.data.gw6; + } else if (rec.data.gw6) { + return rec.data.gw6; + } else { + return rec.data.gw; + } + } + } + ], + listeners: { + activate: me.load, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-lxc-network', { + extend: "Ext.data.Model", + proxy: { type: 'memory' }, + fields: [ 'id', 'name', 'hwaddr', 'bridge', + 'ip', 'gw', 'ip6', 'gw6', 'tag', 'firewall' ] + }); + +}); + +/*jslint confusion: true */ +Ext.define('PVE.lxc.RessourceView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveLxcRessourceView'], + + onlineHelp: 'pct_configuration', + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rowdef = me.rows[key] || {}; + + metaData.tdAttr = "valign=middle"; + if (rowdef.tdCls) { + metaData.tdCls = rowdef.tdCls; + } + return rowdef.header || key; + }, + + initComponent : function() { + var me = this; + var i, confid; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + var diskCap = caps.vms['VM.Config.Disk']; + + var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined; + + var rows = { + memory: { + header: gettext('Memory'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined, + defaultValue: 512, + tdCls: 'pve-itype-icon-memory', + group: 1, + renderer: function(value) { + return Proxmox.Utils.format_size(value*1024*1024); + } + }, + swap: { + header: gettext('Swap'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined, + defaultValue: 512, + tdCls: 'pve-itype-icon-swap', + group: 2, + renderer: function(value) { + return Proxmox.Utils.format_size(value*1024*1024); + } + }, + cores: { + header: gettext('Cores'), + editor: caps.vms['VM.Config.CPU'] ? 'PVE.lxc.CPUEdit' : undefined, + defaultValue: '', + tdCls: 'pve-itype-icon-processor', + group: 3, + renderer: function(value) { + var cpulimit = me.getObjectValue('cpulimit'); + var cpuunits = me.getObjectValue('cpuunits'); + var res; + if (value) { + res = value; + } else { + res = gettext('unlimited'); + } + + if (cpulimit) { + res += ' [cpulimit=' + cpulimit + ']'; + } + + if (cpuunits) { + res += ' [cpuunits=' + cpuunits + ']'; + } + return res; + } + }, + rootfs: { + header: gettext('Root Disk'), + defaultValue: Proxmox.Utils.noneText, + editor: mpeditor, + tdCls: 'pve-itype-icon-storage', + group: 4 + }, + cpulimit: { + visible: false + }, + cpuunits: { + visible: false + }, + unprivileged: { + visible: false + } + }; + + PVE.Utils.forEachMP(function(bus, i) { + confid = bus + i; + var group = 5; + var header; + if (bus === 'mp') { + header = gettext('Mount Point') + ' (' + confid + ')'; + } else { + header = gettext('Unused Disk') + ' ' + i; + group += 1; + } + rows[confid] = { + group: group, + order: i, + tdCls: 'pve-itype-icon-storage', + editor: mpeditor, + header: header + }; + }, true); + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + me.selModel = Ext.create('Ext.selection.RowModel', {}); + + var run_resize = function() { + var rec = me.selModel.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.MPResize', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid + }); + + win.show(); + }; + + var run_remove = function(b, e, rec) { + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'delete': rec.data.key + }, + failure: function (response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + var run_move = function(b, e, rec) { + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.HDMove', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + type: 'lxc' + }); + + win.show(); + + win.on('destroy', me.reload, me); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + selModel: me.selModel, + disabled: true, + enableFn: function(rec) { + if (!rec) { + return false; + } + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: function() { me.run_editor(); } + }); + + var resize_btn = new Proxmox.button.Button({ + text: gettext('Resize disk'), + selModel: me.selModel, + disabled: true, + handler: run_resize + }); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + selModel: me.selModel, + disabled: true, + dangerous: true, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + me.renderKey(rec.data.key, {}, rec) + "'"); + if (rec.data.key.match(/^unused\d+$/)) { + msg += " " + gettext('This will permanently erase all data.'); + } + + return msg; + }, + handler: run_remove + }); + + var move_btn = new Proxmox.button.Button({ + text: gettext('Move Volume'), + selModel: me.selModel, + disabled: true, + dangerous: true, + handler: run_move + }); + + var set_button_status = function() { + var rec = me.selModel.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + remove_btn.disable(); + resize_btn.disable(); + return; + } + var key = rec.data.key; + var value = rec.data.value; + var rowdef = rows[key]; + + var isDisk = (rowdef.tdCls == 'pve-itype-icon-storage'); + + var noedit = rec.data['delete'] || !rowdef.editor; + if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) { + var mp = PVE.Parser.parseLxcMountPoint(value); + if (mp.type !== 'volume') { + noedit = true; + } + } + edit_btn.setDisabled(noedit); + + remove_btn.setDisabled(!isDisk || rec.data.key === 'rootfs' || !diskCap); + resize_btn.setDisabled(!isDisk || !diskCap); + move_btn.setDisabled(!isDisk || !diskCap); + + }; + + var sorterFn = function(rec1, rec2) { + var v1 = rec1.data.key; + var v2 = rec2.data.key; + var g1 = rows[v1].group || 0; + var g2 = rows[v2].group || 0; + var order1 = rows[v1].order || 0; + var order2 = rows[v2].order || 0; + + if ((g1 - g2) !== 0) { + return g1 - g2; + } + + if ((order1 - order2) !== 0) { + return order1 - order2; + } + + if (v1 > v2) { + return 1; + } else if (v1 < v2) { + return -1; + } else { + return 0; + } + }; + + Ext.apply(me, { + url: '/api2/json/' + baseurl, + selModel: me.selModel, + interval: 2000, + cwidth1: 170, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Mount Point'), + iconCls: 'pve-itype-icon-storage', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + var win = Ext.create('PVE.lxc.MountPointEdit', { + url: '/api2/extjs/' + baseurl, + unprivileged: me.getObjectValue('unprivileged'), + pveSelNode: me.pveSelNode + }); + win.show(); + } + } + ] + }) + }, + edit_btn, + remove_btn, + resize_btn, + move_btn + ], + rows: rows, + sorterFn: sorterFn, + editorConfig: { + pveSelNode: me.pveSelNode, + url: '/api2/extjs/' + baseurl + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.lxc.FeaturesInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveLxcFeaturesInputPanel', + + // used to save the mounts fstypes until sending + mounts: [], + + fstypes: ['nfs', 'cifs'], + + viewModel: { + parent: null, + data: { + unprivileged: false + }, + formulas: { + privilegedOnly: function(get) { + return (get('unprivileged') ? gettext('privileged only') : ''); + }, + unprivilegedOnly: function(get) { + return (!get('unprivileged') ? gettext('unprivileged only') : ''); + } + } + }, + + items: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('keyctl'), + name: 'keyctl', + bind: { + disabled: '{!unprivileged}', + boxLabel: '{unprivilegedOnly}' + } + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Nesting'), + name: 'nesting' + }, + { + xtype: 'proxmoxcheckbox', + name: 'nfs', + fieldLabel: 'NFS', + bind: { + disabled: '{unprivileged}', + boxLabel: '{privilegedOnly}' + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'cifs', + fieldLabel: 'CIFS', + bind: { + disabled: '{unprivileged}', + boxLabel: '{privilegedOnly}' + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'fuse', + fieldLabel: 'FUSE' + } + ], + + onGetValues: function(values) { + var me = this; + var mounts = me.mounts; + me.fstypes.forEach(function(fs) { + if (values[fs]) { + mounts.push(fs); + } + delete values[fs]; + }); + + if (mounts.length) { + values.mount = mounts.join(';'); + } + + var featuresstring = PVE.Parser.printPropertyString(values, undefined); + if (featuresstring == '') { + return { 'delete': 'features' }; + } + return { features: featuresstring }; + }, + + setValues: function(values) { + var me = this; + + me.viewModel.set({ unprivileged: values.unprivileged }); + + if (values.features) { + var res = PVE.Parser.parsePropertyString(values.features); + me.mounts = []; + if (res.mount) { + res.mount.split(/[; ]/).forEach(function(item) { + if (me.fstypes.indexOf(item) === -1) { + me.mounts.push(item); + } else { + res[item] = 1; + } + }); + } + this.callParent([res]); + } + } +}); + +Ext.define('PVE.lxc.FeaturesEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveLxcFeaturesEdit', + + subject: gettext('Features'), + + items: [{ + xtype: 'pveLxcFeaturesInputPanel' + }], + + initComponent : function() { + var me = this; + + me.callParent(); + + me.load(); + } +}); +/*jslint confusion: true */ +Ext.define('PVE.lxc.Options', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveLxcOptions'], + + onlineHelp: 'pct_options', + + initComponent : function() { + var me = this; + var i; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + var rows = { + onboot: { + header: gettext('Start at boot'), + defaultValue: '', + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Start at boot'), + items: { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + fieldLabel: gettext('Start at boot') + } + } : undefined + }, + startup: { + header: gettext('Start/Shutdown order'), + defaultValue: '', + renderer: PVE.Utils.render_kvm_startup, + editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ? + { + xtype: 'pveWindowStartupEdit', + onlineHelp: 'pct_startup_and_shutdown' + } : undefined + }, + ostype: { + header: gettext('OS Type'), + defaultValue: Proxmox.Utils.unknownText + }, + arch: { + header: gettext('Architecture'), + defaultValue: Proxmox.Utils.unknownText + }, + console: { + header: '/dev/console', + defaultValue: 1, + renderer: Proxmox.Utils.format_enabled_toggle, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: '/dev/console', + items: { + xtype: 'proxmoxcheckbox', + name: 'console', + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + checked: true, + fieldLabel: '/dev/console' + } + } : undefined + }, + tty: { + header: gettext('TTY count'), + defaultValue: 2, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('TTY count'), + items: { + xtype: 'proxmoxintegerfield', + name: 'tty', + minValue: 0, + maxValue: 6, + value: 2, + fieldLabel: gettext('TTY count'), + emptyText: gettext('Default'), + deleteEmpty: true + } + } : undefined + }, + cmode: { + header: gettext('Console mode'), + defaultValue: 'tty', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Console mode'), + items: { + xtype: 'proxmoxKVComboBox', + name: 'cmode', + deleteEmpty: true, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + " (tty)"], + ['tty', "/dev/tty[X]"], + ['console', "/dev/console"], + ['shell', "shell"] + ], + fieldLabel: gettext('Console mode') + } + } : undefined + }, + protection: { + header: gettext('Protection'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Protection'), + items: { + xtype: 'proxmoxcheckbox', + name: 'protection', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + unprivileged: { + header: gettext('Unprivileged container'), + renderer: Proxmox.Utils.format_boolean, + defaultValue: 0 + }, + features: { + header: gettext('Features'), + defaultValue: Proxmox.Utils.noneText, + editor: Proxmox.UserName === 'root@pam' ? + 'PVE.lxc.FeaturesEdit' : undefined + }, + hookscript: { + header: gettext('Hookscript') + } + }; + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: function() { me.run_editor(); } + }); + + Ext.apply(me, { + url: "/api2/json/" + baseurl, + selModel: sm, + interval: 5000, + tbar: [ edit_btn ], + rows: rows, + editorConfig: { + url: '/api2/extjs/' + baseurl + }, + listeners: { + itemdblclick: me.run_editor + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + } +}); + +Ext.define('PVE.lxc.DNSInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcDNSInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + var deletes = []; + if (!values.searchdomain && !me.insideWizard) { + deletes.push('searchdomain'); + } + + if (values.nameserver) { + var list = values.nameserver.split(/[\ \,\;]+/); + values.nameserver = list.join(' '); + } else if(!me.insideWizard) { + deletes.push('nameserver'); + } + + if (deletes.length) { + values['delete'] = deletes.join(','); + } + + return values; + }, + + initComponent : function() { + var me = this; + + var items = [ + { + xtype: 'proxmoxtextfield', + name: 'searchdomain', + skipEmptyText: true, + fieldLabel: gettext('DNS domain'), + emptyText: gettext('use host settings'), + allowBlank: true + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('DNS servers'), + vtype: 'IP64AddressList', + allowBlank: true, + emptyText: gettext('use host settings'), + name: 'nameserver', + itemId: 'nameserver' + } + ]; + + if (me.insideWizard) { + me.column1 = items; + } else { + me.items = items; + } + + me.callParent(); + } +}); + +Ext.define('PVE.lxc.DNSEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + var ipanel = Ext.create('PVE.lxc.DNSInputPanel'); + + Ext.apply(me, { + subject: gettext('Resources'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + if (values.nameserver) { + values.nameserver.replace(/[,;]/, ' '); + values.nameserver.replace(/^\s+/, ''); + } + + ipanel.setValues(values); + } + }); + } + } +}); + +/*jslint confusion: true */ +Ext.define('PVE.lxc.DNS', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveLxcDNS'], + + onlineHelp: 'pct_container_network', + + initComponent : function() { + var me = this; + var i; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + var rows = { + hostname: { + required: true, + defaultValue: me.pveSelNode.data.name, + header: gettext('Hostname'), + editor: caps.vms['VM.Config.Network'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Hostname'), + items: { + xtype: 'inputpanel', + items:{ + fieldLabel: gettext('Hostname'), + xtype: 'textfield', + name: 'hostname', + vtype: 'DnsName', + allowBlank: true, + emptyText: 'CT' + vmid.toString() + }, + onGetValues: function(values) { + var params = values; + if (values.hostname === undefined || + values.hostname === null || + values.hostname === '') { + params = { hostname: 'CT'+vmid.toString()}; + } + return params; + } + } + } : undefined + }, + searchdomain: { + header: gettext('DNS domain'), + defaultValue: '', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + renderer: function(value) { + return value || gettext('use host settings'); + } + }, + nameserver: { + header: gettext('DNS server'), + defaultValue: '', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + renderer: function(value) { + return value || gettext('use host settings'); + } + } + }; + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var reload = function() { + me.rstore.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rowdef = rows[rec.data.key]; + if (!rowdef.editor) { + return; + } + + var win; + if (Ext.isString(rowdef.editor)) { + win = Ext.create(rowdef.editor, { + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + baseurl + }); + } else { + var config = Ext.apply({ + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + baseurl + }, rowdef.editor); + win = Ext.createWidget(rowdef.editor.xtype, config); + win.load(); + } + //win.load(); + win.show(); + win.on('destroy', reload); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: run_editor + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + var rowdef = rows[rec.data.key]; + edit_btn.setDisabled(!rowdef.editor); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/config", + selModel: sm, + cwidth1: 150, + run_editor: run_editor, + tbar: [ edit_btn ], + rows: rows, + listeners: { + itemdblclick: run_editor, + selectionchange: set_button_status, + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.lxc.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.lxc.Config', + + onlineHelp: 'chapter_pct', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var template = !!me.pveSelNode.data.template; + + var running = !!me.pveSelNode.data.uptime; + + var caps = Ext.state.Manager.get('GuiCap'); + + var base_url = '/nodes/' + nodename + '/lxc/' + vmid; + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json' + base_url + '/status/current', + interval: 1000 + }); + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: base_url + "/status/" + cmd, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + var startBtn = Ext.create('Ext.Button', { + text: gettext('Start'), + disabled: !caps.vms['VM.PowerMgmt'] || running, + hidden: template, + handler: function() { + vm_command('start'); + }, + iconCls: 'fa fa-play' + }); + + var stopBtn = Ext.create('Ext.menu.Item',{ + text: gettext('Stop'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('vzstop', vmid), + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'), + dangerous: true, + handler: function() { + vm_command("stop"); + }, + iconCls: 'fa fa-stop' + }); + + var shutdownBtn = Ext.create('PVE.button.Split', { + text: gettext('Shutdown'), + disabled: !caps.vms['VM.PowerMgmt'] || !running, + hidden: template, + confirmMsg: Proxmox.Utils.format_task_description('vzshutdown', vmid), + handler: function() { + vm_command('shutdown'); + }, + menu: { + items:[stopBtn] + }, + iconCls: 'fa fa-power-off' + }); + + var migrateBtn = Ext.create('Ext.Button', { + text: gettext('Migrate'), + disabled: !caps.vms['VM.Migrate'], + hidden: PVE.data.ResourceStore.getNodes().length < 2, + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'lxc', + nodename: nodename, + vmid: vmid + }); + win.show(); + }, + iconCls: 'fa fa-send-o' + }); + + var moreBtn = Ext.create('Proxmox.button.Button', { + text: gettext('More'), + menu: { items: [ + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: caps.vms['VM.Clone'] ? false : true, + handler: function() { + PVE.window.Clone.wrap(nodename, vmid, template, 'lxc'); + } + }, + { + text: gettext('Convert to template'), + disabled: template, + xtype: 'pveMenuItem', + iconCls: 'fa fa-fw fa-file-o', + hidden: caps.vms['VM.Allocate'] ? false : true, + confirmMsg: Proxmox.Utils.format_task_description('vztemplate', vmid), + handler: function() { + Proxmox.Utils.API2Request({ + url: base_url + '/template', + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + } + }, + { + iconCls: 'fa fa-heartbeat ', + hidden: !caps.nodes['Sys.Console'], + text: gettext('Manage HA'), + handler: function() { + var ha = me.pveSelNode.data.hastate; + Ext.create('PVE.ha.VMResourceEdit', { + vmid: vmid, + guestType: 'ct', + isCreate: (!ha || ha === 'unmanaged') + }).show(); + } + }, + { + text: gettext('Remove'), + disabled: !caps.vms['VM.Allocate'], + itemId: 'removeBtn', + handler: function() { + Ext.create('PVE.window.SafeDestroy', { + url: base_url, + item: { type: 'CT', id: vmid } + }).show(); + }, + iconCls: 'fa fa-trash-o' + } + ]} + }); + + var vm = me.pveSelNode.data; + + var consoleBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.vms['VM.Console'], + consoleType: 'lxc', + consoleName: vm.name, + hidden: template, + nodename: nodename, + vmid: vmid + }); + + var statusTxt = Ext.create('Ext.toolbar.TextItem', { + data: { + lock: undefined + }, + tpl: [ + '', + ' ({lock})', + '' + ] + }); + + + Ext.apply(me, { + title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm.text, nodename), + hstateid: 'lxctab', + tbarSpacing: false, + tbar: [ statusTxt, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn ], + defaults: { statusStore: me.statusStore }, + items: [ + { + title: gettext('Summary'), + xtype: 'pveLxcSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + } + ] + }); + + if (caps.vms['VM.Console'] && !template) { + me.items.push( + { + title: gettext('Console'), + itemId: 'consolejs', + iconCls: 'fa fa-terminal', + xtype: 'pveNoVncConsole', + vmid: vmid, + consoleType: 'lxc', + xtermjs: true, + nodename: nodename + } + ); + } + + me.items.push( + { + title: gettext('Resources'), + itemId: 'resources', + expandedOnInit: true, + iconCls: 'fa fa-cube', + xtype: 'pveLxcRessourceView' + }, + { + title: gettext('Network'), + iconCls: 'fa fa-exchange', + itemId: 'network', + xtype: 'pveLxcNetworkView' + }, + { + title: gettext('DNS'), + iconCls: 'fa fa-globe', + itemId: 'dns', + xtype: 'pveLxcDNS' + }, + { + title: gettext('Options'), + itemId: 'options', + iconCls: 'fa fa-gear', + xtype: 'pveLxcOptions' + }, + { + title: gettext('Task History'), + itemId: 'tasks', + iconCls: 'fa fa-list', + xtype: 'proxmoxNodeTasks', + nodename: nodename, + vmidFilter: vmid + } + ); + + if (caps.vms['VM.Backup']) { + me.items.push({ + title: gettext('Backup'), + iconCls: 'fa fa-floppy-o', + xtype: 'pveBackupView', + itemId: 'backup' + }, + { + title: gettext('Replication'), + iconCls: 'fa fa-retweet', + xtype: 'pveReplicaView', + itemId: 'replication' + }); + } + + if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback']) && !template) { + me.items.push({ + title: gettext('Snapshots'), + iconCls: 'fa fa-history', + xtype: 'pveLxcSnapshotTree', + itemId: 'snapshot' + }); + } + + if (caps.vms['VM.Console']) { + me.items.push( + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + iconCls: 'fa fa-shield', + allow_iface: true, + base_url: base_url + '/firewall/rules', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + groups: ['firewall'], + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_vm_container_configuration', + title: gettext('Options'), + base_url: base_url + '/firewall/options', + fwtype: 'vm', + itemId: 'firewall-options' + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: base_url + '/firewall/aliases', + itemId: 'firewall-aliases' + }, + { + xtype: 'pveIPSet', + title: gettext('IPSet'), + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: base_url + '/firewall/ipset', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall-ipset' + }, + { + title: gettext('Log'), + groups: ['firewall'], + iconCls: 'fa fa-list', + onlineHelp: 'chapter_pve_firewall', + itemId: 'firewall-fwlog', + xtype: 'proxmoxLogView', + url: '/api2/extjs' + base_url + '/firewall/log' + } + ); + } + + if (caps.vms['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + itemId: 'permissions', + iconCls: 'fa fa-unlock', + path: '/vms/' + vmid + }); + } + + me.callParent(); + + me.mon(me.statusStore, 'load', function(s, records, success) { + var status; + var lock; + if (!success) { + status = 'unknown'; + } else { + var rec = s.data.get('status'); + status = rec ? rec.data.value : 'unknown'; + rec = s.data.get('template'); + template = rec.data.value || false; + rec = s.data.get('lock'); + lock = rec ? rec.data.value : undefined; + } + + statusTxt.update({ lock: lock }); + + startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template); + shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running'); + stopBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'stopped'); + me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped'); + consoleBtn.setDisabled(template); + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.lxc.CreateWizard', { + extend: 'PVE.window.Wizard', + mixins: ['Proxmox.Mixin.CBind'], + + viewModel: { + data: { + nodename: '', + storage: '', + unprivileged: true + } + }, + + cbindData: { + nodename: undefined + }, + + subject: gettext('LXC Container'), + + items: [ + { + xtype: 'inputpanel', + title: gettext('General'), + onlineHelp: 'pct_general', + column1: [ + { + xtype: 'pveNodeSelector', + name: 'nodename', + cbind: { + selectCurNode: '{!nodename}', + preferredValue: '{nodename}' + }, + bind: { + value: '{nodename}' + }, + fieldLabel: gettext('Node'), + allowBlank: false, + onlineValidator: true + }, + { + xtype: 'pveGuestIDSelector', + name: 'vmid', // backend only knows vmid + guestType: 'lxc', + value: '', + loadNextFreeID: true, + validateExists: false + }, + { + xtype: 'proxmoxtextfield', + name: 'hostname', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Hostname'), + skipEmptyText: true, + allowBlank: true + }, + { + xtype: 'proxmoxcheckbox', + name: 'unprivileged', + value: true, + bind: { + value: '{unprivileged}' + }, + fieldLabel: gettext('Unprivileged container') + } + ], + column2: [ + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true + }, + { + xtype: 'textfield', + inputType: 'password', + name: 'password', + value: '', + fieldLabel: gettext('Password'), + allowBlank: false, + minLength: 5, + change: function(f, value) { + if (f.rendered) { + f.up().down('field[name=confirmpw]').validate(); + } + } + }, + { + xtype: 'textfield', + inputType: 'password', + name: 'confirmpw', + value: '', + fieldLabel: gettext('Confirm password'), + allowBlank: true, + submitValue: false, + validator: function(value) { + var pw = this.up().down('field[name=password]').getValue(); + if (pw !== value) { + return "Passwords do not match!"; + } + return true; + } + }, + { + xtype: 'proxmoxtextfield', + name: 'ssh-public-keys', + value: '', + fieldLabel: gettext('SSH public key'), + allowBlank: true, + validator: function(value) { + var pwfield = this.up().down('field[name=password]'); + if (value.length) { + var key = PVE.Parser.parseSSHKey(value); + if (!key) { + return "Failed to recognize ssh key"; + } + pwfield.allowBlank = true; + } else { + pwfield.allowBlank = false; + } + pwfield.validate(); + return true; + }, + afterRender: function() { + if (!window.FileReader) { + // No FileReader support in this browser + return; + } + var cancel = function(ev) { + ev = ev.event; + if (ev.preventDefault) { + ev.preventDefault(); + } + }; + var field = this; + field.inputEl.on('dragover', cancel); + field.inputEl.on('dragenter', cancel); + field.inputEl.on('drop', function(ev) { + ev = ev.event; + if (ev.preventDefault) { + ev.preventDefault(); + } + var files = ev.dataTransfer.files; + PVE.Utils.loadSSHKeyFromFile(files[0], function(v) { + field.setValue(v); + }); + }); + } + }, + { + xtype: 'filebutton', + name: 'file', + hidden: !window.FileReader, + text: gettext('Load SSH Key File'), + listeners: { + change: function(btn, e, value) { + e = e.event; + var field = this.up().down('proxmoxtextfield[name=ssh-public-keys]'); + PVE.Utils.loadSSHKeyFromFile(e.target.files[0], function(v) { + field.setValue(v); + }); + btn.reset(); + } + } + } + ] + }, + { + xtype: 'inputpanel', + title: gettext('Template'), + onlineHelp: 'pct_container_images', + column1: [ + { + xtype: 'pveStorageSelector', + name: 'tmplstorage', + fieldLabel: gettext('Storage'), + storageContent: 'vztmpl', + autoSelect: true, + allowBlank: false, + bind: { + value: '{storage}', + nodename: '{nodename}' + } + }, + { + xtype: 'pveFileSelector', + name: 'ostemplate', + storageContent: 'vztmpl', + fieldLabel: gettext('Template'), + bind: { + storage: '{storage}', + nodename: '{nodename}' + }, + allowBlank: false + } + ] + }, + { + xtype: 'pveLxcMountPointInputPanel', + title: gettext('Root Disk'), + insideWizard: true, + isCreate: true, + unused: false, + bind: { + nodename: '{nodename}', + unprivileged: '{unprivileged}' + }, + confid: 'rootfs' + }, + { + xtype: 'pveLxcCPUInputPanel', + title: gettext('CPU'), + insideWizard: true + }, + { + xtype: 'pveLxcMemoryInputPanel', + title: gettext('Memory'), + insideWizard: true + }, + { + xtype: 'pveLxcNetworkInputPanel', + title: gettext('Network'), + insideWizard: true, + bind: { + nodename: '{nodename}' + }, + isCreate: true + }, + { + xtype: 'pveLxcDNSInputPanel', + title: gettext('DNS'), + insideWizard: true + }, + { + title: gettext('Confirm'), + layout: 'fit', + items: [ + { + xtype: 'grid', + store: { + model: 'KeyValue', + sorters: [{ + property : 'key', + direction: 'ASC' + }] + }, + columns: [ + {header: 'Key', width: 150, dataIndex: 'key'}, + {header: 'Value', flex: 1, dataIndex: 'value'} + ] + } + ], + dockedItems: [ + { + xtype: 'proxmoxcheckbox', + name: 'start', + dock: 'bottom', + margin: '5 0 0 0', + boxLabel: gettext('Start after created') + } + ], + listeners: { + show: function(panel) { + var wizard = this.up('window'); + var kv = wizard.getValues(); + var data = []; + Ext.Object.each(kv, function(key, value) { + if (key === 'delete' || key === 'tmplstorage') { // ignore + return; + } + if (key === 'password') { // don't show pw + return; + } + var html = Ext.htmlEncode(Ext.JSON.encode(value)); + data.push({ key: key, value: value }); + }); + + var summarystore = panel.down('grid').getStore(); + summarystore.suspendEvents(); + summarystore.removeAll(); + summarystore.add(data); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh'); + } + }, + onSubmit: function() { + var wizard = this.up('window'); + var kv = wizard.getValues(); + delete kv['delete']; + + var nodename = kv.nodename; + delete kv.nodename; + delete kv.tmplstorage; + + if (!kv.pool.length) { + delete kv.pool; + } + + if (!kv.password.length && kv['ssh-public-keys']) { + delete kv.password; + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/lxc', + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function(response, opts){ + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + wizard.close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + } + ] +}); + + + +Ext.define('PVE.lxc.SnapshotTree', { + extend: 'Ext.tree.Panel', + alias: ['widget.pveLxcSnapshotTree'], + + onlineHelp: 'pct_snapshots', + + load_delay: 3000, + + old_digest: 'invalid', + + stateful: true, + stateId: 'grid-lxc-snapshots', + + sorterFn: function(rec1, rec2) { + var v1 = rec1.data.snaptime; + var v2 = rec2.data.snaptime; + + if (rec1.data.name === 'current') { + return 1; + } + if (rec2.data.name === 'current') { + return -1; + } + + return (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)); + }, + + reload: function(repeat) { + var me = this; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot', + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + me.load_task.delay(me.load_delay); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + var digest = 'invalid'; + var idhash = {}; + var root = { name: '__root', expanded: true, children: [] }; + Ext.Array.each(response.result.data, function(item) { + item.leaf = true; + item.children = []; + if (item.name === 'current') { + digest = item.digest + item.running; + if (item.running) { + item.iconCls = 'fa fa-fw fa-desktop x-fa-tree-running'; + } else { + item.iconCls = 'fa fa-fw fa-desktop x-fa-tree'; + } + } else { + item.iconCls = 'fa fa-fw fa-history x-fa-tree'; + } + idhash[item.name] = item; + }); + + if (digest !== me.old_digest) { + me.old_digest = digest; + + Ext.Array.each(response.result.data, function(item) { + if (item.parent && idhash[item.parent]) { + var parent_item = idhash[item.parent]; + parent_item.children.push(item); + parent_item.leaf = false; + parent_item.expanded = true; + parent_item.expandable = false; + } else { + root.children.push(item); + } + }); + + me.setRootNode(root); + } + + me.load_task.delay(me.load_delay); + } + }); + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/feature', + params: { feature: 'snapshot' }, + method: 'GET', + success: function(response, options) { + var res = response.result.data; + if (res.hasFeature) { + var snpBtns = Ext.ComponentQuery.query('#snapshotBtn'); + snpBtns.forEach(function(item){ + item.enable(); + }); + } + } + }); + + + }, + + listeners: { + beforestatesave: function(grid, state, eopts) { + // extjs cannot serialize functions, + // so a the sorter with only the sorterFn will + // not be a valid sorter when restoring the state + delete state.storeState.sorters; + } + }, + + initComponent: function() { + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + me.vmid = me.pveSelNode.data.vmid; + if (!me.vmid) { + throw "no VM ID specified"; + } + + me.load_task = new Ext.util.DelayedTask(me.reload, me); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var valid_snapshot = function(record) { + return record && record.data && record.data.name && + record.data.name !== 'current'; + }; + + var valid_snapshot_rollback = function(record) { + return record && record.data && record.data.name && + record.data.name !== 'current' && !record.data.snapstate; + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (valid_snapshot(rec)) { + var win = Ext.create('PVE.window.LxcSnapshot', { + snapname: rec.data.name, + nodename: me.nodename, + vmid: me.vmid + }); + win.show(); + me.mon(win, 'close', me.reload, me); + } + }; + + var editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: valid_snapshot, + handler: run_editor + }); + + var rollbackBtn = new Proxmox.button.Button({ + text: gettext('Rollback'), + disabled: true, + dangerous: true, + selModel: sm, + enableFn: valid_snapshot_rollback, + confirmMsg: function(rec) { + var taskdescription = Proxmox.Utils.format_task_description('vzrollback', me.vmid); + var snaptime = Ext.Date.format(rec.data.snaptime,'Y-m-d H:i:s'); + var snapname = rec.data.name; + + var msg = Ext.String.format(gettext('{0} to {1} ({2})'), + taskdescription, snapname, snaptime); + msg += '

' + gettext('Note: Rollback stops CT') + '

'; + + return msg; + }, + handler: function(btn, event) { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var snapname = rec.data.name; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot/' + snapname + '/rollback', + method: 'POST', + waitMsgTarget: me, + callback: function() { + me.reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + } + }); + } + }); + + var removeBtn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.name + "'"); + return msg; + }, + enableFn: valid_snapshot, + handler: function(btn, event) { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var snapname = rec.data.name; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot/' + snapname, + method: 'DELETE', + waitMsgTarget: me, + callback: function() { + me.reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + } + }); + } + }); + + var snapshotBtn = Ext.create('Ext.Button', { + itemId: 'snapshotBtn', + text: gettext('Take Snapshot'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.window.LxcSnapshot', { + nodename: me.nodename, + vmid: me.vmid + }); + win.show(); + } + }); + + Ext.apply(me, { + layout: 'fit', + rootVisible: false, + animate: false, + sortableColumns: false, + selModel: sm, + tbar: [ snapshotBtn, rollbackBtn, removeBtn, editBtn ], + fields: [ + 'name', 'description', 'snapstate', 'vmstate', 'running', + { name: 'snaptime', type: 'date', dateFormat: 'timestamp' } + ], + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'name', + width: 200, + renderer: function(value, metaData, record) { + if (value === 'current') { + return "NOW"; + } else { + return value; + } + } + }, +// { +// text: gettext('RAM'), +// align: 'center', +// resizable: false, +// dataIndex: 'vmstate', +// width: 50, +// renderer: function(value, metaData, record) { +// if (record.data.name !== 'current') { +// return Proxmox.Utils.format_boolean(value); +// } +// } +// }, + { + text: gettext('Date') + "/" + gettext("Status"), + dataIndex: 'snaptime', + resizable: false, + width: 150, + renderer: function(value, metaData, record) { + if (record.data.snapstate) { + return record.data.snapstate; + } + if (value) { + return Ext.Date.format(value,'Y-m-d H:i:s'); + } + } + }, + { + text: gettext('Description'), + dataIndex: 'description', + flex: 1, + renderer: function(value, metaData, record) { + if (record.data.name === 'current') { + return gettext("You are here!"); + } else { + return Ext.String.htmlEncode(value); + } + } + } + ], + columnLines: true, + listeners: { + activate: me.reload, + destroy: me.load_task.cancel, + itemdblclick: run_editor + } + }); + + me.callParent(); + + me.store.sorters.add(new Ext.util.Sorter({ + sorterFn: me.sorterFn + })); + } +}); +Ext.define('PVE.window.LxcSnapshot', { + extend: 'Ext.window.Window', + + resizable: false, + + // needed for finding the reference to submitbutton + // because we do not have a controller + referenceHolder: true, + defaultButton: 'submitbutton', + defaultFocus: 'field', + + take_snapshot: function(snapname, descr, vmstate) { + var me = this; + var params = { snapname: snapname }; + if (descr) { + params.description = descr; + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot", + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + me.close(); + } + }); + }, + + update_snapshot: function(snapname, descr) { + var me = this; + Proxmox.Utils.API2Request({ + params: { description: descr }, + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot/" + + snapname + '/config', + waitMsgTarget: me, + method: 'PUT', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + me.close(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + var summarystore = Ext.create('Ext.data.Store', { + model: 'KeyValue', + sorters: [ + { + property : 'key', + direction: 'ASC' + } + ] + }); + + var items = [ + { + xtype: me.snapname ? 'displayfield' : 'textfield', + name: 'snapname', + value: me.snapname, + fieldLabel: gettext('Name'), + vtype: 'ConfigId', + allowBlank: false + } + ]; + + if (me.snapname) { + items.push({ + xtype: 'displayfield', + name: 'snaptime', + renderer: PVE.Utils.render_timestamp_human_readable, + fieldLabel: gettext('Timestamp') + }); + } + + items.push({ + xtype: 'textareafield', + grow: true, + name: 'description', + fieldLabel: gettext('Description') + }); + + if (me.snapname) { + items.push({ + title: gettext('Settings'), + xtype: 'grid', + height: 200, + store: summarystore, + columns: [ + {header: gettext('Key'), width: 150, dataIndex: 'key'}, + {header: gettext('Value'), flex: 1, dataIndex: 'value'} + ] + }); + } + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + if (me.snapname) { + me.title = gettext('Edit') + ': ' + gettext('Snapshot'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Update'), + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.update_snapshot(me.snapname, values.description); + } + } + }); + } else { + me.title ="VM " + me.vmid + ': ' + gettext('Take Snapshot'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Take Snapshot'), + reference: 'submitbutton', + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.take_snapshot(values.snapname, values.description); + } + } + }); + } + + Ext.apply(me, { + modal: true, + width: 450, + border: false, + layout: 'fit', + buttons: [ submitBtn ], + items: [ me.formPanel ] + }); + + if (me.snapname) { + Ext.apply(me, { + width: 620, + height: 420 + }); + } + + me.callParent(); + + if (!me.snapname) { + return; + } + + // else load data + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot/" + + me.snapname + '/config', + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.close(); + }, + success: function(response, options) { + var data = response.result.data; + var kvarray = []; + Ext.Object.each(data, function(key, value) { + if (key === 'description' || key === 'snaptime') { + return; + } + kvarray.push({ key: key, value: value }); + }); + + summarystore.suspendEvents(); + summarystore.add(kvarray); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh', summarystore); + + form.findField('snaptime').setValue(data.snaptime); + form.findField('description').setValue(data.description); + } + }); + } +}); +/*jslint confusion: true */ +var labelWidth = 120; + +Ext.define('PVE.lxc.MemoryEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.apply(me, { + subject: gettext('Memory'), + items: Ext.create('PVE.lxc.MemoryInputPanel') + }); + + me.callParent(); + + me.load(); + } +}); + + +Ext.define('PVE.lxc.CPUEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.apply(me, { + subject: gettext('CPU'), + items: Ext.create('PVE.lxc.CPUInputPanel') + }); + + me.callParent(); + + me.load(); + } +}); + +Ext.define('PVE.lxc.CPUInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcCPUInputPanel', + + onlineHelp: 'pct_cpu', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + PVE.Utils.delete_if_default(values, 'cores', '', me.insideWizard); + // cpu{limit,unit} aren't in the wizard so create is always false + PVE.Utils.delete_if_default(values, 'cpulimit', '0', 0); + PVE.Utils.delete_if_default(values, 'cpuunits', '1024', 0); + + return values; + }, + + advancedColumn1: [ + { + xtype: 'numberfield', + name: 'cpulimit', + minValue: 0, + value: '', + step: 1, + fieldLabel: gettext('CPU limit'), + allowBlank: true, + emptyText: gettext('unlimited') + } + ], + + advancedColumn2: [ + { + xtype: 'proxmoxintegerfield', + name: 'cpuunits', + fieldLabel: gettext('CPU units'), + value: 1024, + minValue: 8, + maxValue: 500000, + labelWidth: labelWidth, + allowBlank: false + } + ], + + initComponent: function() { + var me = this; + + me.column1 = [ + { + xtype: 'proxmoxintegerfield', + name: 'cores', + minValue: 1, + maxValue: 128, + value: me.insideWizard ? 1 : '', + fieldLabel: gettext('Cores'), + allowBlank: true, + deleteEmpty: true, + emptyText: gettext('unlimited') + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.lxc.MemoryInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcMemoryInputPanel', + + onlineHelp: 'pct_memory', + + insideWizard: false, + + initComponent : function() { + var me = this; + + var items = [ + { + xtype: 'proxmoxintegerfield', + name: 'memory', + minValue: 16, + value: '512', + step: 32, + fieldLabel: gettext('Memory') + ' (MiB)', + labelWidth: labelWidth, + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + name: 'swap', + minValue: 0, + value: '512', + step: 32, + fieldLabel: gettext('Swap') + ' (MiB)', + labelWidth: labelWidth, + allowBlank: false + } + ]; + + if (me.insideWizard) { + me.column1 = items; + } else { + me.items = items; + } + + me.callParent(); + } +}); +Ext.define('PVE.window.MPResize', { + extend: 'Ext.window.Window', + + resizable: false, + + resize_disk: function(disk, size) { + var me = this; + var params = { disk: disk, size: '+' + size + 'G' }; + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/resize', + waitMsgTarget: me, + method: 'PUT', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskViewer', { upid: upid }); + win.show(); + me.close(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + var items = [ + { + xtype: 'displayfield', + name: 'disk', + value: me.disk, + fieldLabel: gettext('Disk'), + vtype: 'StorageId', + allowBlank: false + } + ]; + + me.hdsizesel = Ext.createWidget('numberfield', { + name: 'size', + minValue: 0, + maxValue: 128*1024, + decimalPrecision: 3, + value: '0', + fieldLabel: gettext('Size Increment') + ' (GiB)', + allowBlank: false + }); + + items.push(me.hdsizesel); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 120, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + me.title = gettext('Resize disk'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Resize disk'), + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.resize_disk(me.disk, values.size); + } + } + }); + + Ext.apply(me, { + modal: true, + border: false, + layout: 'fit', + buttons: [ submitBtn ], + items: [ me.formPanel ] + }); + + + me.callParent(); + + if (!me.disk) { + return; + } + + } +}); +/*jslint confusion: true*/ +/* hidden: boolean and string + * bind: function and object + * disabled: boolean and string + */ +Ext.define('PVE.lxc.MountPointInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveLxcMountPointInputPanel', + + insideWizard: false, + + onlineHelp: 'pct_container_storage', + + unused: false, // add unused disk imaged + + unprivileged: false, + + vmconfig: {}, // used to select unused disks + + setUnprivileged: function(unprivileged) { + var me = this; + var vm = me.getViewModel(); + me.unprivileged = unprivileged; + vm.set('unpriv', unprivileged); + }, + + onGetValues: function(values) { + var me = this; + + var confid = me.confid || "mp"+values.mpid; + values.file = me.down('field[name=file]').getValue(); + if (values.mountoptions) { + values.mountoptions = values.mountoptions.join(';'); + } + + if (me.unused) { + confid = "mp"+values.mpid; + } else if (me.isCreate) { + values.file = values.hdstorage + ':' + values.disksize; + } + + // delete unnecessary fields + delete values.mpid; + delete values.hdstorage; + delete values.disksize; + delete values.diskformat; + + var res = {}; + res[confid] = PVE.Parser.printLxcMountPoint(values); + return res; + }, + + + setMountPoint: function(mp) { + var me = this; + var vm = this.getViewModel(); + vm.set('mptype', mp.type); + if (mp.mountoptions) { + mp.mountoptions = mp.mountoptions.split(';'); + } + + if (this.confid === 'rootfs') { + var field = me.down('field[name=mountoptions]'); + var forbidden = ['nodev', 'noexec']; + var filtered = field.comboItems.filter(e => !forbidden.includes(e[0])); + field.setComboItems(filtered); + } + + me.setValues(mp); + }, + + setVMConfig: function(vmconfig) { + var me = this; + var vm = me.getViewModel(); + me.vmconfig = vmconfig; + vm.set('unpriv', vmconfig.unprivileged); + + PVE.Utils.forEachMP(function(bus, i) { + var name = "mp" + i.toString(); + if (!Ext.isDefined(vmconfig[name])) { + me.down('field[name=mpid]').setValue(i); + return false; + } + }); + }, + + setNodename: function(nodename) { + var me = this; + var vm = me.getViewModel(); + vm.set('node', nodename); + me.down('#diskstorage').setNodename(nodename); + }, + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'field[name=mpid]': { + change: function(field, value) { + field.validate(); + } + }, + '#hdstorage': { + change: function(field, newValue) { + var me = this; + if (!newValue) { + return; + } + + var rec = field.store.getById(newValue); + if (!rec) { + return; + } + + var vm = me.getViewModel(); + vm.set('type', rec.data.type); + } + } + }, + + init: function(view) { + var me = this; + var vm = this.getViewModel(); + vm.set('confid', view.confid); + vm.set('unused', view.unused); + vm.set('node', view.nodename); + vm.set('unpriv', view.unprivileged); + vm.set('hideStorSelector', view.unused || !view.isCreate); + } + }, + + viewModel: { + data: { + unpriv: false, + unused: false, + showStorageSelector: false, + mptype: '', + type: '', + confid: '', + node: '' + }, + + formulas: { + quota: function(get) { + return !(get('type') === 'zfs' || + get('type') === 'zfspool' || + get('unpriv') || + get('isBind')); + }, + hasMP: function(get) { + return !!get('confid') && !get('unused'); + }, + isRoot: function(get) { + return get('confid') === 'rootfs'; + }, + isBind: function(get) { + return get('mptype') === 'bind'; + }, + isBindOrRoot: function(get) { + return get('isBind') || get('isRoot'); + } + } + }, + + column1: [ + { + xtype: 'proxmoxintegerfield', + name: 'mpid', + fieldLabel: gettext('Mount Point ID'), + minValue: 0, + maxValue: PVE.Utils.mp_counts.mps - 1, + hidden: true, + allowBlank: false, + disabled: true, + bind: { + hidden: '{hasMP}', + disabled: '{hasMP}' + }, + validator: function(value) { + var me = this.up('inputpanel'); + if (!me.rendered) { + return; + } + if (Ext.isDefined(me.vmconfig["mp"+value])) { + return "Mount point is already in use."; + } + /*jslint confusion: true*/ + /* returns a string above */ + return true; + } + }, + { + xtype: 'pveDiskStorageSelector', + itemId: 'diskstorage', + storageContent: 'rootdir', + hidden: true, + autoSelect: true, + selectformat: false, + defaultSize: 8, + bind: { + hidden: '{hideStorSelector}', + disabled: '{hideStorSelector}', + nodename: '{node}' + } + }, + { + xtype: 'textfield', + disabled: true, + submitValue: false, + fieldLabel: gettext('Disk image'), + name: 'file', + bind: { + hidden: '{!hideStorSelector}' + } + } + ], + + column2: [ + { + xtype: 'textfield', + name: 'mp', + value: '', + emptyText: gettext('/some/path'), + allowBlank: false, + disabled: true, + fieldLabel: gettext('Path'), + bind: { + hidden: '{isRoot}', + disabled: '{isRoot}' + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'backup', + fieldLabel: gettext('Backup'), + bind: { + hidden: '{isRoot}', + disabled: '{isBindOrRoot}' + } + } + ], + + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + name: 'quota', + defaultValue: 0, + bind: { + disabled: '{!quota}' + }, + fieldLabel: gettext('Enable quota'), + listeners: { + disable: function() { + this.reset(); + } + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'ro', + defaultValue: 0, + bind: { + hidden: '{isRoot}', + disabled: '{isRoot}' + }, + fieldLabel: gettext('Read-only') + }, + { + xtype: 'proxmoxKVComboBox', + name: 'mountoptions', + fieldLabel: gettext('Mount options'), + deleteEmpty: false, + comboItems: [ + ['noatime', 'noatime'], + ['nodev', 'nodev'], + ['noexec', 'noexec'], + ['nosuid', 'nosuid'] + ], + multiSelect: true, + value: [], + allowBlank: true + }, + ], + + advancedColumn2: [ + { + xtype: 'proxmoxKVComboBox', + name: 'acl', + fieldLabel: 'ACLs', + deleteEmpty: false, + comboItems: [ + ['__default__', Proxmox.Utils.defaultText], + ['1', Proxmox.Utils.enabledText], + ['0', Proxmox.Utils.disabledText] + ], + value: '__default__', + bind: { + disabled: '{isBind}' + }, + allowBlank: true + }, + { + xtype: 'proxmoxcheckbox', + inputValue: '0', // reverses the logic + name: 'replicate', + fieldLabel: gettext('Skip replication') + } + ] +}); + +Ext.define('PVE.lxc.MountPointEdit', { + extend: 'Proxmox.window.Edit', + + unprivileged: false, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var unused = me.confid && me.confid.match(/^unused\d+$/); + + me.isCreate = me.confid ? unused : true; + + var ipanel = Ext.create('PVE.lxc.MountPointInputPanel', { + confid: me.confid, + nodename: nodename, + unused: unused, + unprivileged: me.unprivileged, + isCreate: me.isCreate + }); + + var subject; + if (unused) { + subject = gettext('Unused Disk'); + } else if (me.isCreate) { + subject = gettext('Mount Point'); + } else { + subject = gettext('Mount Point') + ' (' + me.confid + ')'; + } + + Ext.apply(me, { + subject: subject, + defaultFocus: me.confid !== 'rootfs' ? 'textfield[name=mp]' : 'tool', + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + /*jslint confusion: true*/ + /*data is defined as array above*/ + var value = response.result.data[me.confid]; + /*jslint confusion: false*/ + var mp = PVE.Parser.parseLxcMountPoint(value); + + if (!mp) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse mount point options'); + me.close(); + return; + } + + ipanel.setMountPoint(mp); + me.isValid(); // trigger validation + } + } + }); + } +}); +Ext.define('PVE.pool.StatusView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pvePoolStatusView'], + disabled: true, + + title: gettext('Status'), + cwidth1: 150, + interval: 30000, + //height: 195, + initComponent : function() { + var me = this; + + var pool = me.pveSelNode.data.pool; + if (!pool) { + throw "no pool specified"; + } + + var rows = { + comment: { + header: gettext('Comment'), + renderer: Ext.String.htmlEncode, + required: true + } + }; + + Ext.apply(me, { + url: "/api2/json/pools/" + pool, + rows: rows + }); + + me.callParent(); + } +}); +Ext.define('PVE.pool.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pvePoolSummary', + + initComponent: function() { + var me = this; + + var pool = me.pveSelNode.data.pool; + if (!pool) { + throw "no pool specified"; + } + + var statusview = Ext.create('PVE.pool.StatusView', { + pveSelNode: me.pveSelNode, + style: 'padding-top:0px' + }); + + var rstore = statusview.rstore; + + Ext.apply(me, { + autoScroll: true, + bodyStyle: 'padding:10px', + defaults: { + style: 'padding-top:10px', + width: 800 + }, + items: [ statusview ] + }); + + me.on('activate', rstore.startUpdate); + me.on('destroy', rstore.stopUpdate); + + me.callParent(); + } +}); +Ext.define('PVE.pool.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.pvePoolConfig', + + onlineHelp: 'pveum_pools', + + initComponent: function() { + var me = this; + + var pool = me.pveSelNode.data.pool; + if (!pool) { + throw "no pool specified"; + } + + Ext.apply(me, { + title: Ext.String.format(gettext("Resource Pool") + ': ' + pool), + hstateid: 'pooltab', + items: [ + { + title: gettext('Summary'), + iconCls: 'fa fa-book', + xtype: 'pvePoolSummary', + itemId: 'summary' + }, + { + title: gettext('Members'), + xtype: 'pvePoolMembers', + iconCls: 'fa fa-th', + pool: pool, + itemId: 'members' + }, + { + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: '/pool/' + pool + } + ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.panel.StorageBase', { + extend: 'Proxmox.panel.InputPanel', + controller: 'storageEdit', + + type: '', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.storage; + } + + values.disable = values.enable ? 0 : 1; + delete values.enable; + + return values; + }, + + initComponent : function() { + var me = this; + + me.column1.unshift({ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'storage', + value: me.storageId || '', + fieldLabel: 'ID', + vtype: 'StorageId', + allowBlank: false + }); + + me.column2.unshift( + { + xtype: 'pveNodeSelector', + name: 'nodes', + disabled: me.storageId === 'local', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', + multiSelect: true, + autoSelect: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: true, + uncheckedValue: 0, + fieldLabel: gettext('Enable') + } + ); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.BaseEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + me.isCreate = !me.storageId; + + if (me.isCreate) { + me.url = '/api2/extjs/storage'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/storage/' + me.storageId; + me.method = 'PUT'; + } + + var ipanel = Ext.create(me.paneltype, { + type: me.type, + isCreate: me.isCreate, + storageId: me.storageId + }); + + Ext.apply(me, { + subject: PVE.Utils.format_storage_type(me.type), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + var ctypes = values.content || ''; + + values.content = ctypes.split(','); + + if (values.nodes) { + values.nodes = values.nodes.split(','); + } + values.enable = values.disable ? 0 : 1; + + ipanel.setValues(values); + } + }); + } + } +}); +Ext.define('PVE.grid.TemplateSelector', { + extend: 'Ext.grid.GridPanel', + + alias: 'widget.pveTemplateSelector', + + stateful: true, + stateId: 'grid-template-selector', + viewConfig: { + trackOver: false + }, + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var baseurl = "/nodes/" + me.nodename + "/aplinfo"; + var store = new Ext.data.Store({ + model: 'pve-aplinfo', + groupField: 'section', + proxy: { + type: 'proxmox', + url: '/api2/json' + baseurl + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var groupingFeature = Ext.create('Ext.grid.feature.Grouping',{ + groupHeaderTpl: '{[ "Section: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})' + }); + + var reload = function() { + store.load(); + }; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + '->', + gettext('Search'), + { + xtype: 'textfield', + width: 200, + enableKeyEvents: true, + listeners: { + buffer: 500, + keyup: function(field) { + var value = field.getValue().toLowerCase(); + store.clearFilter(true); + store.filterBy(function(rec) { + return (rec.data['package'].toLowerCase().indexOf(value) !== -1) + || (rec.data.headline.toLowerCase().indexOf(value) !== -1); + }); + } + } + } + ], + features: [ groupingFeature ], + columns: [ + { + header: gettext('Type'), + width: 80, + dataIndex: 'type' + }, + { + header: gettext('Package'), + flex: 1, + dataIndex: 'package' + }, + { + header: gettext('Version'), + width: 80, + dataIndex: 'version' + }, + { + header: gettext('Description'), + flex: 1.5, + renderer: Ext.String.htmlEncode, + dataIndex: 'headline' + } + ], + listeners: { + afterRender: reload + } + }); + + me.callParent(); + } + +}, function() { + + Ext.define('pve-aplinfo', { + extend: 'Ext.data.Model', + fields: [ + 'template', 'type', 'package', 'version', 'headline', 'infopage', + 'description', 'os', 'section' + ], + idProperty: 'template' + }); + +}); + +Ext.define('PVE.storage.TemplateDownload', { + extend: 'Ext.window.Window', + alias: 'widget.pveTemplateDownload', + + modal: true, + title: gettext('Templates'), + layout: 'fit', + width: 900, + height: 600, + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + var grid = Ext.create('PVE.grid.TemplateSelector', { + border: false, + scrollable: true, + nodename: me.nodename + }); + + var sm = grid.getSelectionModel(); + + var submitBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Download'), + disabled: true, + selModel: sm, + handler: function(button, event, rec) { + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/aplinfo', + params: { + storage: me.storage, + template: rec.data.template + }, + method: 'POST', + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + + Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + listeners: { + destroy: me.reloadGrid + } + }).show(); + + me.close(); + } + }); + } + }); + + Ext.apply(me, { + items: grid, + buttons: [ submitBtn ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.Upload', { + extend: 'Ext.window.Window', + alias: 'widget.pveStorageUpload', + + resizable: false, + + modal: true, + + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + var xhr; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.storage) { + throw "no storage ID specified"; + } + + var baseurl = "/nodes/" + me.nodename + "/storage/" + me.storage + "/upload"; + + var pbar = Ext.create('Ext.ProgressBar', { + text: 'Ready', + hidden: true + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + method: 'POST', + waitMsgTarget: true, + bodyPadding: 10, + border: false, + width: 300, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [ + { + xtype: 'pveContentTypeSelector', + cts: me.contents, + fieldLabel: gettext('Content'), + name: 'content', + value: me.contents[0] || '', + allowBlank: false + }, + { + xtype: 'filefield', + name: 'filename', + buttonText: gettext('Select File...'), + allowBlank: false + }, + pbar + ] + }); + + var form = me.formPanel.getForm(); + + var doStandardSubmit = function() { + form.submit({ + url: "/api2/htmljs" + baseurl, + waitMsg: gettext('Uploading file...'), + success: function(f, action) { + me.close(); + }, + failure: function(f, action) { + var msg = PVE.Utils.extractFormActionError(action); + Ext.Msg.alert(gettext('Error'), msg); + } + }); + }; + + var updateProgress = function(per, bytes) { + var text = (per * 100).toFixed(2) + '%'; + if (bytes) { + text += " (" + Proxmox.Utils.format_size(bytes) + ')'; + } + pbar.updateProgress(per, text); + }; + + var abortBtn = Ext.create('Ext.Button', { + text: gettext('Abort'), + disabled: true, + handler: function() { + me.close(); + } + }); + + var submitBtn = Ext.create('Ext.Button', { + text: gettext('Upload'), + disabled: true, + handler: function(button) { + var fd; + try { + fd = new FormData(); + } catch (err) { + doStandardSubmit(); + return; + } + + button.setDisabled(true); + abortBtn.setDisabled(false); + + var field = form.findField('content'); + fd.append("content", field.getValue()); + field.setDisabled(true); + + field = form.findField('filename'); + var file = field.fileInputEl.dom; + fd.append("filename", file.files[0]); + field.setDisabled(true); + + pbar.setVisible(true); + updateProgress(0); + + xhr = new XMLHttpRequest(); + + xhr.addEventListener("load", function(e) { + if (xhr.status == 200) { + me.close(); + } else { + var msg = gettext('Error') + " " + xhr.status.toString() + ": " + Ext.htmlEncode(xhr.statusText); + if (xhr.responseText !== "") { + var result = Ext.decode(xhr.responseText); + result.message = msg; + msg = Proxmox.Utils.extractRequestError(result, true); + } + Ext.Msg.alert(gettext('Error'), msg, function(btn) { + me.close(); + }); + } + }, false); + + xhr.addEventListener("error", function(e) { + var msg = "Error " + e.target.status.toString() + " occurred while receiving the document."; + Ext.Msg.alert(gettext('Error'), msg, function(btn) { + me.close(); + }); + }); + + xhr.upload.addEventListener("progress", function(evt) { + if (evt.lengthComputable) { + var percentComplete = evt.loaded / evt.total; + updateProgress(percentComplete, evt.loaded); + } + }, false); + + xhr.open("POST", "/api2/json" + baseurl, true); + xhr.send(fd); + } + }); + + form.on('validitychange', function(f, valid) { + submitBtn.setDisabled(!valid); + }); + + Ext.apply(me, { + title: gettext('Upload'), + items: me.formPanel, + buttons: [ abortBtn, submitBtn ], + listeners: { + close: function() { + if (xhr) { + xhr.abort(); + } + } + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.ContentView', { + extend: 'Ext.grid.GridPanel', + + alias: 'widget.pveStorageContentView', + + stateful: true, + stateId: 'grid-storage-content', + viewConfig: { + trackOver: false, + loadMask: false + }, + features: [ + { + ftype: 'grouping', + groupHeaderTpl: '{name} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})' + } + ], + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var storage = me.pveSelNode.data.storage; + if (!storage) { + throw "no storage ID specified"; + } + + var baseurl = "/nodes/" + nodename + "/storage/" + storage + "/content"; + var store = Ext.create('Ext.data.Store',{ + model: 'pve-storage-content', + groupField: 'content', + proxy: { + type: 'proxmox', + url: '/api2/json' + baseurl + }, + sorters: { + property: 'volid', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + store.load(); + me.statusStore.load(); + }; + + Proxmox.Utils.monStoreErrors(me, store); + + var templateButton = Ext.create('Proxmox.button.Button',{ + itemId: 'tmpl-btn', + text: gettext('Templates'), + handler: function() { + var win = Ext.create('PVE.storage.TemplateDownload', { + nodename: nodename, + storage: storage, + reloadGrid: reload + }); + win.show(); + } + }); + + var uploadButton = Ext.create('Proxmox.button.Button', { + contents : ['iso','vztmpl'], + text: gettext('Upload'), + handler: function() { + var me = this; + var win = Ext.create('PVE.storage.Upload', { + nodename: nodename, + storage: storage, + contents: me.contents + }); + win.show(); + win.on('destroy', reload); + } + }); + + var imageRemoveButton; + var removeButton = Ext.create('Proxmox.button.StdRemoveButton',{ + selModel: sm, + enableFn: function(rec) { + if (rec && rec.data.content !== 'images') { + imageRemoveButton.setVisible(false); + removeButton.setVisible(true); + return true; + } + return false; + }, + callback: function() { + reload(); + }, + baseurl: baseurl + '/' + }); + + imageRemoveButton = Ext.create('Proxmox.button.Button',{ + selModel: sm, + hidden: true, + text: gettext('Remove'), + enableFn: function(rec) { + if (rec && rec.data.content === 'images') { + removeButton.setVisible(false); + imageRemoveButton.setVisible(true); + return true; + } + return false; + }, + handler: function(btn, event, rec) { + me = this; + + var url = baseurl + '/' + rec.data.volid; + var vmid = rec.data.vmid; + + var store = PVE.data.ResourceStore; + + if (vmid && store.findVMID(vmid)) { + var guest_node = store.guestNode(vmid); + var storage_path = 'storage/' + nodename + '/' + storage; + + // allow to delete local backed images if a VMID exists on another node. + if (store.storageIsShared(storage_path) || guest_node == nodename) { + var msg = Ext.String.format( + gettext("Cannot remove image, a guest with VMID '{0}' exists!"), vmid); + msg += '
' + gettext("You can delete the image from the guest's hardware pane"); + + Ext.Msg.show({ + title: gettext('Cannot remove disk image.'), + icon: Ext.Msg.ERROR, + msg: msg + }); + return; + } + } + var win = Ext.create('PVE.window.SafeDestroy', { + title: Ext.String.format(gettext("Destroy '{0}'"), rec.data.volid), + showProgress: true, + url: url, + item: { type: 'Image', id: vmid } + }).show(); + win.on('destroy', function() { + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status' + }); + reload(); + + }); + } + }); + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status' + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Restore'), + selModel: sm, + disabled: true, + enableFn: function(rec) { + return rec && rec.data.content === 'backup'; + }, + handler: function(b, e, rec) { + var vmtype; + if (rec.data.volid.match(/vzdump-qemu-/)) { + vmtype = 'qemu'; + } else if (rec.data.volid.match(/vzdump-openvz-/) || rec.data.volid.match(/vzdump-lxc-/)) { + vmtype = 'lxc'; + } else { + return; + } + + var win = Ext.create('PVE.window.Restore', { + nodename: nodename, + volid: rec.data.volid, + volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec), + vmtype: vmtype + }); + win.show(); + win.on('destroy', reload); + } + }, + removeButton, + imageRemoveButton, + templateButton, + uploadButton, + { + xtype: 'proxmoxButton', + text: gettext('Show Configuration'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return rec && rec.data.content === 'backup'; + }, + handler: function(b,e,rec) { + var win = Ext.create('PVE.window.BackupConfig', { + volume: rec.data.volid, + pveSelNode: me.pveSelNode + }); + + win.show(); + } + }, + '->', + gettext('Search') + ':', ' ', + { + xtype: 'textfield', + width: 200, + enableKeyEvents: true, + listeners: { + buffer: 500, + keyup: function(field) { + store.clearFilter(true); + store.filter([ + { + property: 'text', + value: field.getValue(), + anyMatch: true, + caseSensitive: false + } + ]); + } + } + } + ], + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + renderer: PVE.Utils.render_storage_content, + dataIndex: 'text' + }, + { + header: gettext('Format'), + width: 100, + dataIndex: 'format' + }, + { + header: gettext('Type'), + width: 100, + dataIndex: 'content', + renderer: PVE.Utils.format_content_types + }, + { + header: gettext('Size'), + width: 100, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + } + ], + listeners: { + activate: reload + } + }); + + me.callParent(); + + // disable the buttons/restrict the upload window + // if templates or uploads are not allowed + me.mon(me.statusStore, 'load', function(s, records, success) { + var availcontent = []; + Ext.Array.each(records, function(item){ + if (item.id === 'content') { + availcontent = item.data.value.split(','); + } + }); + var templ = false; + var upload = false; + var cts = []; + + Ext.Array.each(availcontent, function(content) { + if (content === 'vztmpl') { + templ = true; + cts.push('vztmpl'); + } else if (content === 'iso') { + upload = true; + cts.push('iso'); + } + }); + + if (templ !== upload) { + uploadButton.contents = cts; + } + + templateButton.setDisabled(!templ); + uploadButton.setDisabled(!upload && !templ); + }); + } +}, function() { + + Ext.define('pve-storage-content', { + extend: 'Ext.data.Model', + fields: [ + 'volid', 'content', 'format', 'size', 'used', 'vmid', + 'channel', 'id', 'lun', + { + name: 'text', + convert: function(value, record) { + // check for volid, because if you click on a grouping header, + // it calls convert (but with an empty volid) + if (value || record.data.volid === null) { + return value; + } + return PVE.Utils.render_storage_content(value, {}, record); + } + } + ], + idProperty: 'volid' + }); + +}); +Ext.define('PVE.storage.StatusView', { + extend: 'PVE.panel.StatusView', + alias: 'widget.pveStorageStatusView', + + height: 230, + title: gettext('Status'), + + layout: { + type: 'vbox', + align: 'stretch' + }, + + defaults: { + xtype: 'pveInfoWidget', + padding: '0 30 5 30' + }, + items: [ + { + xtype: 'box', + height: 30 + }, + { + itemId: 'enabled', + title: gettext('Enabled'), + printBar: false, + textField: 'disabled', + renderer: Proxmox.Utils.format_neg_boolean + }, + { + itemId: 'active', + title: gettext('Active'), + printBar: false, + textField: 'active', + renderer: Proxmox.Utils.format_boolean + }, + { + itemId: 'content', + title: gettext('Content'), + printBar: false, + textField: 'content', + renderer: PVE.Utils.format_content_types + }, + { + itemId: 'type', + title: gettext('Type'), + printBar: false, + textField: 'type', + renderer: PVE.Utils.format_storage_type + }, + { + xtype: 'box', + height: 10 + }, + { + itemId: 'usage', + title: gettext('Usage'), + valueField: 'used', + maxField: 'total' + } + ], + + updateTitle: function() { + return; + } +}); +Ext.define('PVE.storage.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveStorageSummary', + scrollable: true, + bodyPadding: 5, + tbar: [ + '->', + { + xtype: 'proxmoxRRDTypeSelector' + } + ], + layout: { + type: 'column' + }, + defaults: { + padding: 5, + columnWidth: 1 + }, + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var storage = me.pveSelNode.data.storage; + if (!storage) { + throw "no storage ID specified"; + } + + var rstore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status", + interval: 1000 + }); + + var rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata", + model: 'pve-rrd-storage' + }); + + Ext.apply(me, { + items: [ + { + xtype: 'pveStorageStatusView', + pveSelNode: me.pveSelNode, + rstore: rstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Usage'), + fields: ['total','used'], + fieldTitles: ['Total Size', 'Used Size'], + store: rrdstore + } + ], + listeners: { + activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); }, + destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); } + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.storage.Browser', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.storage.Browser', + + onlineHelp: 'chapter_storage', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var storeid = me.pveSelNode.data.storage; + if (!storeid) { + throw "no storage ID specified"; + } + + + me.items = [ + { + title: gettext('Summary'), + xtype: 'pveStorageSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + } + ]; + + var caps = Ext.state.Manager.get('GuiCap'); + + Ext.apply(me, { + title: Ext.String.format(gettext("Storage {0} on node {1}"), + "'" + storeid + "'", "'" + nodename + "'"), + hstateid: 'storagetab' + }); + + if (caps.storage['Datastore.Allocate'] || + caps.storage['Datastore.AllocateSpace'] || + caps.storage['Datastore.Audit']) { + me.items.push({ + xtype: 'pveStorageContentView', + title: gettext('Content'), + iconCls: 'fa fa-th', + itemId: 'content' + }); + } + + if (caps.storage['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: '/storage/' + storeid + }); + } + + me.callParent(); + } +}); +Ext.define('PVE.storage.DirInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_directory', + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'path', + value: '', + fieldLabel: gettext('Directory'), + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'shared', + uncheckedValue: 0, + fieldLabel: gettext('Shared') + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + disabled: true, + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.NFSScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveNFSScan', + + queryParam: 'server', + + valueField: 'path', + displayField: 'path', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.nfsServer) { + me.store.removeAll(); + } + + me.allQuery = me.nfsServer; + + me.callParent(); + }, + + setServer: function(server) { + var me = this; + + me.nfsServer = server; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'path', 'options' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/nfs' + } + }); + + store.sort('path', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.NFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_nfs', + + options : [], + + onGetValues: function(values) { + var me = this; + + var i; + var res = []; + for (i = 0; i < me.options.length; i++) { + var item = me.options[i]; + if (!item.match(/^vers=(.*)$/)) { + res.push(item); + } + } + if (values.nfsversion && values.nfsversion !== '__default__') { + res.push('vers=' + values.nfsversion); + } + delete values.nfsversion; + values.options = res.join(','); + if (values.options === '') { + delete values.options; + if (!me.isCreate) { + values["delete"] = "options"; + } + } + + return me.callParent([values]); + }, + + setValues: function(values) { + var me = this; + if (values.options) { + var res = values.options; + me.options = values.options.split(','); + me.options.forEach(function(item) { + var match = item.match(/^vers=(.*)$/); + if (match) { + values.nfsversion = match[1]; + } + }); + } + return me.callParent([values]); + }, + + initComponent : function() { + var me = this; + + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=export]'); + exportField.setServer(value); + exportField.setValue(''); + } + } + } + }, + { + xtype: me.isCreate ? 'pveNFSScan' : 'displayfield', + name: 'export', + value: '', + fieldLabel: 'Export', + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + disabled: true, + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('NFS Version'), + name: 'nfsversion', + value: '__default__', + deleteEmpty: false, + comboItems: [ + ['__default__', Proxmox.Utils.defaultText], + ['3', '3'], + ['4', '4'], + ['4.1', '4.1'], + ['4.2', '4.2'] + ] + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.CIFSScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCIFSScan', + + queryParam: 'server', + + valueField: 'share', + displayField: 'share', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.cifsServer) { + me.store.removeAll(); + } + + var params = {}; + if (me.cifsUsername && me.cifsPassword) { + params.username = me.cifsUsername; + params.password = me.cifsPassword; + } + + if (me.cifsDomain) { + params.domain = me.cifsDomain; + } + + me.store.getProxy().setExtraParams(params); + me.allQuery = me.cifsServer; + + me.callParent(); + }, + + setServer: function(server) { + this.cifsServer = server; + }, + + setUsername: function(username) { + this.cifsUsername = username; + }, + + setPassword: function(password) { + this.cifsPassword = password; + }, + + setDomain: function(domain) { + this.cifsDomain = domain; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['description', 'share'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/cifs' + } + }); + store.sort('share', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.CIFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_cifs', + + initComponent : function() { + var me = this; + + var passwordfield = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + inputType: 'password', + name: 'password', + value: me.isCreate ? '' : '********', + fieldLabel: gettext('Password'), + allowBlank: false, + disabled: me.isCreate, + minLength: 1, + listeners: { + change: function(f, value) { + + if (me.isCreate) { + var exportField = me.down('field[name=share]'); + exportField.setPassword(value); + } + } + } + }); + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=share]'); + exportField.setServer(value); + } + } + } + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + value: '', + fieldLabel: gettext('Username'), + emptyText: gettext('Guest user'), + allowBlank: true, + listeners: { + change: function(f, value) { + if (!me.isCreate) { + return; + } + var exportField = me.down('field[name=share]'); + exportField.setUsername(value); + + if (value == "") { + passwordfield.disable(); + } else { + passwordfield.enable(); + } + passwordfield.validate(); + } + } + }, + passwordfield, + { + xtype: me.isCreate ? 'pveCIFSScan' : 'displayfield', + name: 'share', + value: '', + fieldLabel: 'Share', + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'domain', + value: me.isCreate ? '' : undefined, + fieldLabel: gettext('Domain'), + allowBlank: true, + listeners: { + change: function(f, value) { + if (me.isCreate) { + + var exportField = me.down('field[name=share]'); + exportField.setDomain(value); + } + } + } + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.GlusterFsScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveGlusterFsScan', + + queryParam: 'server', + + valueField: 'volname', + displayField: 'volname', + matchFieldWidth: false, + listConfig: { + loadingText: 'Scanning...', + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.glusterServer) { + me.store.removeAll(); + } + + me.allQuery = me.glusterServer; + + me.callParent(); + }, + + setServer: function(server) { + var me = this; + + me.glusterServer = server; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'volname' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/glusterfs' + } + }); + + store.sort('volname', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.GlusterFsInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_glusterfs', + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var volumeField = me.down('field[name=volume]'); + volumeField.setServer(value); + volumeField.setValue(''); + } + } + } + }, + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + name: 'server2', + value: '', + fieldLabel: gettext('Second Server'), + allowBlank: true + }, + { + xtype: me.isCreate ? 'pveGlusterFsScan' : 'displayfield', + name: 'volume', + value: '', + fieldLabel: 'Volume name', + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'iso', 'backup', 'vztmpl', 'snippets'], + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + disabled: true, + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.IScsiScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveIScsiScan', + + queryParam: 'portal', + valueField: 'target', + displayField: 'target', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.portal) { + me.store.removeAll(); + } + + me.allQuery = me.portal; + + me.callParent(); + }, + + setPortal: function(portal) { + var me = this; + + me.portal = portal; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'target', 'portal' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/iscsi' + } + }); + + store.sort('target', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.IScsiInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_open_iscsi', + + onGetValues: function(values) { + var me = this; + + values.content = values.luns ? 'images' : 'none'; + delete values.luns; + + return me.callParent([values]); + }, + + setValues: function(values) { + values.luns = (values.content.indexOf('images') !== -1) ? true : false; + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'portal', + value: '', + fieldLabel: 'Portal', + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=target]'); + exportField.setPortal(value); + exportField.setValue(''); + } + } + } + }, + { + readOnly: !me.isCreate, + xtype: me.isCreate ? 'pveIScsiScan' : 'displayfield', + name: 'target', + value: '', + fieldLabel: 'Target', + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'checkbox', + name: 'luns', + checked: true, + fieldLabel: gettext('Use LUNs directly') + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.VgSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveVgSelector', + valueField: 'vg', + displayField: 'vg', + queryMode: 'local', + editable: false, + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: {}, // true, + fields: [ 'vg', 'size', 'free' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/lvm' + } + }); + + store.sort('vg', 'ASC'); + + Ext.apply(me, { + store: store, + listConfig: { + loadingText: gettext('Scanning...') + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.BaseStorageSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveBaseStorageSelector', + + existingGroupsText: gettext("Existing volume groups"), + queryMode: 'local', + editable: false, + value: '', + valueField: 'storage', + displayField: 'text', + initComponent : function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: { + addRecords: true, + params: { + type: 'iscsi' + } + }, + fields: [ 'storage', 'type', 'content', + { + name: 'text', + convert: function(value, record) { + if (record.data.storage) { + return record.data.storage + " (iSCSI)"; + } else { + return me.existingGroupsText; + } + } + }], + proxy: { + type: 'proxmox', + url: '/api2/json/storage/' + } + }); + + store.loadData([{ storage: '' }], true); + + store.sort('storage', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.LVMInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_lvm', + + initComponent : function() { + var me = this; + + me.column1 = []; + + var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + name: 'vgname', + hidden: !!me.isCreate, + disabled: !!me.isCreate, + value: '', + fieldLabel: gettext('Volume group'), + allowBlank: false + }); + + if (me.isCreate) { + var vgField = Ext.create('PVE.storage.VgSelector', { + name: 'vgname', + fieldLabel: gettext('Volume group'), + allowBlank: false + }); + + var baseField = Ext.createWidget('pveFileSelector', { + name: 'base', + hidden: true, + disabled: true, + nodename: 'localhost', + storageContent: 'images', + fieldLabel: gettext('Base volume'), + allowBlank: false + }); + + me.column1.push({ + xtype: 'pveBaseStorageSelector', + name: 'basesel', + fieldLabel: gettext('Base storage'), + submitValue: false, + listeners: { + change: function(f, value) { + if (value) { + vgnameField.setVisible(true); + vgnameField.setDisabled(false); + vgField.setVisible(false); + vgField.setDisabled(true); + baseField.setVisible(true); + baseField.setDisabled(false); + } else { + vgnameField.setVisible(false); + vgnameField.setDisabled(true); + vgField.setVisible(true); + vgField.setDisabled(false); + baseField.setVisible(false); + baseField.setDisabled(true); + } + baseField.setStorage(value); + } + } + }); + + me.column1.push(baseField); + + me.column1.push(vgField); + } + + me.column1.push(vgnameField); + + // here value is an array, + // while before it was a string + /*jslint confusion: true*/ + me.column1.push({ + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false + }); + /*jslint confusion: false*/ + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'shared', + uncheckedValue: 0, + fieldLabel: gettext('Shared') + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.TPoolSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveTPSelector', + + queryParam: 'vg', + valueField: 'lv', + displayField: 'lv', + editable: false, + + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.vg) { + me.store.removeAll(); + } + + me.allQuery = me.vg; + + me.callParent(); + }, + + setVG: function(myvg) { + var me = this; + + me.vg = myvg; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'lv' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/lvmthin' + } + }); + + store.sort('lv', 'ASC'); + + Ext.apply(me, { + store: store, + listConfig: { + loadingText: gettext('Scanning...') + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.BaseVGSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveBaseVGSelector', + + valueField: 'vg', + displayField: 'vg', + queryMode: 'local', + editable: false, + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: {}, + fields: [ 'vg', 'size', 'free'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/lvm' + } + }); + + Ext.apply(me, { + store: store, + listConfig: { + loadingText: gettext('Scanning...') + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.LvmThinInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_lvmthin', + + initComponent : function() { + var me = this; + + me.column1 = []; + + var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + name: 'vgname', + hidden: !!me.isCreate, + disabled: !!me.isCreate, + value: '', + fieldLabel: gettext('Volume group'), + allowBlank: false + }); + + var thinpoolField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + name: 'thinpool', + hidden: !!me.isCreate, + disabled: !!me.isCreate, + value: '', + fieldLabel: gettext('Thin Pool'), + allowBlank: false + }); + + if (me.isCreate) { + var vgField = Ext.create('PVE.storage.TPoolSelector', { + name: 'thinpool', + fieldLabel: gettext('Thin Pool'), + allowBlank: false + }); + + me.column1.push({ + xtype: 'pveBaseVGSelector', + name: 'vgname', + fieldLabel: gettext('Volume group'), + listeners: { + change: function(f, value) { + if (me.isCreate) { + vgField.setVG(value); + vgField.setValue(''); + } + } + } + }); + + me.column1.push(vgField); + } + + me.column1.push(vgnameField); + + me.column1.push(thinpoolField); + + // here value is an array, + // while before it was a string + /*jslint confusion: true*/ + me.column1.push({ + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false + }); + /*jslint confusion: false*/ + + me.column2 = []; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.CephFSInputPanel', { + extend: 'PVE.panel.StorageBase', + controller: 'cephstorage', + + onlineHelp: 'storage_cephfs', + + viewModel: { + type: 'cephstorage' + }, + + setValues: function(values) { + if (values.monhost) { + this.viewModel.set('pveceph', false); + this.lookupReference('pvecephRef').setValue(false); + this.lookupReference('pvecephRef').resetOriginalValue(); + } + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + me.type = 'cephfs'; + + me.column1 = []; + + me.column1.push( + { + xtype: 'textfield', + name: 'monhost', + vtype: 'HostList', + value: '', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}' + }, + fieldLabel: 'Monitor(s)', + allowBlank: false + }, + { + xtype: 'displayfield', + reference: 'monhost', + bind: { + disabled: '{!pveceph}', + hidden: '{!pveceph}' + }, + value: '', + fieldLabel: 'Monitor(s)' + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + value: 'admin', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}' + }, + fieldLabel: gettext('User name'), + allowBlank: true + } + ); + + me.column2 = [ + { + xtype: 'pveContentTypeSelector', + cts: ['backup', 'iso', 'vztmpl', 'snippets'], + fieldLabel: gettext('Content'), + name: 'content', + value: 'backup', + multiSelect: true, + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.columnB = [{ + xtype: 'proxmoxcheckbox', + name: 'pveceph', + reference: 'pvecephRef', + bind : { + disabled: '{!pvecephPossible}', + value: '{pveceph}' + }, + checked: true, + uncheckedValue: 0, + submitValue: false, + hidden: !me.isCreate, + boxLabel: gettext('Use Proxmox VE managed hyper-converged cephFS') + }]; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.Ceph.Model', { + extend: 'Ext.app.ViewModel', + alias: 'viewmodel.cephstorage', + + data: { + pveceph: true, + pvecephPossible: true + } +}); + +Ext.define('PVE.storage.Ceph.Controller', { + extend: 'PVE.controller.StorageEdit', + alias: 'controller.cephstorage', + + control: { + '#': { + afterrender: 'queryMonitors' + }, + 'textfield[name=username]': { + disable: 'resetField' + }, + 'displayfield[name=monhost]': { + enable: 'queryMonitors' + }, + 'textfield[name=monhost]': { + disable: 'resetField', + enable: 'resetField' + } + }, + resetField: function(field) { + field.reset(); + }, + queryMonitors: function(field, newVal, oldVal) { + // we get called with two signatures, the above one for a field + // change event and the afterrender from the view, this check only + // can be true for the field change one and omit the API request if + // pveceph got unchecked - as it's not needed there. + if (field && !newVal && oldVal) { + return; + } + var view = this.getView(); + var vm = this.getViewModel(); + if (!(view.isCreate || vm.get('pveceph'))) { + return; // only query on create or if editing a pveceph store + } + + var monhostField = this.lookupReference('monhost'); + + Proxmox.Utils.API2Request({ + url: '/api2/json/nodes/localhost/ceph/mon', + method: 'GET', + scope: this, + callback: function(options, success, response) { + var data = response.result.data; + if (response.status === 200) { + if (data.length > 0) { + var monhost = Ext.Array.pluck(data, 'name').sort().join(','); + monhostField.setValue(monhost); + monhostField.resetOriginalValue(); + if (view.isCreate) { + vm.set('pvecephPossible', true); + } + } else { + vm.set('pveceph', false); + } + } else { + vm.set('pveceph', false); + vm.set('pvecephPossible', false); + } + } + }); + } +}); + +Ext.define('PVE.storage.RBDInputPanel', { + extend: 'PVE.panel.StorageBase', + controller: 'cephstorage', + + onlineHelp: 'ceph_rados_block_devices', + + viewModel: { + type: 'cephstorage' + }, + + setValues: function(values) { + if (values.monhost) { + this.viewModel.set('pveceph', false); + this.lookupReference('pvecephRef').setValue(false); + this.lookupReference('pvecephRef').resetOriginalValue(); + } + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + me.type = 'rbd'; + + me.column1 = []; + + if (me.isCreate) { + me.column1.push({ + xtype: 'pveCephPoolSelector', + nodename: me.nodename, + name: 'pool', + bind: { + disabled: '{!pveceph}', + submitValue: '{pveceph}', + hidden: '{!pveceph}' + }, + fieldLabel: gettext('Pool'), + allowBlank: false + },{ + xtype: 'textfield', + name: 'pool', + value: 'rbd', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}' + }, + fieldLabel: gettext('Pool'), + allowBlank: false + }); + } else { + me.column1.push({ + xtype: 'displayfield', + nodename: me.nodename, + name: 'pool', + fieldLabel: gettext('Pool'), + allowBlank: false + }); + } + + me.column1.push( + { + xtype: 'textfield', + name: 'monhost', + vtype: 'HostList', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}' + }, + value: '', + fieldLabel: 'Monitor(s)', + allowBlank: false + }, + { + xtype: 'displayfield', + reference: 'monhost', + bind: { + disabled: '{!pveceph}', + hidden: '{!pveceph}' + }, + value: '', + fieldLabel: 'Monitor(s)' + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}' + }, + value: 'admin', + fieldLabel: gettext('User name'), + allowBlank: true + } + ); + + me.column2 = [ + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images'], + multiSelect: true, + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'krbd', + uncheckedValue: 0, + fieldLabel: 'KRBD' + } + ]; + + me.columnB = [{ + xtype: 'proxmoxcheckbox', + name: 'pveceph', + reference: 'pvecephRef', + bind : { + disabled: '{!pvecephPossible}', + value: '{pveceph}' + }, + checked: true, + uncheckedValue: 0, + submitValue: false, + hidden: !me.isCreate, + boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool') + }]; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.ZFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + viewModel: { + parent: null, + data: { + isLIO: false, + isComstar: true, + hasWriteCacheOption: true + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'field[name=iscsiprovider]': { + change: 'changeISCSIProvider' + } + }, + changeISCSIProvider: function(f, newVal, oldVal) { + var vm = this.getViewModel(); + vm.set('isLIO', newVal === 'LIO'); + vm.set('isComstar', newVal === 'comstar'); + vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt'); + } + }, + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.content = 'images'; + } + + values.nowritecache = values.writecache ? 0 : 1; + delete values.writecache; + + return me.callParent([values]); + }, + + setValues: function diff(values) { + values.writecache = values.nowritecache ? 0 : 1; + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'portal', + value: '', + fieldLabel: gettext('Portal'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'pool', + value: '', + fieldLabel: gettext('Pool'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'blocksize', + value: '4k', + fieldLabel: gettext('Block Size'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'target', + value: '', + fieldLabel: gettext('Target'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'comstar_tg', + value: '', + fieldLabel: gettext('Target group'), + bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, + allowBlank: true + } + ]; + + me.column2 = [ + { + xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield', + name: 'iscsiprovider', + value: 'comstar', + fieldLabel: gettext('iSCSI Provider'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'sparse', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Thin provision') + }, + { + xtype: 'proxmoxcheckbox', + name: 'writecache', + checked: true, + bind: me.isCreate ? { disabled: '{!hasWriteCacheOption}' } : { hidden: '{!hasWriteCacheOption}' }, + uncheckedValue: 0, + fieldLabel: gettext('Write cache') + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'comstar_hg', + value: '', + bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, + fieldLabel: gettext('Host group'), + allowBlank: true + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'lio_tpg', + value: '', + bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' }, + allowBlank: false, + fieldLabel: gettext('Target portal group') + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.ZFSPoolSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveZFSPoolSelector', + valueField: 'pool', + displayField: 'pool', + queryMode: 'local', + editable: false, + listConfig: { + loadingText: gettext('Scanning...') + }, + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: {}, // true, + fields: [ 'pool', 'size', 'free' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/zfs' + } + }); + + store.sort('pool', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.ZFSPoolInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_zfspool', + + initComponent : function() { + var me = this; + + me.column1 = []; + + if (me.isCreate) { + me.column1.push(Ext.create('PVE.storage.ZFSPoolSelector', { + name: 'pool', + fieldLabel: gettext('ZFS Pool'), + allowBlank: false + })); + } else { + me.column1.push(Ext.createWidget('displayfield', { + name: 'pool', + value: '', + fieldLabel: gettext('ZFS Pool'), + allowBlank: false + })); + } + + // value is an array, + // while before it was a string + /*jslint confusion: true*/ + me.column1.push( + {xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false + }); + /*jslint confusion: false*/ + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'sparse', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Thin provision') + }, + { + xtype: 'textfield', + name: 'blocksize', + emptyText: '8k', + fieldLabel: gettext('Block Size'), + allowBlank: true + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.ha.StatusView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAStatusView'], + + onlineHelp: 'chapter_ha_manager', + + sortPriority: { + quorum: 1, + master: 2, + lrm: 3, + service: 4 + }, + + initComponent : function() { + var me = this; + + if (!me.rstore) { + throw "no rstore given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + sortAfterUpdate: true, + sorters: [{ + sorterFn: function(rec1, rec2) { + var p1 = me.sortPriority[rec1.data.type]; + var p2 = me.sortPriority[rec2.data.type]; + return (p1 !== p2) ? ((p1 > p2) ? 1 : -1) : 0; + } + }], + filters: { + property: 'type', + value: 'service', + operator: '!=' + } + }); + + Ext.apply(me, { + store: store, + stateful: false, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Type'), + width: 80, + dataIndex: 'type' + }, + { + header: gettext('Status'), + width: 80, + flex: 1, + dataIndex: 'status' + } + ] + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + + } +}, function() { + + Ext.define('pve-ha-status', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'type', 'node', 'status', 'sid', + 'state', 'group', 'comment', + 'max_restart', 'max_relocate', 'type', + 'crm_state', 'request_state' + ], + idProperty: 'id' + }); + +}); +Ext.define('PVE.ha.Status', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveHAStatus', + + onlineHelp: 'chapter_ha_manager', + layout: { + type: 'vbox', + align: 'stretch' + }, + + initComponent: function() { + var me = this; + + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + interval: me.interval, + model: 'pve-ha-status', + storeid: 'pve-store-' + (++Ext.idSeed), + groupField: 'type', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/ha/status/current' + } + }); + + me.items = [{ + xtype: 'pveHAStatusView', + title: gettext('Status'), + rstore: me.rstore, + border: 0, + collapsible: true, + padding: '0 0 20 0' + },{ + xtype: 'pveHAResourcesView', + flex: 1, + collapsible: true, + title: gettext('Resources'), + border: 0, + rstore: me.rstore + }]; + + me.callParent(); + me.on('activate', me.rstore.startUpdate); + } +}); +Ext.define('PVE.ha.GroupSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveHAGroupSelector'], + + value: [], + autoSelect: false, + valueField: 'group', + displayField: 'group', + listConfig: { + columns: [ + { + header: gettext('Group'), + width: 100, + sortable: true, + dataIndex: 'group' + }, + { + header: gettext('Nodes'), + width: 100, + sortable: false, + dataIndex: 'nodes' + }, + { + header: gettext('Comment'), + flex: 1, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode + } + ] + }, + store: { + model: 'pve-ha-groups', + sorters: { + property: 'group', + order: 'DESC' + } + }, + + initComponent: function() { + var me = this; + me.callParent(); + me.getStore().load(); + } + +}, function() { + + Ext.define('pve-ha-groups', { + extend: 'Ext.data.Model', + fields: [ + 'group', 'type', 'digest', 'nodes', 'comment', + { + name : 'restricted', + type: 'boolean' + }, + { + name : 'nofailback', + type: 'boolean' + } + ], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/ha/groups" + }, + idProperty: 'group' + }); +}); +Ext.define('PVE.ha.VMResourceInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'ha_manager_resource_config', + vmid: undefined, + + onGetValues: function(values) { + var me = this; + + if (values.vmid) { + values.sid = values.vmid; + } + delete values.vmid; + + PVE.Utils.delete_if_default(values, 'group', '', me.isCreate); + PVE.Utils.delete_if_default(values, 'max_restart', '1', me.isCreate); + PVE.Utils.delete_if_default(values, 'max_relocate', '1', me.isCreate); + + return values; + }, + + initComponent : function() { + var me = this; + var MIN_QUORUM_VOTES = 3; + + var disabledHint = Ext.createWidget({ + xtype: 'displayfield', // won't get submitted by default + userCls: 'pve-hint', + value: 'Disabling the resource will stop the guest system. ' + + 'See the online help for details.', + hidden: true + }); + + var fewVotesHint = Ext.createWidget({ + itemId: 'fewVotesHint', + xtype: 'displayfield', + userCls: 'pve-hint', + value: 'At least three quorum votes are recommended for reliable HA.', + hidden: true + }); + + Proxmox.Utils.API2Request({ + url: '/cluster/config/nodes', + method: 'GET', + failure: function(response) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response) { + var nodes = response.result.data; + var votes = 0; + Ext.Array.forEach(nodes, function(node) { + var vote = parseInt(node.quorum_votes, 10); // parse as base 10 + votes += vote || 0; // parseInt might return NaN, which is false + }); + + if (votes < MIN_QUORUM_VOTES) { + fewVotesHint.setVisible(true); + } + } + }); + + /*jslint confusion: true */ + var vmidStore = (me.vmid) ? {} : { + model: 'PVEResources', + autoLoad: true, + sorters: 'vmid', + filters: [ + { + property: 'type', + value: /lxc|qemu/ + }, + { + property: 'hastate', + value: /unmanaged/ + } + ] + }; + + // value is a string above, but a number below + me.column1 = [ + { + xtype: me.vmid ? 'displayfield' : 'vmComboSelector', + submitValue: me.isCreate, + name: 'vmid', + fieldLabel: (me.vmid && me.guestType === 'ct') ? 'CT' : 'VM', + value: me.vmid, + store: vmidStore, + validateExists: true + }, + { + xtype: 'proxmoxintegerfield', + name: 'max_restart', + fieldLabel: gettext('Max. Restart'), + value: 1, + minValue: 0, + maxValue: 10, + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + name: 'max_relocate', + fieldLabel: gettext('Max. Relocate'), + value: 1, + minValue: 0, + maxValue: 10, + allowBlank: false + } + ]; + /*jslint confusion: false */ + + me.column2 = [ + { + xtype: 'pveHAGroupSelector', + name: 'group', + fieldLabel: gettext('Group') + }, + { + xtype: 'proxmoxKVComboBox', + name: 'state', + value: 'started', + fieldLabel: gettext('Request State'), + comboItems: [ + ['started', 'started'], + ['stopped', 'stopped'], + ['ignored', 'ignored'], + ['disabled', 'disabled'] + ], + listeners: { + 'change': function(field, newValue) { + if (newValue === 'disabled') { + disabledHint.setVisible(true); + } + else { + if (disabledHint.isVisible()) { + disabledHint.setVisible(false); + } + } + } + } + }, + disabledHint + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + }, + fewVotesHint + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.ha.VMResourceEdit', { + extend: 'Proxmox.window.Edit', + + vmid: undefined, + guestType: undefined, + isCreate: undefined, + + initComponent : function() { + var me = this; + + if (me.isCreate === undefined) { + me.isCreate = !me.vmid; + } + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/ha/resources'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/ha/resources/' + me.vmid; + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.ha.VMResourceInputPanel', { + isCreate: me.isCreate, + vmid: me.vmid, + guestType: me.guestType + }); + + Ext.apply(me, { + subject: gettext('Resource') + ': ' + gettext('Container') + + '/' + gettext('Virtual Machine'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + var regex = /^(\S+):(\S+)$/; + var res = regex.exec(values.sid); + + if (res[1] !== 'vm' && res[1] !== 'ct') { + throw "got unexpected resource type"; + } + + values.vmid = res[2]; + + ipanel.setValues(values); + } + }); + } + } +}); +Ext.define('PVE.ha.ResourcesView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAResourcesView'], + + onlineHelp: 'ha_manager_resources', + + stateful: true, + stateId: 'grid-ha-resources', + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + if (!me.rstore) { + throw "no store given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + filters: { + property: 'type', + value: 'service' + } + }); + + var reload = function() { + me.rstore.load(); + }; + + var render_error = function(dataIndex, value, metaData, record) { + var errors = record.data.errors; + if (errors) { + var msg = errors[dataIndex]; + if (msg) { + metaData.tdCls = 'proxmox-invalid-row'; + var html = '

' + Ext.htmlEncode(msg) + '

'; + metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + + html.replace(/\"/g,'"') + '"'; + } + } + return value; + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + var sid = rec.data.sid; + + var regex = /^(\S+):(\S+)$/; + var res = regex.exec(sid); + + if (res[1] !== 'vm' && res[1] !== 'ct') { + return; + } + var guestType = res[1]; + var vmid = res[2]; + + var win = Ext.create('PVE.ha.VMResourceEdit',{ + guestType: guestType, + vmid: vmid + }); + win.on('destroy', reload); + win.show(); + }; + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/ha/resources/', + getUrl: function(rec) { + var me = this; + return me.baseurl + '/' + rec.get('sid'); + }, + callback: function() { + reload(); + } + }); + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + Ext.apply(me, { + store: store, + selModel: sm, + viewConfig: { + trackOver: false + }, + tbar: [ + { + text: gettext('Add'), + disabled: !caps.nodes['Sys.Console'], + handler: function() { + var win = Ext.create('PVE.ha.VMResourceEdit',{}); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn + ], + + columns: [ + { + header: 'ID', + width: 100, + sortable: true, + dataIndex: 'sid' + }, + { + header: gettext('State'), + width: 100, + sortable: true, + dataIndex: 'state' + }, + { + header: gettext('Node'), + width: 100, + sortable: true, + dataIndex: 'node' + }, + { + header: gettext('Request State'), + width: 100, + hidden: true, + sortable: true, + renderer: function(v) { + return v || 'started'; + }, + dataIndex: 'request_state' + }, + { + header: gettext('CRM State'), + width: 100, + hidden: true, + sortable: true, + dataIndex: 'crm_state' + }, + { + header: gettext('Max. Restart'), + width: 100, + sortable: true, + renderer: (v) => v === undefined ? '1' : v, + dataIndex: 'max_restart' + }, + { + header: gettext('Max. Relocate'), + width: 100, + sortable: true, + renderer: (v) => v === undefined ? '1' : v, + dataIndex: 'max_relocate' + }, + { + header: gettext('Group'), + width: 200, + sortable: true, + renderer: function(value, metaData, record) { + return render_error('group', value, metaData, record); + }, + dataIndex: 'group' + }, + { + header: gettext('Description'), + flex: 1, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment' + } + ], + listeners: { + beforeselect: function(grid, record, index, eOpts) { + if (!caps.nodes['Sys.Console']) { + return false; + } + }, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-ha-resources', { + extend: 'Ext.data.Model', + fields: [ + 'sid', 'state', 'digest', 'errors', 'group', 'comment', + 'max_restart', 'max_relocate', 'type', 'status', 'node', + 'crm_state', 'request_state' + ], + idProperty: 'sid' + }); + +}); +Ext.define('PVE.ha.GroupInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'ha_manager_groups', + + groupId: undefined, + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = 'group'; + } + + return values; + }, + + initComponent : function() { + var me = this; + + var update_nodefield, update_node_selection; + + var sm = Ext.create('Ext.selection.CheckboxModel', { + mode: 'SIMPLE', + listeners: { + selectionchange: function(model, selected) { + update_nodefield(selected); + } + } + }); + + // use already cached data to avoid an API call + var data = PVE.data.ResourceStore.getNodes(); + + var store = Ext.create('Ext.data.Store', { + fields: [ 'node', 'mem', 'cpu', 'priority' ], + data: data, + proxy: { + type: 'memory', + reader: {type: 'json'} + }, + sorters: [ + { + property : 'node', + direction: 'ASC' + } + ] + }); + + var nodegrid = Ext.createWidget('grid', { + store: store, + border: true, + height: 300, + selModel: sm, + columns: [ + { + header: gettext('Node'), + flex: 1, + dataIndex: 'node' + }, + { + header: gettext('Memory usage') + " %", + renderer: PVE.Utils.render_mem_usage_percent, + sortable: true, + width: 150, + dataIndex: 'mem' + }, + { + header: gettext('CPU usage'), + renderer: PVE.Utils.render_cpu, + sortable: true, + width: 150, + dataIndex: 'cpu' + }, + { + header: 'Priority', + xtype: 'widgetcolumn', + dataIndex: 'priority', + sortable: true, + stopSelection: true, + widget: { + xtype: 'proxmoxintegerfield', + minValue: 0, + maxValue: 1000, + isFormField: false, + listeners: { + change: function(numberfield, value, old_value) { + var record = numberfield.getWidgetRecord(); + record.set('priority', value); + update_nodefield(sm.getSelection()); + } + } + } + } + ] + }); + + var nodefield = Ext.create('Ext.form.field.Hidden', { + name: 'nodes', + value: '', + listeners: { + change: function (nodefield, value) { + update_node_selection(value); + } + }, + isValid: function () { + var value = nodefield.getValue(); + return (value && 0 !== value.length); + } + }); + + update_node_selection = function(string) { + sm.deselectAll(true); + + string.split(',').forEach(function (e, idx, array) { + var res = e.split(':'); + + store.each(function(record) { + var node = record.get('node'); + + if (node == res[0]) { + sm.select(record, true); + record.set('priority', res[1]); + record.commit(); + } + }); + }); + nodegrid.reconfigure(store); + + }; + + update_nodefield = function(selected) { + var nodes = ''; + var first_iteration = true; + Ext.Array.each(selected, function(record) { + if (!first_iteration) { + nodes += ','; + } + first_iteration = false; + + nodes += record.data.node; + if (record.data.priority) { + nodes += ':' + record.data.priority; + } + }); + + // nodefield change listener calls us again, which results in a + // endless recursion, suspend the event temporary to avoid this + nodefield.suspendEvent('change'); + nodefield.setValue(nodes); + nodefield.resumeEvent('change'); + }; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'group', + value: me.groupId || '', + fieldLabel: 'ID', + vtype: 'StorageId', + allowBlank: false + }, + nodefield + ]; + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'restricted', + uncheckedValue: 0, + fieldLabel: 'restricted' + }, + { + xtype: 'proxmoxcheckbox', + name: 'nofailback', + uncheckedValue: 0, + fieldLabel: 'nofailback' + } + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + }, + nodegrid + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.ha.GroupEdit', { + extend: 'Proxmox.window.Edit', + + groupId: undefined, + + initComponent : function() { + var me = this; + + me.isCreate = !me.groupId; + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/ha/groups'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/ha/groups/' + me.groupId; + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.ha.GroupInputPanel', { + isCreate: me.isCreate, + groupId: me.groupId + }); + + Ext.apply(me, { + subject: gettext('HA Group'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + ipanel.setValues(values); + } + }); + } + } +}); +Ext.define('PVE.ha.GroupsView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAGroupsView'], + + onlineHelp: 'ha_manager_groups', + + stateful: true, + stateId: 'grid-ha-groups', + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + var store = new Ext.data.Store({ + model: 'pve-ha-groups', + sorters: { + property: 'group', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + + var win = Ext.create('PVE.ha.GroupEdit',{ + groupId: rec.data.group + }); + win.on('destroy', reload); + win.show(); + }; + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/ha/groups/', + callback: function() { + reload(); + } + }); + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + Ext.apply(me, { + store: store, + selModel: sm, + viewConfig: { + trackOver: false + }, + tbar: [ + { + text: gettext('Create'), + disabled: !caps.nodes['Sys.Console'], + handler: function() { + var win = Ext.create('PVE.ha.GroupEdit',{}); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn + ], + columns: [ + { + header: gettext('Group'), + width: 150, + sortable: true, + dataIndex: 'group' + }, + { + header: 'restricted', + width: 100, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'restricted' + }, + { + header: 'nofailback', + width: 100, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'nofailback' + }, + { + header: gettext('Nodes'), + flex: 1, + sortable: false, + dataIndex: 'nodes' + }, + { + header: gettext('Comment'), + flex: 1, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment' + } + ], + listeners: { + activate: reload, + beforeselect: function(grid, record, index, eOpts) { + if (!caps.nodes['Sys.Console']) { + return false; + } + }, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.ha.FencingView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveFencingView'], + + onlineHelp: 'ha_manager_fencing', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-ha-fencing', + data: [] + }); + + Ext.apply(me, { + store: store, + stateful: false, + viewConfig: { + trackOver: false, + deferEmptyText: false, + emptyText: 'Use watchdog based fencing.' + }, + columns: [ + { + header: 'Node', + width: 100, + sortable: true, + dataIndex: 'node' + }, + { + header: gettext('Command'), + flex: 1, + dataIndex: 'command' + } + ] + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-ha-fencing', { + extend: 'Ext.data.Model', + fields: [ + 'node', 'command', 'digest' + ] + }); + +}); +Ext.define('PVE.dc.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcSummary', + + scrollable: true, + + bodyPadding: 5, + + layout: 'column', + + defaults: { + padding: 5, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1 + }, + 'width >= 1900': { + columnWidth: 0.5 + } + } + }, + + items: [ + { + itemId: 'dcHealth', + xtype: 'pveDcHealth' + }, + { + itemId: 'dcGuests', + xtype: 'pveDcGuests' + }, + { + title: gettext('Resources'), + xtype: 'panel', + minHeight: 250, + bodyPadding: 5, + layout: 'hbox', + defaults: { + xtype: 'proxmoxGauge', + flex: 1 + }, + items:[ + { + title: gettext('CPU'), + itemId: 'cpu' + }, + { + title: gettext('Memory'), + itemId: 'memory' + }, + { + title: gettext('Storage'), + itemId: 'storage' + } + ] + }, + { + itemId: 'nodeview', + xtype: 'pveDcNodeView', + height: 250 + }, + { + title: gettext('Subscriptions'), + height: 220, + items: [ + { + itemId: 'subscriptions', + xtype: 'pveHealthWidget', + userCls: 'pointer', + listeners: { + element: 'el', + click: function() { + if (this.component.userCls === 'pointer') { + window.open('https://www.proxmox.com/en/proxmox-ve/pricing', '_blank'); + } + } + } + } + ] + } + ], + + initComponent: function() { + var me = this; + + var rstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: 'pve-cluster-status', + model: 'pve-dc-nodes', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/status" + } + }); + + var gridstore = Ext.create('Proxmox.data.DiffStore', { + rstore: rstore, + filters: { + property: 'type', + value: 'node' + }, + sorters: { + property: 'id', + direction: 'ASC' + } + }); + + me.callParent(); + + me.getComponent('nodeview').setStore(gridstore); + + var gueststatus = me.getComponent('dcGuests'); + + var cpustat = me.down('#cpu'); + var memorystat = me.down('#memory'); + var storagestat = me.down('#storage'); + var sp = Ext.state.Manager.getProvider(); + + me.mon(PVE.data.ResourceStore, 'load', function(curstore, results) { + me.suspendLayout = true; + + var cpu = 0; + var maxcpu = 0; + + var nodes = 0; + + var memory = 0; + var maxmem = 0; + + var countedStorages = {}; + var used = 0; + var total = 0; + var usableStorages = {}; + var storages = sp.get('dash-storages') || ''; + storages.split(',').forEach(function(storage){ + if (storage !== '') { + usableStorages[storage] = true; + } + }); + + var qemu = { + running: 0, + paused: 0, + stopped: 0, + template: 0 + }; + var lxc = { + running: 0, + paused: 0, + stopped: 0, + template: 0 + }; + var error = 0; + + var i; + + for (i = 0; i < results.length; i++) { + var item = results[i]; + switch(item.data.type) { + case 'node': + cpu += (item.data.cpu * item.data.maxcpu); + maxcpu += item.data.maxcpu || 0; + memory += item.data.mem || 0; + maxmem += item.data.maxmem || 0; + nodes++; + + // update grid also + var griditem = gridstore.getById(item.data.id); + if (griditem) { + griditem.set('cpuusage', item.data.cpu); + var max = item.data.maxmem || 1; + var val = item.data.mem || 0; + griditem.set('memoryusage', val/max); + griditem.set('uptime', item.data.uptime); + griditem.commit(); //else it marks the fields as dirty + } + break; + case 'storage': + if (!Ext.Object.isEmpty(usableStorages)) { + if (usableStorages[item.data.id] === true) { + used += item.data.disk; + total += item.data.maxdisk; + } + break; + } + if (!countedStorages[item.data.storage] || + (item.data.storage === 'local' && + !countedStorages[item.data.id])) { + used += item.data.disk; + total += item.data.maxdisk; + + countedStorages[item.data.storage === 'local'?item.data.id:item.data.storage] = true; + } + break; + case 'qemu': + qemu[item.data.template ? 'template' : item.data.status]++; + if (item.data.hastate === 'error') { + error++; + } + break; + case 'lxc': + lxc[item.data.template ? 'template' : item.data.status]++; + if (item.data.hastate === 'error') { + error++; + } + break; + default: break; + } + } + + var text = Ext.String.format(gettext('of {0} CPU(s)'), maxcpu); + cpustat.updateValue((cpu/maxcpu), text); + + text = Ext.String.format(gettext('{0} of {1}'), PVE.Utils.render_size(memory), PVE.Utils.render_size(maxmem)); + memorystat.updateValue((memory/maxmem), text); + + text = Ext.String.format(gettext('{0} of {1}'), PVE.Utils.render_size(used), PVE.Utils.render_size(total)); + storagestat.updateValue((used/total), text); + + gueststatus.updateValues(qemu,lxc,error); + + me.suspendLayout = false; + me.updateLayout(true); + }); + + var dcHealth = me.getComponent('dcHealth'); + me.mon(rstore, 'load', dcHealth.updateStatus, dcHealth); + + var subs = me.down('#subscriptions'); + me.mon(rstore, 'load', function(store, records, success) { + var i; + var level; + var mixed = false; + for (i = 0; i < records.length; i++) { + if (records[i].get('type') !== 'node') { + continue; + } + var node = records[i]; + if (node.get('status') === 'offline') { + continue; + } + + var curlevel = node.get('level'); + + if (curlevel === '') { // no subscription trumps all, set and break + level = ''; + break; + } + + if (level === undefined) { // save level + level = curlevel; + } else if (level !== curlevel) { // detect different levels + mixed = true; + } + } + + var data = { + title: Proxmox.Utils.unknownText, + text: Proxmox.Utils.unknownText, + iconCls: PVE.Utils.get_health_icon(undefined, true) + }; + if (level === '') { + data = { + title: gettext('No Subscription'), + iconCls: PVE.Utils.get_health_icon('critical', true), + text: gettext('You have at least one node without subscription.') + }; + subs.setUserCls('pointer'); + } else if (mixed) { + data = { + title: gettext('Mixed Subscriptions'), + iconCls: PVE.Utils.get_health_icon('warning', true), + text: gettext('Warning: Your subscription levels are not the same.') + }; + subs.setUserCls('pointer'); + } else if (level) { + data = { + title: PVE.Utils.render_support_level(level), + iconCls: PVE.Utils.get_health_icon('good', true), + text: gettext('Your subscription status is valid.') + }; + subs.setUserCls(''); + } + + subs.setData(data); + }); + + me.on('destroy', function(){ + rstore.stopUpdate(); + }); + + rstore.startUpdate(); + } + +}); +Ext.define('PVE.window.ReplicaEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveReplicaEdit', + + subject: gettext('Replication Job'), + + + url: '/cluster/replication', + method: 'POST', + + initComponent: function() { + var me = this; + + var vmid = me.pveSelNode.data.vmid; + var nodename = me.pveSelNode.data.node; + + var items = []; + + items.push({ + xtype: (me.isCreate && !vmid)?'pveGuestIDSelector':'displayfield', + name: 'guest', + fieldLabel: 'CT/VM ID', + value: vmid || '' + }); + + items.push( + { + xtype: me.isCreate ? 'pveNodeSelector':'displayfield', + name: 'target', + disallowedNodes: [nodename], + allowBlank: false, + onlineValidator: true, + fieldLabel: gettext("Target") + }, + { + xtype: 'pveCalendarEvent', + fieldLabel: gettext('Schedule'), + emptyText: '*/15 - ' + Ext.String.format(gettext('Every {0} minutes'), 15), + name: 'schedule' + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Rate limit') + ' (MB/s)', + step: 1, + minValue: 1, + emptyText: gettext('unlimited'), + name: 'rate' + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment' + }, + { + xtype: 'proxmoxcheckbox', + name: 'enabled', + defaultValue: 'on', + checked: true, + fieldLabel: gettext('Enabled') + } + ); + + me.items = [ + { + xtype: 'inputpanel', + itemId: 'ipanel', + onlineHelp: 'pvesr_schedule_time_format', + + onGetValues: function(values) { + var me = this.up('window'); + + values.disable = values.enabled ? 0 : 1; + delete values.enabled; + + PVE.Utils.delete_if_default(values, 'rate', '', me.isCreate); + PVE.Utils.delete_if_default(values, 'disable', 0, me.isCreate); + PVE.Utils.delete_if_default(values, 'schedule', '*/15', me.isCreate); + PVE.Utils.delete_if_default(values, 'comment', '', me.isCreate); + + if (me.isCreate) { + values.type = 'local'; + var vm = vmid || values.guest; + var id = -1; + if (me.highestids[vm] !== undefined) { + id = me.highestids[vm]; + } + id++; + values.id = vm + '-' + id.toString(); + delete values.guest; + } + return values; + }, + items: items + } + ]; + + me.callParent(); + + if (me.isCreate) { + me.load({ + success: function(response) { + var jobs = response.result.data; + var highestids = {}; + Ext.Array.forEach(jobs, function(job) { + var match = /^([0-9]+)\-([0-9]+)$/.exec(job.id); + if (match) { + var vmid = parseInt(match[1],10); + var id = parseInt(match[2],10); + if (highestids[vmid] < id || + highestids[vmid] === undefined) { + highestids[vmid] = id; + } + } + }); + + me.highestids = highestids; + } + }); + + } else { + me.load({ + success: function(response, options) { + response.result.data.enabled = !response.result.data.disable; + me.setValues(response.result.data); + me.digest = response.result.data.digest; + } + }); + } + } +}); + +/*jslint confusion: true */ +/* callback is a function and string */ +Ext.define('PVE.grid.ReplicaView', { + extend: 'Ext.grid.Panel', + xtype: 'pveReplicaView', + + onlineHelp: 'chapter_pvesr', + + stateful: true, + stateId: 'grid-pve-replication-status', + + controller: { + xclass: 'Ext.app.ViewController', + + addJob: function(button,event,rec) { + var me = this.getView(); + var controller = this; + var win = Ext.create('PVE.window.ReplicaEdit', { + isCreate: true, + method: 'POST', + pveSelNode: me.pveSelNode + }); + win.on('destroy', function() { controller.reload(); }); + win.show(); + }, + + editJob: function(button,event,rec) { + var me = this.getView(); + var controller = this; + var data = rec.data; + var win = Ext.create('PVE.window.ReplicaEdit', { + url: '/cluster/replication/' + data.id, + method: 'PUT', + pveSelNode: me.pveSelNode + }); + win.on('destroy', function() { controller.reload(); }); + win.show(); + }, + + scheduleJobNow: function(button,event,rec) { + var me = this.getView(); + var controller = this; + + Proxmox.Utils.API2Request({ + url: "/api2/extjs/nodes/" + me.nodename + "/replication/" + rec.data.id + "/schedule_now", + method: 'POST', + waitMsgTarget: me, + callback: function() { controller.reload(); }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + showLog: function(button, event, rec) { + var me = this.getView(); + var controller = this; + var logView = Ext.create('Proxmox.panel.LogView', { + border: false, + url: "/api2/extjs/nodes/" + me.nodename + "/replication/" + rec.data.id + "/log" + }); + var win = Ext.create('Ext.window.Window', { + items: [ logView ], + layout: 'fit', + width: 800, + height: 400, + modal: true, + title: gettext("Replication Log") + }); + var task = { + run: function() { + logView.requestUpdate(); + }, + interval: 1000 + }; + Ext.TaskManager.start(task); + win.on('destroy', function() { + Ext.TaskManager.stop(task); + controller.reload(); + }); + win.show(); + }, + + reload: function() { + var me = this.getView(); + me.rstore.load(); + }, + + dblClick: function(grid, record, item) { + var me = this; + me.editJob(undefined, undefined, record); + }, + + // check for cluster + // currently replication is for cluster only, so we disable the whole + // component + checkPrerequisites: function() { + var me = this.getView(); + if (PVE.data.ResourceStore.getNodes().length < 2) { + me.mask(gettext("Replication needs at least two nodes"), ['pve-static-mask']); + } + }, + + control: { + '#': { + itemdblclick: 'dblClick', + afterlayout: 'checkPrerequisites' + } + } + }, + + tbar: [ + { + text: gettext('Add'), + itemId: 'addButton', + handler: 'addJob' + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + itemId: 'editButton', + handler: 'editJob', + disabled: true + }, + { + xtype: 'proxmoxStdRemoveButton', + itemId: 'removeButton', + baseurl: '/api2/extjs/cluster/replication/', + dangerous: true, + callback: 'reload' + }, + { + xtype: 'proxmoxButton', + text: gettext('Log'), + itemId: 'logButton', + handler: 'showLog', + disabled: true + }, + { + xtype: 'proxmoxButton', + text: gettext('Schedule now'), + itemId: 'scheduleNowButton', + handler: 'scheduleJobNow', + disabled: true + } + ], + + initComponent: function() { + var me = this; + var mode = ''; + var url = '/cluster/replication'; + + me.nodename = me.pveSelNode.data.node; + me.vmid = me.pveSelNode.data.vmid; + + me.columns = [ + { + text: gettext('Enabled'), + dataIndex: 'enabled', + xtype: 'checkcolumn', + sortable: true, + disabled: true + }, + { + text: 'ID', + dataIndex: 'id', + width: 60, + hidden: true + }, + { + text: gettext('Guest'), + dataIndex: 'guest', + width: 75 + }, + { + text: gettext('Job'), + dataIndex: 'jobnum', + width: 60 + }, + { + text: gettext('Target'), + dataIndex: 'target' + } + ]; + + if (!me.nodename) { + mode = 'dc'; + me.stateId = 'grid-pve-replication-dc'; + } else if (!me.vmid) { + mode = 'node'; + url = '/nodes/' + me.nodename + '/replication'; + } else { + mode = 'vm'; + url = '/nodes/' + me.nodename + '/replication' + '?guest=' + me.vmid; + } + + if (mode !== 'dc') { + me.columns.push( + { + text: gettext('Status'), + dataIndex: 'state', + minWidth: 160, + flex: 1, + renderer: function(value, metadata, record) { + + if (record.data.pid) { + metadata.tdCls = 'x-grid-row-loading'; + return ''; + } + + var icons = []; + var states = []; + + if (record.data.remove_job) { + icons.push(''); + states.push(gettext("Removal Scheduled")); + } + + if (record.data.error) { + icons.push(''); + states.push(record.data.error); + } + + if (icons.length == 0) { + icons.push(''); + states.push(gettext('OK')); + } + + return icons.join(',') + ' ' + states.join(','); + } + }, + { + text: gettext('Last Sync'), + dataIndex: 'last_sync', + width: 150, + renderer: function(value, metadata, record) { + if (!value) { + return '-'; + } + + if (record.data.pid) { + return gettext('syncing'); + } + + return Proxmox.Utils.render_timestamp(value); + } + }, + { + text: gettext('Duration'), + dataIndex: 'duration', + width: 60, + renderer: PVE.Utils.render_duration + }, + { + text: gettext('Next Sync'), + dataIndex: 'next_sync', + width: 150, + renderer: function(value) { + if (!value) { + return '-'; + } + + var now = new Date(); + var next = new Date(value*1000); + + if (next < now) { + return gettext('pending'); + } + + return Proxmox.Utils.render_timestamp(value); + } + } + ); + } + + me.columns.push( + { + text: gettext('Schedule'), + width: 75, + dataIndex: 'schedule' + }, + { + text: gettext('Rate limit'), + dataIndex: 'rate', + renderer: function(value) { + if (!value) { + return gettext('unlimited'); + } + + return value.toString() + ' MB/s'; + }, + hidden: true + }, + { + text: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.htmlEncode + } + ); + + me.rstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-replica-' + me.nodename + me.vmid, + model: (mode === 'dc')? 'pve-replication' : 'pve-replication-state', + interval: 3000, + proxy: { + type: 'proxmox', + url: "/api2/json" + url + } + }); + + me.store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + sorters: [ + { + property: 'guest' + }, + { + property: 'jobnum' + } + ] + }); + + me.callParent(); + + // we cannot access the log and scheduleNow button + // in the datacenter, because + // we do not know where/if the jobs runs + if (mode === 'dc') { + me.down('#logButton').setHidden(true); + me.down('#scheduleNowButton').setHidden(true); + } + + // if we set the warning mask, we do not want to load + // or set the mask on store errors + if (PVE.data.ResourceStore.getNodes().length < 2) { + return; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + me.on('destroy', me.rstore.stopUpdate); + me.rstore.startUpdate(); + } +}, function() { + + Ext.define('pve-replication', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'target', 'comment', 'rate', 'type', + { name: 'guest', type: 'integer' }, + { name: 'jobnum', type: 'integer' }, + { name: 'schedule', defaultValue: '*/15' }, + { name: 'disable', defaultValue: '' }, + { name: 'enabled', calculate: function(data) { return !data.disable; } } + ] + }); + + Ext.define('pve-replication-state', { + extend: 'pve-replication', + fields: [ + 'last_sync', 'next_sync', 'error', 'duration', 'state', + 'fail_count', 'remove_job', 'pid' + ] + }); + +}); +Ext.define('PVE.dc.Health', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcHealth', + + title: gettext('Health'), + + bodyPadding: 10, + height: 220, + layout: { + type: 'hbox', + align: 'stretch' + }, + + defaults: { + flex: 1, + xtype: 'box', + style: { + 'text-align':'center' + } + }, + + nodeList: [], + nodeIndex: 0, + + updateStatus: function(store, records, success) { + var me = this; + if (!success) { + return; + } + + var cluster = { + iconCls: PVE.Utils.get_health_icon('good', true), + text: gettext("Standalone node - no cluster defined") + }; + + var nodes = { + online: 0, + offline: 0 + }; + + // by default we have one node + var numNodes = 1; + var i; + + for (i = 0; i < records.length; i++) { + var item = records[i]; + if (item.data.type === 'node') { + nodes[item.data.online === 1 ? 'online':'offline']++; + } else if(item.data.type === 'cluster') { + cluster.text = gettext("Cluster") + ": "; + cluster.text += item.data.name + ", "; + cluster.text += gettext("Quorate") + ": "; + cluster.text += Proxmox.Utils.format_boolean(item.data.quorate); + if (item.data.quorate != 1) { + cluster.iconCls = PVE.Utils.get_health_icon('critical', true); + } + + numNodes = item.data.nodes; + } + } + + if (numNodes !== (nodes.online + nodes.offline)) { + nodes.offline = numNodes - nodes.online; + } + + me.getComponent('clusterstatus').updateHealth(cluster); + me.getComponent('nodestatus').update(nodes); + }, + + updateCeph: function(store, records, success) { + var me = this; + var cephstatus = me.getComponent('ceph'); + if (!success || records.length < 1) { + + // if ceph status is already visible + // don't stop to update + if (cephstatus.isVisible()) { + return; + } + + // try all nodes until we either get a successful api call, + // or we tried all nodes + if (++me.nodeIndex >= me.nodeList.length) { + me.cephstore.stopUpdate(); + } else { + store.getProxy().setUrl('/api2/json/nodes/' + me.nodeList[me.nodeIndex].node + '/ceph/status'); + } + + return; + } + + var state = PVE.Utils.render_ceph_health(records[0].data.health || {}); + cephstatus.updateHealth(state); + cephstatus.setVisible(true); + }, + + listeners: { + destroy: function() { + var me = this; + me.cephstore.stopUpdate(); + } + }, + + items: [ + { + itemId: 'clusterstatus', + xtype: 'pveHealthWidget', + title: gettext('Status') + }, + { + itemId: 'nodestatus', + data: { + online: 0, + offline: 0 + }, + tpl: [ + '

' + gettext('Nodes') + '


', + '
', + '
', + ' ', + gettext('Online'), + '
', + '
{online}
', + '

', + '
', + ' ', + gettext('Offline'), + '
', + '
{offline}
', + '
' + ] + }, + { + itemId: 'ceph', + width: 250, + columnWidth: undefined, + userCls: 'pointer', + title: 'Ceph', + xtype: 'pveHealthWidget', + hidden: true, + listeners: { + element: 'el', + click: function() { + var sp = Ext.state.Manager.getProvider(); + sp.set('dctab', {value:'ceph'}, true); + } + } + } + ], + + initComponent: function() { + var me = this; + + me.nodeList = PVE.data.ResourceStore.getNodes(); + me.nodeIndex = 0; + me.cephstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: 'pve-cluster-ceph', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodeList[me.nodeIndex].node + '/ceph/status' + } + }); + me.callParent(); + me.mon(me.cephstore, 'load', me.updateCeph, me); + me.cephstore.startUpdate(); + } +}); +Ext.define('PVE.dc.Guests', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcGuests', + + + title: gettext('Guests'), + height: 220, + layout: { + type: 'table', + columns: 2, + tableAttrs: { + style: { + width: '100%' + } + } + }, + bodyPadding: '0 20 20 20', + + defaults: { + xtype: 'box', + padding: '0 50 0 50', + style: { + 'text-align':'center', + 'line-height':'1.2' + } + }, + items: [{ + itemId: 'qemu', + data: { + running: 0, + paused: 0, + stopped: 0, + template: 0 + }, + tpl: [ + '

' + gettext("Virtual Machines") + '

', + '
', + ' ', + gettext('Running'), + '
', + '
{running}
' + '
', + '', + '
', + ' ', + gettext('Paused'), + '
', + '
{paused}
' + '
', + '
', + '
', + ' ', + gettext('Stopped'), + '
', + '
{stopped}
' + '
', + '', + '
', + ' ', + gettext('Templates'), + '
', + '
{template}
', + '
' + ] + },{ + itemId: 'lxc', + data: { + running: 0, + paused: 0, + stopped: 0, + template: 0 + }, + tpl: [ + '

' + gettext("LXC Container") + '

', + '
', + ' ', + gettext('Running'), + '
', + '
{running}
' + '
', + '', + '
', + ' ', + gettext('Paused'), + '
', + '
{paused}
' + '
', + '
', + '
', + ' ', + gettext('Stopped'), + '
', + '
{stopped}
' + '
', + '', + '
', + ' ', + gettext('Templates'), + '
', + '
{template}
', + '
' + ] + },{ + itemId: 'error', + colspan: 2, + data: { + num: 0 + }, + columnWidth: 1, + padding: '10 250 0 250', + tpl: [ + '', + '
', + ' ', + gettext('Error'), + '
', + '
{num}
', + '
' + ] + }], + + updateValues: function(qemu, lxc, error) { + var me = this; + me.getComponent('qemu').update(qemu); + me.getComponent('lxc').update(lxc); + me.getComponent('error').update({num: error}); + } +}); + /*jslint confusion: true*/ +Ext.define('PVE.dc.OptionView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveDcOptionView'], + + onlineHelp: 'datacenter_configuration_file', + + monStoreErrors: true, + + add_inputpanel_row: function(name, text, opts) { + var me = this; + + opts = opts || {}; + me.rows = me.rows || {}; + + var canEdit = (opts.caps === undefined || opts.caps); + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue, + header: text, + renderer: opts.renderer, + editor: canEdit ? { + xtype: 'proxmoxWindowEdit', + width: 350, + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + setValues: function(values) { + // FIXME: run through parsePropertyString if not an object? + var edit_value = values[name]; + Ext.Array.each(this.query('inputpanel'), function(panel) { + panel.setValues(edit_value); + }); + }, + url: opts.url, + items: [{ + xtype: 'inputpanel', + onGetValues: function(values) { + if (values === undefined || Object.keys(values).length === 0) { + return { 'delete': name }; + } + var ret_val = {}; + ret_val[name] = PVE.Parser.printPropertyString(values); + return ret_val; + }, + items: opts.items + }] + } : undefined + }; + }, + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + me.add_combobox_row('keyboard', gettext('Keyboard Layout'), { + renderer: PVE.Utils.render_kvm_language, + comboItems: PVE.Utils.kvm_keymap_array(), + defaultValue: '__default__', + deleteEmpty: true + }); + me.add_text_row('http_proxy', gettext('HTTP proxy'), { + defaultValue: Proxmox.Utils.noneText, + vtype: 'HttpProxy', + deleteEmpty: true + }); + me.add_combobox_row('console', gettext('Console Viewer'), { + renderer: PVE.Utils.render_console_viewer, + comboItems: PVE.Utils.console_viewer_array(), + defaultValue: '__default__', + deleteEmpty: true + }); + me.add_text_row('email_from', gettext('Email from address'), { + deleteEmpty: true, + vtype: 'proxmoxMail', + defaultValue: 'root@$hostname' + }); + me.add_text_row('mac_prefix', gettext('MAC address prefix'), { + deleteEmpty: true, + vtype: 'MacPrefix', + defaultValue: Proxmox.Utils.noneText + }); + me.add_inputpanel_row('migration', gettext('Migration Settings'), { + renderer: PVE.Utils.render_dc_ha_opts, + caps: caps.vms['Sys.Modify'], + labelWidth: 120, + url: "/api2/extjs/cluster/options", + defaultKey: 'type', + items: [{ + xtype: 'displayfield', + name: 'type', + fieldLabel: gettext('Type'), + value: 'secure', + submitValue: true, + }, { + xtype: 'proxmoxNetworkSelector', + name: 'network', + fieldLabel: gettext('Network'), + value: null, + emptyText: Proxmox.Utils.defaultText, + autoSelect: false, + skipEmptyText: true + }] + }); + me.add_inputpanel_row('ha', gettext('HA Settings'), { + renderer: PVE.Utils.render_dc_ha_opts, + caps: caps.vms['Sys.Modify'], + labelWidth: 120, + url: "/api2/extjs/cluster/options", + items: [{ + xtype: 'proxmoxKVComboBox', + name: 'shutdown_policy', + fieldLabel: gettext('Shutdown Policy'), + deleteEmpty: false, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (conditional)' ], + ['freeze', 'freeze'], + ['failover', 'failover'], + ['conditional', 'conditional'] + ], + defaultValue: '__default__' + }] + }); + + // TODO: bwlimits, u2f? + + me.selModel = Ext.create('Ext.selection.RowModel', {}); + + Ext.apply(me, { + tbar: [{ + text: gettext('Edit'), + xtype: 'proxmoxButton', + disabled: true, + handler: function() { me.run_editor(); }, + selModel: me.selModel + }], + url: "/api2/json/cluster/options", + editorConfig: { + url: "/api2/extjs/cluster/options" + }, + interval: 5000, + cwidth1: 200, + listeners: { + itemdblclick: me.run_editor + } + }); + + me.callParent(); + + // set the new value for the default console + me.mon(me.rstore, 'load', function(store, records, success) { + if (!success) { + return; + } + + var rec = store.getById('console'); + PVE.VersionInfo.console = rec.data.value; + if (rec.data.value === '__default__') { + delete PVE.VersionInfo.console; + } + }); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + } +}); +Ext.define('PVE.dc.StorageView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveStorageView'], + + onlineHelp: 'chapter_storage', + + stateful: true, + stateId: 'grid-dc-storage', + + createStorageEditWindow: function(type, sid) { + var schema = PVE.Utils.storageSchema[type]; + if (!schema || !schema.ipanel) { + throw "no editor registered for storage type: " + type; + } + + Ext.create('PVE.storage.BaseEdit', { + paneltype: 'PVE.storage.' + schema.ipanel, + type: type, + storageId: sid, + autoShow: true, + listeners: { + destroy: this.reloadStore + } + }); + }, + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-storage', + proxy: { + type: 'proxmox', + url: "/api2/json/storage" + }, + sorters: { + property: 'storage', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var type = rec.data.type, + sid = rec.data.storage; + + me.createStorageEditWindow(type, sid); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/storage/', + callback: reload + }); + + // else we cannot dynamically generate the add menu handlers + var addHandleGenerator = function(type) { + return function() { me.createStorageEditWindow(type); }; + }; + var addMenuItems = [], type; + /*jslint forin: true */ + for (type in PVE.Utils.storageSchema) { + var storage = PVE.Utils.storageSchema[type]; + if (storage.hideAdd) { + continue; + } + addMenuItems.push({ + text: PVE.Utils.format_storage_type(type), + iconCls: 'fa fa-fw fa-' + storage.faIcon, + handler: addHandleGenerator(type) + }); + } + + Ext.apply(me, { + store: store, + reloadStore: reload, + selModel: sm, + viewConfig: { + trackOver: false + }, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: addMenuItems + }) + }, + remove_btn, + edit_btn + ], + columns: [ + { + header: 'ID', + flex: 2, + sortable: true, + dataIndex: 'storage' + }, + { + header: gettext('Type'), + flex: 1, + sortable: true, + dataIndex: 'type', + renderer: PVE.Utils.format_storage_type + }, + { + header: gettext('Content'), + flex: 3, + sortable: true, + dataIndex: 'content', + renderer: PVE.Utils.format_content_types + }, + { + header: gettext('Path') + '/' + gettext('Target'), + flex: 2, + sortable: true, + dataIndex: 'path', + renderer: function(value, metaData, record) { + if (record.data.target) { + return record.data.target; + } + return value; + } + }, + { + header: gettext('Shared'), + flex: 1, + sortable: true, + dataIndex: 'shared', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('Enabled'), + flex: 1, + sortable: true, + dataIndex: 'disable', + renderer: Proxmox.Utils.format_neg_boolean + }, + { + header: gettext('Bandwidth Limit'), + flex: 2, + sortable: true, + dataIndex: 'bwlimit' + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-storage', { + extend: 'Ext.data.Model', + fields: [ + 'path', 'type', 'content', 'server', 'portal', 'target', 'export', 'storage', + { name: 'shared', type: 'boolean'}, + { name: 'disable', type: 'boolean'} + ], + idProperty: 'storage' + }); + +}); +/*global u2f,QRCode,Uint8Array*/ +/*jslint confusion: true*/ +Ext.define('PVE.window.TFAEdit', { + extend: 'Ext.window.Window', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'pveum_tfa_auth', // fake to ensure this gets a link target + + modal: true, + resizable: false, + title: gettext('Two Factor Authentication'), + subject: 'TFA', + url: '/api2/extjs/access/tfa', + width: 512, + + layout: { + type: 'vbox', + align: 'stretch' + }, + + updateQrCode: function() { + var me = this; + var values = me.lookup('totp_form').getValues(); + var algorithm = values.algorithm; + if (!algorithm) { + algorithm = 'SHA1'; + } + + me.qrcode.makeCode( + 'otpauth://totp/' + encodeURIComponent(me.userid) + + '?secret=' + values.secret + + '&period=' + values.step + + '&digits=' + values.digits + + '&algorithm=' + algorithm + + '&issuer=' + encodeURIComponent(values.issuer) + ); + + me.lookup('challenge').setVisible(true); + me.down('#qrbox').setVisible(true); + }, + + showError: function(error) { + Ext.Msg.alert( + gettext('Error'), + PVE.Utils.render_u2f_error(error) + ); + }, + + doU2FChallenge: function(response) { + var me = this; + + var data = response.result.data; + me.lookup('password').setDisabled(true); + var msg = Ext.Msg.show({ + title: 'U2F: '+gettext('Setup'), + message: gettext('Please press the button on your U2F Device'), + buttons: [] + }); + Ext.Function.defer(function() { + u2f.register(data.appId, [data], [], function(data) { + msg.close(); + if (data.errorCode) { + me.showError(data.errorCode); + } else { + me.respondToU2FChallenge(data); + } + }); + }, 500, me); + }, + + respondToU2FChallenge: function(data) { + var me = this; + var params = { + userid: me.userid, + action: 'confirm', + response: JSON.stringify(data) + }; + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + success: function() { + me.close(); + Ext.Msg.show({ + title: gettext('Success'), + message: gettext('U2F Device successfully connected.'), + buttons: Ext.Msg.OK + }); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + viewModel: { + data: { + in_totp_tab: true, + tfa_required: false, + tfa_type: null, // dependencies of formulas should not be undefined + valid: false, + u2f_available: true + }, + formulas: { + canDeleteTFA: function(get) { + return (get('tfa_type') !== null && !get('tfa_required')); + }, + canSetupTOTP: function(get) { + var tfa = get('tfa_type'); + return (tfa === null || tfa === 'totp' || tfa === 1); + }, + canSetupU2F: function(get) { + var tfa = get('tfa_type'); + return (get('u2f_available') && (tfa === null || tfa === 'u2f' || tfa === 1)); + } + } + }, + + afterLoading: function(realm_tfa_type, user_tfa_type) { + var me = this; + var viewmodel = me.getViewModel(); + if (user_tfa_type === 'oath') { + user_tfa_type = 'totp'; + } + viewmodel.set('tfa_type', user_tfa_type || null); + if (!realm_tfa_type) { + // There's no TFA enforced by the realm, everything works. + viewmodel.set('u2f_available', true); + viewmodel.set('tfa_required', false); + } else if (realm_tfa_type === 'oath') { + // The realm explicitly requires TOTP + if (user_tfa_type !== 'totp' && user_tfa_type !== null) { + // user had a different tfa method, so + // we have to change back to the totp tab and + // generate a secret + viewmodel.set('tfa_type', null); + me.lookup('tfatabs').setActiveTab(me.lookup('totp_panel')); + me.getController().randomizeSecret(); + } + viewmodel.set('tfa_required', true); + viewmodel.set('u2f_available', false); + } else { + // The realm enforces some other TFA type (yubico) + me.close(); + Ext.Msg.alert( + gettext('Error'), + Ext.String.format( + gettext("Custom 2nd factor configuration is not supported on realms with '{0}' TFA."), + realm_tfa_type + ) + ); + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'field[qrupdate=true]': { + change: function() { + var me = this.getView(); + me.updateQrCode(); + } + }, + 'field': { + validitychange: function(field, valid) { + var me = this; + var viewModel = me.getViewModel(); + var form = me.lookup('totp_form'); + var challenge = me.lookup('challenge'); + var password = me.lookup('password'); + viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid()); + } + }, + '#': { + show: function() { + var me = this.getView(); + var viewmodel = this.getViewModel(); + + var loadMaskContainer = me.down('#tfatabs'); + Proxmox.Utils.API2Request({ + url: '/access/users/' + encodeURIComponent(me.userid) + '/tfa', + waitMsgTarget: loadMaskContainer, + method: 'GET', + success: function(response, opts) { + var data = response.result.data; + me.afterLoading(data.realm, data.user); + }, + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(loadMaskContainer, response.htmlStatus); + } + }); + + me.qrdiv = document.createElement('center'); + me.qrcode = new QRCode(me.qrdiv, { + width: 256, + height: 256, + correctLevel: QRCode.CorrectLevel.M + }); + me.down('#qrbox').getEl().appendChild(me.qrdiv); + + viewmodel.set('tfa_type', me.tfa_type || null); + if (!me.tfa_type) { + this.randomizeSecret(); + } else { + me.down('#qrbox').setVisible(false); + me.lookup('challenge').setVisible(false); + if (me.tfa_type === 'u2f') { + var u2f_panel = me.lookup('u2f_panel'); + me.lookup('tfatabs').setActiveTab(u2f_panel); + } + } + + if (Proxmox.UserName === 'root@pam') { + me.lookup('password').setVisible(false); + me.lookup('password').setDisabled(true); + } + } + }, + '#tfatabs': { + tabchange: function(panel, newcard) { + var viewmodel = this.getViewModel(); + viewmodel.set('in_totp_tab', newcard.itemId === 'totp-panel'); + } + } + }, + + applySettings: function() { + var me = this; + var values = me.lookup('totp_form').getValues(); + var params = { + userid: me.getView().userid, + action: 'new', + key: values.secret, + config: PVE.Parser.printPropertyString({ + type: 'oath', + digits: values.digits, + step: values.step + }), + // this is used to verify that the client generates the correct codes: + response: me.lookup('challenge').value + }; + + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + waitMsgTarget: me.getView(), + success: function(response, opts) { + me.getView().close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + deleteTFA: function() { + var me = this; + var values = me.lookup('totp_form').getValues(); + var params = { + userid: me.getView().userid, + action: 'delete' + }; + + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + waitMsgTarget: me.getView(), + success: function(response, opts) { + me.getView().close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + randomizeSecret: function() { + var me = this; + var rnd = new Uint8Array(16); + window.crypto.getRandomValues(rnd); + var data = ''; + rnd.forEach(function(b) { + // secret must be base32, so just use the first 5 bits + b = b & 0x1f; + if (b < 26) { + // A..Z + data += String.fromCharCode(b + 0x41); + } else { + // 2..7 + data += String.fromCharCode(b-26 + 0x32); + } + }); + me.lookup('tfa_secret').setValue(data); + }, + + startU2FRegistration: function() { + var me = this; + + var params = { + userid: me.getView().userid, + action: 'new' + }; + + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + waitMsgTarget: me.getView(), + success: function(response) { + me.getView().doU2FChallenge(response); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }, + + items: [ + { + xtype: 'tabpanel', + itemId: 'tfatabs', + reference: 'tfatabs', + border: false, + items: [ + { + xtype: 'panel', + title: 'TOTP', + itemId: 'totp-panel', + reference: 'totp_panel', + tfa_type: 'totp', + border: false, + bind: { + disabled: '{!canSetupTOTP}' + }, + layout: { + type: 'vbox', + align: 'stretch' + }, + items: [ + { + xtype: 'form', + layout: 'anchor', + border: false, + reference: 'totp_form', + fieldDefaults: { + anchor: '100%', + padding: '0 5' + }, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('User name'), + cbind: { + value: '{userid}' + } + }, + { + layout: 'hbox', + border: false, + padding: '0 0 5 0', + items: [{ + xtype: 'textfield', + fieldLabel: gettext('Secret'), + emptyText: gettext('Unchanged'), + name: 'secret', + reference: 'tfa_secret', + regex: /^[A-Z2-7=]+$/, + regexText: 'Must be base32 [A-Z2-7=]', + maskRe: /[A-Z2-7=]/, + qrupdate: true, + flex: 4 + }, + { + xtype: 'button', + text: gettext('Randomize'), + reference: 'randomize_button', + handler: 'randomizeSecret', + flex: 1 + }] + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Time period'), + name: 'step', + // Google Authenticator ignores this and generates bogus data + hidden: true, + value: 30, + minValue: 10, + qrupdate: true + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Digits'), + name: 'digits', + value: 6, + // Google Authenticator ignores this and generates bogus data + hidden: true, + minValue: 6, + maxValue: 8, + qrupdate: true + }, + { + xtype: 'textfield', + fieldLabel: gettext('Issuer Name'), + name: 'issuer', + value: 'Proxmox Web UI', + qrupdate: true + } + ] + }, + { + xtype: 'box', + itemId: 'qrbox', + visible: false, // will be enabled when generating a qr code + style: { + 'background-color': '#23272a', + padding: '5px', + width: '266px', + height: '266px' + } + }, + { + xtype: 'textfield', + fieldLabel: gettext('Verification Code'), + allowBlank: false, + reference: 'challenge', + padding: '0 5', + emptyText: gettext('Scan QR code and enter TOTP auth. code to verify') + } + ] + }, + { + title: 'U2F', + itemId: 'u2f-panel', + reference: 'u2f_panel', + tfa_type: 'u2f', + border: false, + padding: '5 5', + layout: { + type: 'vbox', + align: 'middle' + }, + bind: { + disabled: '{!canSetupU2F}' + }, + items: [ + { + xtype: 'label', + width: 500, + text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.') + } + ] + } + ] + }, + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Password'), + minLength: 5, + reference: 'password', + allowBlank: false, + validateBlank: true, + padding: '0 0 5 5', + emptyText: gettext('verify current password') + } + ], + + buttons: [ + { + xtype: 'proxmoxHelpButton' + }, + '->', + { + text: gettext('Apply'), + handler: 'applySettings', + bind: { + hidden: '{!in_totp_tab}', + disabled: '{!valid}' + } + }, + { + xtype: 'button', + text: gettext('Register U2F Device'), + handler: 'startU2FRegistration', + bind: { + hidden: '{in_totp_tab}', + disabled: '{tfa_type}' + } + }, + { + text: gettext('Delete'), + reference: 'delete_button', + disabled: true, + handler: 'deleteTFA', + bind: { + disabled: '{!canDeleteTFA}' + } + } + ], + + initComponent: function() { + var me = this; + + if (!me.userid) { + throw "no userid given"; + } + + me.callParent(); + + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', 'pveum_tfa_auth'); + } +}); +Ext.define('PVE.dc.UserEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcUserEdit'], + + isAdd: true, + + initComponent : function() { + var me = this; + + me.isCreate = !me.userid; + + var url; + var method; + var realm; + + if (me.isCreate) { + url = '/api2/extjs/access/users'; + method = 'POST'; + } else { + url = '/api2/extjs/access/users/' + encodeURIComponent(me.userid); + method = 'PUT'; + } + + var verifypw; + var pwfield; + + var validate_pw = function() { + if (verifypw.getValue() !== pwfield.getValue()) { + return gettext("Passwords do not match"); + } + return true; + }; + + verifypw = Ext.createWidget('textfield', { + inputType: 'password', + fieldLabel: gettext('Confirm password'), + name: 'verifypassword', + submitValue: false, + disabled: true, + hidden: true, + validator: validate_pw + }); + + pwfield = Ext.createWidget('textfield', { + inputType: 'password', + fieldLabel: gettext('Password'), + minLength: 5, + name: 'password', + disabled: true, + hidden: true, + validator: validate_pw + }); + + var update_passwd_field = function(realm) { + if (realm === 'pve') { + pwfield.setVisible(true); + pwfield.setDisabled(false); + verifypw.setVisible(true); + verifypw.setDisabled(false); + } else { + pwfield.setVisible(false); + pwfield.setDisabled(true); + verifypw.setVisible(false); + verifypw.setDisabled(true); + } + + }; + + var column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'userid', + fieldLabel: gettext('User name'), + value: me.userid, + allowBlank: false, + submitValue: me.isCreate ? true : false + }, + pwfield, verifypw, + { + xtype: 'pveGroupSelector', + name: 'groups', + multiSelect: true, + allowBlank: true, + fieldLabel: gettext('Group') + }, + { + xtype: 'datefield', + name: 'expire', + emptyText: 'never', + format: 'Y-m-d', + submitFormat: 'U', + fieldLabel: gettext('Expire') + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enabled'), + name: 'enable', + uncheckedValue: 0, + defaultValue: 1, + checked: true + } + ]; + + var column2 = [ + { + xtype: 'textfield', + name: 'firstname', + fieldLabel: gettext('First Name') + }, + { + xtype: 'textfield', + name: 'lastname', + fieldLabel: gettext('Last Name') + }, + { + xtype: 'textfield', + name: 'email', + fieldLabel: gettext('E-Mail'), + vtype: 'proxmoxMail' + } + ]; + + if (me.isCreate) { + column1.splice(1,0,{ + xtype: 'pveRealmComboBox', + name: 'realm', + fieldLabel: gettext('Realm'), + allowBlank: false, + matchFieldWidth: false, + listConfig: { width: 300 }, + listeners: { + change: function(combo, newValue){ + realm = newValue; + update_passwd_field(realm); + } + }, + submitValue: false + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + column1: column1, + column2: column2, + columnB: [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + } + ], + advancedItems: [ + { + xtype: 'textfield', + name: 'keys', + fieldLabel: gettext('Key IDs') + } + ], + onGetValues: function(values) { + // hack: ExtJS datefield does not submit 0, so we need to set that + if (!values.expire) { + values.expire = 0; + } + + if (realm) { + values.userid = values.userid + '@' + realm; + } + + if (!values.password) { + delete values.password; + } + + return values; + } + }); + + Ext.applyIf(me, { + subject: gettext('User'), + url: url, + method: method, + fieldDefaults: { + labelWidth: 110 // for spanish translation + }, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var data = response.result.data; + if (Ext.isDefined(data.expire)) { + if (data.expire) { + data.expire = new Date(data.expire * 1000); + } else { + // display 'never' instead of '1970-01-01' + data.expire = null; + } + } + me.setValues(data); + } + }); + } + } +}); +/*jslint confusion: true */ +Ext.define('PVE.dc.UserView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveUserView'], + + onlineHelp: 'pveum_users', + + stateful: true, + stateId: 'grid-users', + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + var store = new Ext.data.Store({ + id: "users", + model: 'pve-users', + sorters: { + property: 'userid', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/access/users/', + enableFn: function(rec) { + if (!caps.access['User.Modify']) { + return false; + } + return rec.data.userid !== 'root@pam'; + }, + callback: function() { + reload(); + } + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec || !caps.access['User.Modify']) { + return; + } + + var win = Ext.create('PVE.dc.UserEdit',{ + userid: rec.data.userid + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + enableFn: function(rec) { + return !!caps.access['User.Modify']; + }, + selModel: sm, + handler: run_editor + }); + + var pwchange_btn = new Proxmox.button.Button({ + text: gettext('Password'), + disabled: true, + selModel: sm, + handler: function(btn, event, rec) { + var win = Ext.create('Proxmox.window.PasswordEdit', { + userid: rec.data.userid + }); + win.on('destroy', reload); + win.show(); + } + }); + + var tfachange_btn = new Proxmox.button.Button({ + text: 'TFA', + disabled: true, + selModel: sm, + handler: function(btn, event, rec) { + var d = rec.data; + var tfa_type = PVE.Parser.parseTfaType(d.keys); + var win = Ext.create('PVE.window.TFAEdit',{ + tfa_type: tfa_type, + userid: d.userid + }); + win.on('destroy', reload); + win.show(); + } + }); + + var tbar = [ + { + text: gettext('Add'), + disabled: !caps.access['User.Modify'], + handler: function() { + var win = Ext.create('PVE.dc.UserEdit',{ + }); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn, pwchange_btn, tfachange_btn + ]; + + var render_username = function(userid) { + return userid.match(/^(.+)(@[^@]+)$/)[1]; + }; + + var render_realm = function(userid) { + return userid.match(/@([^@]+)$/)[1]; + }; + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('User name'), + width: 200, + sortable: true, + renderer: render_username, + dataIndex: 'userid' + }, + { + header: gettext('Realm'), + width: 100, + sortable: true, + renderer: render_realm, + dataIndex: 'userid' + }, + { + header: gettext('Enabled'), + width: 80, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'enable' + }, + { + header: gettext('Expire'), + width: 80, + sortable: true, + renderer: Proxmox.Utils.format_expire, + dataIndex: 'expire' + }, + { + header: gettext('Name'), + width: 150, + sortable: true, + renderer: PVE.Utils.render_full_name, + dataIndex: 'firstname' + }, + { + header: 'TFA', + width: 50, + sortable: true, + renderer: function(v) { + var tfa_type = PVE.Parser.parseTfaType(v); + if (tfa_type === undefined) { + return Proxmox.Utils.noText; + } else if (tfa_type === 1) { + return Proxmox.Utils.yesText; + } else { + return tfa_type; + } + }, + dataIndex: 'keys' + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.PoolView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pvePoolView'], + + onlineHelp: 'pveum_pools', + + stateful: true, + stateId: 'grid-pools', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-pools', + sorters: { + property: 'poolid', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/pools/', + callback: function () { + reload(); + } + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.PoolEdit',{ + poolid: rec.data.poolid + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var tbar = [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.PoolEdit', {}); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn + ]; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Name'), + width: 200, + sortable: true, + dataIndex: 'poolid' + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.PoolEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcPoolEdit'], + + initComponent : function() { + var me = this; + + me.isCreate = !me.poolid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/pools'; + method = 'POST'; + } else { + url = '/api2/extjs/pools/' + me.poolid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Pool'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + fieldLabel: gettext('Name'), + name: 'poolid', + value: me.poolid, + allowBlank: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment', + allowBlank: true + } + ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load(); + } + } +}); +Ext.define('PVE.dc.GroupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveGroupView'], + + onlineHelp: 'pveum_groups', + + stateful: true, + stateId: 'grid-groups', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-groups', + sorters: { + property: 'groupid', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + callback: function() { + reload(); + }, + baseurl: '/access/groups/' + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.GroupEdit',{ + groupid: rec.data.groupid + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var tbar = [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.GroupEdit', {}); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn + ]; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Name'), + width: 200, + sortable: true, + dataIndex: 'groupid' + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.GroupEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcGroupEdit'], + + initComponent : function() { + var me = this; + + me.isCreate = !me.groupid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/access/groups'; + method = 'POST'; + } else { + url = '/api2/extjs/access/groups/' + me.groupid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Group'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + fieldLabel: gettext('Name'), + name: 'groupid', + value: me.groupid, + allowBlank: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment', + allowBlank: true + } + ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load(); + } + } +}); +Ext.define('PVE.dc.RoleView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveRoleView'], + + onlineHelp: 'pveum_roles', + + stateful: true, + stateId: 'grid-roles', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-roles', + sorters: { + property: 'roleid', + order: 'DESC' + } + }); + + var render_privs = function(value, metaData) { + + if (!value) { + return '-'; + } + + // allow word wrap + metaData.style = 'white-space:normal;'; + + return value.replace(/\,/g, ' '); + }; + + Proxmox.Utils.monStoreErrors(me, store); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + store.load(); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + if (rec.data.special === "1") { + return; + } + + var win = Ext.create('PVE.dc.RoleEdit',{ + roleid: rec.data.roleid, + privs: rec.data.privs + }); + win.on('destroy', reload); + win.show(); + }; + + Ext.apply(me, { + store: store, + selModel: sm, + + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Built-In'), + width: 65, + sortable: true, + dataIndex: 'special', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('Name'), + width: 150, + sortable: true, + dataIndex: 'roleid' + }, + { + itemid: 'privs', + header: gettext('Privileges'), + sortable: false, + renderer: render_privs, + dataIndex: 'privs', + flex: 1 + } + ], + listeners: { + activate: function() { + store.load(); + }, + itemdblclick: run_editor + }, + tbar: [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.RoleEdit', {}); + win.on('destroy', reload); + win.show(); + } + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + enableFn: function(record) { + return record.data.special !== '1'; + } + }, + { + xtype: 'proxmoxStdRemoveButton', + selModel: sm, + callback: function() { + reload(); + }, + baseurl: '/access/roles/', + enableFn: function(record) { + return record.data.special !== '1'; + } + } + ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.RoleEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveDcRoleEdit', + + width: 400, + + initComponent : function() { + var me = this; + + me.isCreate = !me.roleid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/access/roles'; + method = 'POST'; + } else { + url = '/api2/extjs/access/roles/' + me.roleid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Role'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + name: 'roleid', + value: me.roleid, + allowBlank: false, + fieldLabel: gettext('Name') + }, + { + xtype: 'pvePrivilegesSelector', + name: 'privs', + value: me.privs, + allowBlank: false, + fieldLabel: gettext('Privileges') + } + ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response) { + var data = response.result.data; + var keys = Ext.Object.getKeys(data); + + me.setValues({ + privs: keys, + roleid: me.roleid + }); + } + }); + } + } +}); +Ext.define('PVE.dc.ACLAdd', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveACLAdd'], + url: '/access/acl', + method: 'PUT', + isAdd: true, + initComponent : function() { + + var me = this; + + me.isCreate = true; + + var items = [ + { + xtype: me.path ? 'hiddenfield' : 'pvePermPathSelector', + name: 'path', + value: me.path, + allowBlank: false, + fieldLabel: gettext('Path') + } + ]; + + if (me.aclType === 'group') { + me.subject = gettext("Group Permission"); + items.push({ + xtype: 'pveGroupSelector', + name: 'groups', + fieldLabel: gettext('Group') + }); + } else if (me.aclType === 'user') { + me.subject = gettext("User Permission"); + items.push({ + xtype: 'pveUserSelector', + name: 'users', + fieldLabel: gettext('User') + }); + } else { + throw "unknown ACL type"; + } + + items.push({ + xtype: 'pveRoleSelector', + name: 'roles', + value: 'NoAccess', + fieldLabel: gettext('Role') + }); + + if (!me.path) { + items.push({ + xtype: 'proxmoxcheckbox', + name: 'propagate', + checked: true, + uncheckedValue: 0, + fieldLabel: gettext('Propagate') + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + items: items, + onlineHelp: 'pveum_permission_management' + }); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.dc.ACLView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveACLView'], + + onlineHelp: 'chapter_user_management', + + stateful: true, + stateId: 'grid-acls', + + // use fixed path + path: undefined, + + initComponent : function() { + var me = this; + + var store = Ext.create('Ext.data.Store',{ + model: 'pve-acl', + proxy: { + type: 'proxmox', + url: "/api2/json/access/acl" + }, + sorters: { + property: 'path', + order: 'DESC' + } + }); + + if (me.path) { + store.addFilter(Ext.create('Ext.util.Filter',{ + filterFn: function(item) { + if (item.data.path === me.path) { + return true; + } + } + })); + } + + var render_ugid = function(ugid, metaData, record) { + if (record.data.type == 'group') { + return '@' + ugid; + } + + return ugid; + }; + + var columns = [ + { + header: gettext('User') + '/' + gettext('Group'), + flex: 1, + sortable: true, + renderer: render_ugid, + dataIndex: 'ugid' + }, + { + header: gettext('Role'), + flex: 1, + sortable: true, + dataIndex: 'roleid' + } + ]; + + if (!me.path) { + columns.unshift({ + header: gettext('Path'), + flex: 1, + sortable: true, + dataIndex: 'path' + }); + columns.push({ + header: gettext('Propagate'), + width: 80, + sortable: true, + dataIndex: 'propagate' + }); + } + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + store.load(); + }; + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: gettext('Are you sure you want to remove this entry'), + handler: function(btn, event, rec) { + var params = { + 'delete': 1, + path: rec.data.path, + roles: rec.data.roleid + }; + if (rec.data.type === 'group') { + params.groups = rec.data.ugid; + } else if (rec.data.type === 'user') { + params.users = rec.data.ugid; + } else { + throw 'unknown data type'; + } + + Proxmox.Utils.API2Request({ + url: '/access/acl', + params: params, + method: 'PUT', + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + menu: { + xtype: 'menu', + items: [ + { + text: gettext('Group Permission'), + iconCls: 'fa fa-fw fa-group', + handler: function() { + var win = Ext.create('PVE.dc.ACLAdd',{ + aclType: 'group', + path: me.path + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('User Permission'), + iconCls: 'fa fa-fw fa-user', + handler: function() { + var win = Ext.create('PVE.dc.ACLAdd',{ + aclType: 'user', + path: me.path + }); + win.on('destroy', reload); + win.show(); + } + } + ] + } + }, + remove_btn + ], + viewConfig: { + trackOver: false + }, + columns: columns, + listeners: { + activate: reload + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-acl', { + extend: 'Ext.data.Model', + fields: [ + 'path', 'type', 'ugid', 'roleid', + { + name: 'propagate', + type: 'boolean' + } + ] + }); + +}); +Ext.define('PVE.dc.AuthView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveAuthView'], + + onlineHelp: 'pveum_authentication_realms', + + stateful: true, + stateId: 'grid-authrealms', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-domains', + sorters: { + property: 'realm', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.AuthEdit',{ + realm: rec.data.realm, + authType: rec.data.type + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + baseurl: '/access/domains/', + selModel: sm, + enableFn: function(rec) { + return !(rec.data.type === 'pve' || rec.data.type === 'pam'); + }, + callback: function() { + reload(); + } + }); + + var tbar = [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Active Directory Server'), + handler: function() { + var win = Ext.create('PVE.dc.AuthEdit', { + authType: 'ad' + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('LDAP Server'), + handler: function() { + var win = Ext.create('PVE.dc.AuthEdit',{ + authType: 'ldap' + }); + win.on('destroy', reload); + win.show(); + } + } + ] + }) + }, + edit_btn, remove_btn + ]; + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Realm'), + width: 100, + sortable: true, + dataIndex: 'realm' + }, + { + header: gettext('Type'), + width: 100, + sortable: true, + dataIndex: 'type' + }, + { + header: gettext('TFA'), + width: 100, + sortable: true, + dataIndex: 'tfa' + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.AuthEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcAuthEdit'], + + isAdd: true, + + initComponent : function() { + var me = this; + + me.isCreate = !me.realm; + + var url; + var method; + var serverlist; + + if (me.isCreate) { + url = '/api2/extjs/access/domains'; + method = 'POST'; + } else { + url = '/api2/extjs/access/domains/' + me.realm; + method = 'PUT'; + } + + var column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'realm', + fieldLabel: gettext('Realm'), + value: me.realm, + allowBlank: false + } + ]; + + if (me.authType === 'ad') { + + me.subject = gettext('Active Directory Server'); + + column1.push({ + xtype: 'textfield', + name: 'domain', + fieldLabel: gettext('Domain'), + emptyText: 'company.net', + allowBlank: false + }); + + } else if (me.authType === 'ldap') { + + me.subject = gettext('LDAP Server'); + + column1.push({ + xtype: 'textfield', + name: 'base_dn', + fieldLabel: gettext('Base Domain Name'), + emptyText: 'CN=Users,DC=Company,DC=net', + allowBlank: false + }); + + column1.push({ + xtype: 'textfield', + name: 'user_attr', + emptyText: 'uid / sAMAccountName', + fieldLabel: gettext('User Attribute Name'), + allowBlank: false + }); + } else if (me.authType === 'pve') { + + if (me.isCreate) { + throw 'unknown auth type'; + } + + me.subject = 'Proxmox VE authentication server'; + + } else if (me.authType === 'pam') { + + if (me.isCreate) { + throw 'unknown auth type'; + } + + me.subject = 'linux PAM'; + + } else { + throw 'unknown auth type '; + } + + column1.push({ + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Default'), + name: 'default', + uncheckedValue: 0 + }); + + var column2 = []; + + if (me.authType === 'ldap' || me.authType === 'ad') { + column2.push( + { + xtype: 'textfield', + fieldLabel: gettext('Server'), + name: 'server1', + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Fallback Server'), + deleteEmpty: !me.isCreate, + name: 'server2' + }, + { + xtype: 'proxmoxintegerfield', + name: 'port', + fieldLabel: gettext('Port'), + minValue: 1, + maxValue: 65535, + emptyText: gettext('Default'), + submitEmptyText: false + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'SSL', + name: 'secure', + uncheckedValue: 0 + } + ); + } + + // Two Factor Auth settings + + column2.push({ + xtype: 'proxmoxKVComboBox', + name: 'tfa', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('TFA'), + comboItems: [ ['__default__', Proxmox.Utils.noneText], ['oath', 'OATH'], ['yubico', 'Yubico']], + listeners: { + change: function(f, value) { + if (!me.rendered) { + return; + } + me.down('field[name=oath_step]').setVisible(value === 'oath'); + me.down('field[name=oath_digits]').setVisible(value === 'oath'); + me.down('field[name=yubico_api_id]').setVisible(value === 'yubico'); + me.down('field[name=yubico_api_key]').setVisible(value === 'yubico'); + me.down('field[name=yubico_url]').setVisible(value === 'yubico'); + } + } + }); + + column2.push({ + xtype: 'proxmoxintegerfield', + name: 'oath_step', + value: '', + minValue: 10, + emptyText: Proxmox.Utils.defaultText + ' (30)', + submitEmptyText: false, + hidden: true, + fieldLabel: 'OATH time step' + }); + + column2.push({ + xtype: 'proxmoxintegerfield', + name: 'oath_digits', + value: '', + minValue: 6, + maxValue: 8, + emptyText: Proxmox.Utils.defaultText + ' (6)', + submitEmptyText: false, + hidden: true, + fieldLabel: 'OATH password length' + }); + + column2.push({ + xtype: 'textfield', + name: 'yubico_api_id', + hidden: true, + fieldLabel: 'Yubico API Id' + }); + + column2.push({ + xtype: 'textfield', + name: 'yubico_api_key', + hidden: true, + fieldLabel: 'Yubico API Key' + }); + + column2.push({ + xtype: 'textfield', + name: 'yubico_url', + hidden: true, + fieldLabel: 'Yubico URL' + }); + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + column1: column1, + column2: column2, + columnB: [{ + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + }], + onGetValues: function(values) { + if (!values.port) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'port' }); + } + delete values.port; + } + + if (me.isCreate) { + values.type = me.authType; + } + + if (values.tfa === 'oath') { + values.tfa = "type=oath"; + if (values.oath_step) { + values.tfa += ",step=" + values.oath_step; + } + if (values.oath_digits) { + values.tfa += ",digits=" + values.oath_digits; + } + } else if (values.tfa === 'yubico') { + values.tfa = "type=yubico"; + values.tfa += ",id=" + values.yubico_api_id; + values.tfa += ",key=" + values.yubico_api_key; + if (values.yubico_url) { + values.tfa += ",url=" + values.yubico_url; + } + } else { + delete values.tfa; + } + + delete values.oath_step; + delete values.oath_digits; + delete values.yubico_api_id; + delete values.yubico_api_key; + delete values.yubico_url; + + return values; + } + }); + + Ext.applyIf(me, { + url: url, + method: method, + fieldDefaults: { + labelWidth: 120 + }, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var data = response.result.data || {}; + // just to be sure (should not happen) + if (data.type !== me.authType) { + me.close(); + throw "got wrong auth type"; + } + + if (data.tfa) { + var tfacfg = PVE.Parser.parseTfaConfig(data.tfa); + data.tfa = tfacfg.type; + if (tfacfg.type === 'yubico') { + data.yubico_api_key = tfacfg.key; + data.yubico_api_id = tfacfg.id; + data.yubico_url = tfacfg.url; + } else if (tfacfg.type === 'oath') { + // step is a number before + /*jslint confusion: true*/ + data.oath_step = tfacfg.step; + data.oath_digits = tfacfg.digits; + /*jslint confusion: false*/ + } + } + + me.setValues(data); + } + }); + } + } +}); +Ext.define('PVE.dc.BackupEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcBackupEdit'], + + defaultFocus: undefined, + + initComponent : function() { + var me = this; + + me.isCreate = !me.jobid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/cluster/backup'; + method = 'POST'; + } else { + url = '/api2/extjs/cluster/backup/' + me.jobid; + method = 'PUT'; + } + + var vmidField = Ext.create('Ext.form.field.Hidden', { + name: 'vmid' + }); + + /*jslint confusion: true*/ + // 'value' can be assigned a string or an array + var selModeField = Ext.create('Proxmox.form.KVComboBox', { + xtype: 'proxmoxKVComboBox', + comboItems: [ + ['include', gettext('Include selected VMs')], + ['all', gettext('All')], + ['exclude', gettext('Exclude selected VMs')], + ['pool', gettext('Pool based')] + ], + fieldLabel: gettext('Selection mode'), + name: 'selMode', + value: '' + }); + + var sm = Ext.create('Ext.selection.CheckboxModel', { + mode: 'SIMPLE', + listeners: { + selectionchange: function(model, selected) { + var sel = []; + Ext.Array.each(selected, function(record) { + sel.push(record.data.vmid); + }); + + // to avoid endless recursion suspend the vmidField change + // event temporary as it calls us again + vmidField.suspendEvent('change'); + vmidField.setValue(sel); + vmidField.resumeEvent('change'); + } + } + }); + + var storagesel = Ext.create('PVE.form.StorageSelector', { + fieldLabel: gettext('Storage'), + nodename: 'localhost', + storageContent: 'backup', + allowBlank: false, + name: 'storage' + }); + + var store = new Ext.data.Store({ + model: 'PVEResources', + sorters: { + property: 'vmid', + order: 'ASC' + } + }); + + var vmgrid = Ext.createWidget('grid', { + store: store, + border: true, + height: 300, + selModel: sm, + disabled: true, + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 60 + }, + { + header: gettext('Node'), + dataIndex: 'node' + }, + { + header: gettext('Status'), + dataIndex: 'uptime', + renderer: function(value) { + if (value) { + return Proxmox.Utils.runningText; + } else { + return Proxmox.Utils.stoppedText; + } + } + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + header: gettext('Type'), + dataIndex: 'type' + } + ] + }); + + var selectPoolMembers = function(poolid) { + if (!poolid) { + return; + } + sm.deselectAll(true); + store.filter([ + { + id: 'poolFilter', + property: 'pool', + value: poolid + } + ]); + sm.selectAll(true); + }; + + var selPool = Ext.create('PVE.form.PoolSelector', { + fieldLabel: gettext('Pool to backup'), + hidden: true, + allowBlank: true, + name: 'pool', + listeners: { + change: function( selpool, newValue, oldValue) { + selectPoolMembers(newValue); + } + } + }); + + var nodesel = Ext.create('PVE.form.NodeSelector', { + name: 'node', + fieldLabel: gettext('Node'), + allowBlank: true, + editable: true, + autoSelect: false, + emptyText: '-- ' + gettext('All') + ' --', + listeners: { + change: function(f, value) { + storagesel.setNodename(value || 'localhost'); + var mode = selModeField.getValue(); + store.clearFilter(); + store.filterBy(function(rec) { + return (!value || rec.get('node') === value); + }); + if (mode === 'all') { + sm.selectAll(true); + } + + if (mode === 'pool') { + selectPoolMembers(selPool.value); + } + } + } + }); + + var column1 = [ + nodesel, + storagesel, + { + xtype: 'pveDayOfWeekSelector', + name: 'dow', + fieldLabel: gettext('Day of week'), + multiSelect: true, + value: ['sat'], + allowBlank: false + }, + { + xtype: 'timefield', + fieldLabel: gettext('Start Time'), + name: 'starttime', + format: 'H:i', + formatText: 'HH:MM', + value: '00:00', + allowBlank: false + }, + selModeField, + selPool + ]; + + var column2 = [ + { + xtype: 'textfield', + fieldLabel: gettext('Send email to'), + name: 'mailto' + }, + { + xtype: 'pveEmailNotificationSelector', + fieldLabel: gettext('Email notification'), + name: 'mailnotification', + deleteEmpty: me.isCreate ? false : true, + value: me.isCreate ? 'always' : '' + }, + { + xtype: 'pveCompressionSelector', + fieldLabel: gettext('Compression'), + name: 'compress', + deleteEmpty: me.isCreate ? false : true, + value: 'lzo' + }, + { + xtype: 'pveBackupModeSelector', + fieldLabel: gettext('Mode'), + value: 'snapshot', + name: 'mode' + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enable'), + name: 'enabled', + uncheckedValue: 0, + defaultValue: 1, + checked: true + }, + vmidField + ]; + /*jslint confusion: false*/ + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + onlineHelp: 'chapter_vzdump', + column1: column1, + column2: column2, + onGetValues: function(values) { + if (!values.node) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' }); + } + delete values.node; + } + + var selMode = values.selMode; + delete values.selMode; + + if (selMode === 'all') { + values.all = 1; + values.exclude = ''; + delete values.vmid; + } else if (selMode === 'exclude') { + values.all = 1; + values.exclude = values.vmid; + delete values.vmid; + } else if (selMode === 'pool') { + delete values.vmid; + } + + if (selMode !== 'pool') { + delete values.pool; + } + return values; + } + }); + + var update_vmid_selection = function(list, mode) { + if (mode !== 'all' && mode !== 'pool') { + sm.deselectAll(true); + if (list) { + Ext.Array.each(list.split(','), function(vmid) { + var rec = store.findRecord('vmid', vmid); + if (rec) { + sm.select(rec, true); + } + }); + } + } + }; + + vmidField.on('change', function(f, value) { + var mode = selModeField.getValue(); + update_vmid_selection(value, mode); + }); + + selModeField.on('change', function(f, value, oldValue) { + if (oldValue === 'pool') { + store.removeFilter('poolFilter'); + } + + if (oldValue === 'all') { + sm.deselectAll(true); + vmidField.setValue(''); + } + + if (value === 'all') { + sm.selectAll(true); + vmgrid.setDisabled(true); + } else { + vmgrid.setDisabled(false); + } + + if (value === 'pool') { + vmgrid.setDisabled(true); + vmidField.setValue(''); + selPool.setVisible(true); + selPool.allowBlank = false; + selectPoolMembers(selPool.value); + + } else { + selPool.setVisible(false); + selPool.allowBlank = true; + } + var list = vmidField.getValue(); + update_vmid_selection(list, value); + }); + + var reload = function() { + store.load({ + params: { type: 'vm' }, + callback: function() { + var node = nodesel.getValue(); + store.clearFilter(); + store.filterBy(function(rec) { + return (!node || node.length === 0 || rec.get('node') === node); + }); + var list = vmidField.getValue(); + var mode = selModeField.getValue(); + if (mode === 'all') { + sm.selectAll(true); + } else if (mode === 'pool'){ + selectPoolMembers(selPool.value); + } else { + update_vmid_selection(list, mode); + } + } + }); + }; + + Ext.applyIf(me, { + subject: gettext("Backup Job"), + url: url, + method: method, + items: [ ipanel, vmgrid ] + }); + + me.callParent(); + + if (me.isCreate) { + selModeField.setValue('include'); + } else { + me.load({ + success: function(response, options) { + var data = response.result.data; + + data.dow = data.dow.split(','); + + if (data.all || data.exclude) { + if (data.exclude) { + data.vmid = data.exclude; + data.selMode = 'exclude'; + } else { + data.vmid = ''; + data.selMode = 'all'; + } + } else if (data.pool) { + data.selMode = 'pool'; + data.selPool = data.pool; + } else { + data.selMode = 'include'; + } + + me.setValues(data); + } + }); + } + + reload(); + } +}); + + +Ext.define('PVE.dc.BackupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveDcBackupView'], + + onlineHelp: 'chapter_vzdump', + + allText: '-- ' + gettext('All') + ' --', + allExceptText: gettext('All except {0}'), + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-cluster-backup', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/backup" + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.BackupEdit',{ + jobid: rec.data.id + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/backup', + callback: function() { + reload(); + } + }); + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + stateful: true, + stateId: 'grid-dc-backup', + viewConfig: { + trackOver: false + }, + tbar: [ + { + text: gettext('Add'), + handler: function() { + var win = Ext.create('PVE.dc.BackupEdit',{}); + win.on('destroy', reload); + win.show(); + } + }, + remove_btn, + edit_btn + ], + columns: [ + { + header: gettext('Enabled'), + width: 80, + dataIndex: 'enabled', + xtype: 'checkcolumn', + sortable: true, + disabled: true, + disabledCls: 'x-item-enabled', + stopSelection: false + }, + { + header: gettext('Node'), + width: 100, + sortable: true, + dataIndex: 'node', + renderer: function(value) { + if (value) { + return value; + } + return me.allText; + } + }, + { + header: gettext('Day of week'), + width: 200, + sortable: false, + dataIndex: 'dow', + renderer: function(val) { + var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + var selected = []; + var cur = -1; + val.split(',').forEach(function(day){ + cur++; + var dow = (dows.indexOf(day)+6)%7; + if (cur === dow) { + if (selected.length === 0 || selected[selected.length-1] === 0) { + selected.push(1); + } else { + selected[selected.length-1]++; + } + } else { + while (cur < dow) { + cur++; + selected.push(0); + } + selected.push(1); + } + }); + + cur = -1; + var days = []; + selected.forEach(function(item) { + cur++; + if (item > 2) { + days.push(Ext.Date.dayNames[(cur+1)] + '-' + Ext.Date.dayNames[(cur+item)%7]); + cur += item-1; + } else if (item == 2) { + days.push(Ext.Date.dayNames[cur+1]); + days.push(Ext.Date.dayNames[(cur+2)%7]); + cur++; + } else if (item == 1) { + days.push(Ext.Date.dayNames[(cur+1)%7]); + } + }); + return days.join(', '); + } + }, + { + header: gettext('Start Time'), + width: 60, + sortable: true, + dataIndex: 'starttime' + }, + { + header: gettext('Storage'), + width: 100, + sortable: true, + dataIndex: 'storage' + }, + { + header: gettext('Selection'), + flex: 1, + sortable: false, + dataIndex: 'vmid', + renderer: function(value, metaData, record) { + /*jslint confusion: true */ + if (record.data.all) { + if (record.data.exclude) { + return Ext.String.format(me.allExceptText, record.data.exclude); + } + return me.allText; + } + if (record.data.vmid) { + return record.data.vmid; + } + + if (record.data.pool) { + return "Pool '"+ record.data.pool + "'"; + } + + return "-"; + } + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-cluster-backup', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'starttime', 'dow', + 'storage', 'node', 'vmid', 'exclude', + 'mailto', 'pool', + { name: 'enabled', type: 'boolean' }, + { name: 'all', type: 'boolean' }, + { name: 'snapshot', type: 'boolean' }, + { name: 'stop', type: 'boolean' }, + { name: 'suspend', type: 'boolean' }, + { name: 'compress', type: 'boolean' } + ] + }); +}); +Ext.define('PVE.dc.Support', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcSupport', + pveGuidePath: '/pve-docs/index.html', + onlineHelp: 'getting_help', + + invalidHtml: '

No valid subscription

' + PVE.Utils.noSubKeyHtml, + + communityHtml: 'Please use the public community forum for any questions.', + + activeHtml: 'Please use our support portal for any questions. You can also use the public community forum to get additional information.', + + bugzillaHtml: '

Bug Tracking

Our bug tracking system is available here.', + + docuHtml: function() { + var me = this; + var guideUrl = window.location.origin + me.pveGuidePath; + var text = Ext.String.format('

Documentation

' + + 'The official Proxmox VE Administration Guide' + + ' is included with this installation and can be browsed at ' + + '{0}', guideUrl); + return text; + }, + + updateActive: function(data) { + var me = this; + + var html = '

' + data.productname + '

' + me.activeHtml; + html += '

' + me.docuHtml(); + html += '

' + me.bugzillaHtml; + + me.update(html); + }, + + updateCommunity: function(data) { + var me = this; + + var html = '

' + data.productname + '

' + me.communityHtml; + html += '

' + me.docuHtml(); + html += '

' + me.bugzillaHtml; + + me.update(html); + }, + + updateInactive: function(data) { + var me = this; + me.update(me.invalidHtml); + }, + + initComponent: function() { + var me = this; + + var reload = function() { + Proxmox.Utils.API2Request({ + url: '/nodes/localhost/subscription', + method: 'GET', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.update('Unable to load subscription status' + ": " + response.htmlStatus); + }, + success: function(response, opts) { + var data = response.result.data; + + if (data.status === 'Active') { + if (data.level === 'c') { + me.updateCommunity(data); + } else { + me.updateActive(data); + } + } else { + me.updateInactive(data); + } + } + }); + }; + + Ext.apply(me, { + autoScroll: true, + bodyStyle: 'padding:10px', + listeners: { + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('pve-security-groups', { + extend: 'Ext.data.Model', + + fields: [ 'group', 'comment', 'digest' ], + idProperty: 'group' +}); + +Ext.define('PVE.SecurityGroupEdit', { + extend: 'Proxmox.window.Edit', + + base_url: "/cluster/firewall/groups", + + allow_iface: false, + + initComponent : function() { + var me = this; + + me.isCreate = (me.group_name === undefined); + + var subject; + + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + + var items = [ + { + xtype: 'textfield', + name: 'group', + value: me.group_name || '', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: me.group_comment || '', + fieldLabel: gettext('Comment') + } + ]; + + if (me.isCreate) { + subject = gettext('Security Group'); + } else { + subject = gettext('Security Group') + " '" + me.group_name + "'"; + items.push({ + xtype: 'hiddenfield', + name: 'rename', + value: me.group_name + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + // InputPanel does not have a 'create' property, does it need a 'isCreate' + isCreate: me.isCreate, + items: items + }); + + + Ext.apply(me, { + subject: subject, + items: [ ipanel ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.SecurityGroupList', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveSecurityGroupList', + + stateful: true, + stateId: 'grid-securitygroups', + + rule_panel: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + base_url: "/cluster/firewall/groups", + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (me.rule_panel == undefined) { + throw "no rule panel specified"; + } + + if (me.base_url == undefined) { + throw "no base_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-security-groups', + proxy: { + type: 'proxmox', + url: '/api2/json' + me.base_url + }, + sorters: { + property: 'group', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + var oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('group', oldrec.data.group); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('PVE.SecurityGroupEdit', { + digest: rec.data.digest, + group_name: rec.data.group, + group_comment: rec.data.comment + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Create'), + handler: function() { + sm.deselectAll(); + var win = Ext.create('PVE.SecurityGroupEdit', {}); + win.show(); + win.on('destroy', reload); + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + enableFn: function(rec) { + return (rec && me.base_url); + }, + callback: function() { + reload(); + } + }); + + Ext.apply(me, { + store: store, + tbar: [ '' + gettext('Group') + ':', me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: gettext('Group'), dataIndex: 'group', width: '100' }, + { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 } + ], + listeners: { + itemdblclick: run_editor, + select: function(sm, rec) { + var url = '/cluster/firewall/groups/' + rec.data.group; + me.rule_panel.setBaseUrl(url); + }, + deselect: function() { + me.rule_panel.setBaseUrl(undefined); + }, + show: reload + } + }); + + me.callParent(); + + store.load(); + } +}); + +Ext.define('PVE.SecurityGroups', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveSecurityGroups', + + title: 'Security Groups', + + initComponent: function() { + var me = this; + + var rule_panel = Ext.createWidget('pveFirewallRules', { + region: 'center', + allow_groups: false, + list_refs_url: '/cluster/firewall/refs', + tbar_prefix: '' + gettext('Rules') + ':', + border: false + }); + + var sglist = Ext.createWidget('pveSecurityGroupList', { + region: 'west', + rule_panel: rule_panel, + width: '25%', + border: false, + split: true + }); + + + Ext.apply(me, { + layout: 'border', + items: [ sglist, rule_panel ], + listeners: { + show: function() { + sglist.fireEvent('show', sglist); + } + } + }); + + me.callParent(); + } +}); +/* + * Datacenter config panel, located in the center of the ViewPort after the Datacenter view is selected + */ + +Ext.define('PVE.dc.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.dc.Config', + + onlineHelp: 'pve_admin_guide', + + initComponent: function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + me.items = []; + + Ext.apply(me, { + title: gettext("Datacenter"), + hstateid: 'dctab' + }); + + if (caps.dc['Sys.Audit']) { + me.items.push({ + title: gettext('Summary'), + xtype: 'pveDcSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + }, + { + title: gettext('Cluster'), + xtype: 'pveClusterAdministration', + iconCls: 'fa fa-server', + itemId: 'cluster' + }, + { + title: 'Ceph', + itemId: 'ceph', + iconCls: 'fa fa-ceph', + xtype: 'pveNodeCephStatus' + }, + { + xtype: 'pveDcOptionView', + title: gettext('Options'), + iconCls: 'fa fa-gear', + itemId: 'options' + }); + } + + if (caps.storage['Datastore.Allocate'] || caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveStorageView', + title: gettext('Storage'), + iconCls: 'fa fa-database', + itemId: 'storage' + }); + } + + if (caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveDcBackupView', + iconCls: 'fa fa-floppy-o', + title: gettext('Backup'), + itemId: 'backup' + }, + { + xtype: 'pveReplicaView', + iconCls: 'fa fa-retweet', + title: gettext('Replication'), + itemId: 'replication' + }, + { + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + expandedOnInit: true + }); + } + + me.items.push({ + xtype: 'pveUserView', + groups: ['permissions'], + iconCls: 'fa fa-user', + title: gettext('Users'), + itemId: 'users' + }); + + if (caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveGroupView', + title: gettext('Groups'), + iconCls: 'fa fa-users', + groups: ['permissions'], + itemId: 'groups' + }, + { + xtype: 'pvePoolView', + title: gettext('Pools'), + iconCls: 'fa fa-tags', + groups: ['permissions'], + itemId: 'pools' + }, + { + xtype: 'pveRoleView', + title: gettext('Roles'), + iconCls: 'fa fa-male', + groups: ['permissions'], + itemId: 'roles' + }, + { + xtype: 'pveAuthView', + title: gettext('Authentication'), + groups: ['permissions'], + iconCls: 'fa fa-key', + itemId: 'domains' + }, + { + xtype: 'pveHAStatus', + title: 'HA', + iconCls: 'fa fa-heartbeat', + itemId: 'ha' + }, + { + title: gettext('Groups'), + groups: ['ha'], + xtype: 'pveHAGroupsView', + iconCls: 'fa fa-object-group', + itemId: 'ha-groups' + }, + { + title: gettext('Fencing'), + groups: ['ha'], + iconCls: 'fa fa-bolt', + xtype: 'pveFencingView', + itemId: 'ha-fencing' + }, + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + allow_iface: true, + base_url: '/cluster/firewall/rules', + list_refs_url: '/cluster/firewall/refs', + iconCls: 'fa fa-shield', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + title: gettext('Options'), + groups: ['firewall'], + iconCls: 'fa fa-gear', + base_url: '/cluster/firewall/options', + onlineHelp: 'pve_firewall_cluster_wide_setup', + fwtype: 'dc', + itemId: 'firewall-options' + }, + { + xtype: 'pveSecurityGroups', + title: gettext('Security Group'), + groups: ['firewall'], + iconCls: 'fa fa-group', + itemId: 'firewall-sg' + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: '/cluster/firewall/aliases', + itemId: 'firewall-aliases' + }, + { + xtype: 'pveIPSet', + title: 'IPSet', + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: '/cluster/firewall/ipset', + list_refs_url: '/cluster/firewall/refs', + itemId: 'firewall-ipset' + }, + { + xtype: 'pveDcSupport', + title: gettext('Support'), + itemId: 'support', + iconCls: 'fa fa-comments-o' + }); + } + + me.callParent(); + } +}); +Ext.define('PVE.dc.NodeView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveDcNodeView', + + title: gettext('Nodes'), + disableSelection: true, + scrollable: true, + + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + dataIndex: 'name' + }, + { + header: 'ID', + width: 40, + sortable: true, + dataIndex: 'nodeid' + }, + { + header: gettext('Online'), + width: 60, + sortable: true, + dataIndex: 'online', + renderer: function(value) { + var cls = (value)?'good':'critical'; + return ''; + } + }, + { + header: gettext('Support'), + width: 100, + sortable: true, + dataIndex: 'level', + renderer: PVE.Utils.render_support_level + }, + { + header: gettext('Server Address'), + width: 115, + sortable: true, + dataIndex: 'ip' + }, + { + header: gettext('CPU usage'), + sortable: true, + width: 110, + dataIndex: 'cpuusage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Memory usage'), + width: 110, + sortable: true, + tdCls: 'x-progressbar-default-cell', + dataIndex: 'memoryusage', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Uptime'), + sortable: true, + dataIndex: 'uptime', + align: 'right', + renderer: Proxmox.Utils.render_uptime + } + ], + + stateful: true, + stateId: 'grid-cluster-nodes', + tools: [ + { + type: 'up', + handler: function(){ + var me = this.up('grid'); + var height = Math.max(me.getHeight()-50, 250); + me.setHeight(height); + } + }, + { + type: 'down', + handler: function(){ + var me = this.up('grid'); + var height = me.getHeight()+50; + me.setHeight(height); + } + } + ] +}, function() { + + Ext.define('pve-dc-nodes', { + extend: 'Ext.data.Model', + fields: [ 'id', 'type', 'name', 'nodeid', 'ip', 'level', 'local', 'online'], + idProperty: 'id' + }); + +}); + +Ext.define('PVE.widget.ProgressBar',{ + extend: 'Ext.Progress', + alias: 'widget.pveProgressBar', + + animate: true, + textTpl: [ + '{percent}%' + ], + + setValue: function(value){ + var me = this; + me.callParent([value]); + + me.removeCls(['warning', 'critical']); + + if (value > 0.89) { + me.addCls('critical'); + } else if (value > 0.59) { + me.addCls('warning'); + } + } +}); +/*jslint confusion: true*/ +Ext.define('pve-cluster-nodes', { + extend: 'Ext.data.Model', + fields: [ + 'node', { type: 'integer', name: 'nodeid' }, 'ring0_addr', 'ring1_addr', + { type: 'integer', name: 'quorum_votes' } + ], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/config/nodes" + }, + idProperty: 'nodeid' +}); + +Ext.define('pve-cluster-info', { + extend: 'Ext.data.Model', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/config/join" + } +}); + +Ext.define('PVE.ClusterAdministration', { + extend: 'Ext.panel.Panel', + xtype: 'pveClusterAdministration', + + title: gettext('Cluster Administration'), + onlineHelp: 'chapter_pvecm', + + border: false, + defaults: { border: false }, + + viewModel: { + parent: null, + data: { + totem: {}, + nodelist: [], + preferred_node: { + name: '', + fp: '', + addr: '' + }, + isInCluster: false, + nodecount: 0 + } + }, + + items: [ + { + xtype: 'panel', + title: gettext('Cluster Information'), + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.store = Ext.create('Proxmox.data.UpdateStore', { + autoStart: true, + interval: 15 * 1000, + storeid: 'pve-cluster-info', + model: 'pve-cluster-info' + }); + view.store.on('load', this.onLoad, this); + view.on('destroy', view.store.stopUpdate); + }, + + onLoad: function(store, records, success) { + var vm = this.getViewModel(); + if (!success || !records || !records[0].data) { + vm.set('totem', {}); + vm.set('isInCluster', false); + vm.set('nodelist', []); + vm.set('preferred_node', { + name: '', + addr: '', + fp: '' + }); + return; + } + var data = records[0].data; + vm.set('totem', data.totem); + vm.set('isInCluster', !!data.totem.cluster_name); + vm.set('nodelist', data.nodelist); + + var nodeinfo = Ext.Array.findBy(data.nodelist, function (el) { + return el.name === data.preferred_node; + }); + + vm.set('preferred_node', { + name: data.preferred_node, + addr: nodeinfo.pve_addr, + ring_addr: [ nodeinfo.ring0_addr, nodeinfo.ring1_addr ], + fp: nodeinfo.pve_fp + }); + }, + + onCreate: function() { + var view = this.getView(); + view.store.stopUpdate(); + var win = Ext.create('PVE.ClusterCreateWindow', { + autoShow: true, + listeners: { + destroy: function() { + view.store.startUpdate(); + } + } + }); + }, + + onClusterInfo: function() { + var vm = this.getViewModel(); + var win = Ext.create('PVE.ClusterInfoWindow', { + joinInfo: { + ipAddress: vm.get('preferred_node.addr'), + fingerprint: vm.get('preferred_node.fp'), + ring_addr: vm.get('preferred_node.ring_addr'), + totem: vm.get('totem') + } + }); + win.show(); + }, + + onJoin: function() { + var view = this.getView(); + view.store.stopUpdate(); + var win = Ext.create('PVE.ClusterJoinNodeWindow', { + autoShow: true, + listeners: { + destroy: function() { + view.store.startUpdate(); + } + } + }); + } + }, + tbar: [ + { + text: gettext('Create Cluster'), + reference: 'createButton', + handler: 'onCreate', + bind: { + disabled: '{isInCluster}' + } + }, + { + text: gettext('Join Information'), + reference: 'addButton', + handler: 'onClusterInfo', + bind: { + disabled: '{!isInCluster}' + } + }, + { + text: gettext('Join Cluster'), + reference: 'joinButton', + handler: 'onJoin', + bind: { + disabled: '{isInCluster}' + } + } + ], + layout: 'hbox', + bodyPadding: 5, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Cluster Name'), + bind: { + value: '{totem.cluster_name}', + hidden: '{!isInCluster}' + }, + flex: 1 + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Config Version'), + bind: { + value: '{totem.config_version}', + hidden: '{!isInCluster}' + }, + flex: 1 + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Number of Nodes'), + labelWidth: 120, + bind: { + value: '{nodecount}', + hidden: '{!isInCluster}' + }, + flex: 1 + }, + { + xtype: 'displayfield', + value: gettext('Standalone node - no cluster defined'), + bind: { + hidden: '{isInCluster}' + }, + flex: 1 + } + ] + }, + { + xtype: 'grid', + title: gettext('Cluster Nodes'), + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.rstore = Ext.create('Proxmox.data.UpdateStore', { + autoLoad: true, + xtype: 'update', + interval: 5 * 1000, + autoStart: true, + storeid: 'pve-cluster-nodes', + model: 'pve-cluster-nodes' + }); + view.setStore(Ext.create('Proxmox.data.DiffStore', { + rstore: view.rstore, + sorters: { + property: 'nodeid', + order: 'DESC' + } + })); + Proxmox.Utils.monStoreErrors(view, view.rstore); + view.rstore.on('load', this.onLoad, this); + view.on('destroy', view.rstore.stopUpdate); + }, + + onLoad: function(store, records, success) { + var vm = this.getViewModel(); + if (!success || !records) { + vm.set('nodecount', 0); + return; + } + vm.set('nodecount', records.length); + } + }, + columns: [ + { + header: gettext('Nodename'), + flex: 2, + dataIndex: 'name' + }, + { + header: gettext('ID'), + flex: 1, + dataIndex: 'nodeid' + }, + { + header: gettext('Votes'), + flex: 1, + dataIndex: 'quorum_votes' + }, + { + header: Ext.String.format(gettext('Link {0}'), 0), + flex: 2, + dataIndex: 'ring0_addr' + }, + { + header: Ext.String.format(gettext('Link {0}'), 1), + flex: 2, + dataIndex: 'ring1_addr' + } + ] + } + ] +}); +/*jslint confusion: true*/ +Ext.define('PVE.ClusterCreateWindow', { + extend: 'Proxmox.window.Edit', + xtype: 'pveClusterCreateWindow', + + title: gettext('Create Cluster'), + width: 600, + + method: 'POST', + url: '/cluster/config', + + isCreate: true, + subject: gettext('Cluster'), + showTaskViewer: true, + + onlineHelp: 'pvecm_create_cluster', + + items: { + xtype: 'inputpanel', + items: [{ + xtype: 'textfield', + fieldLabel: gettext('Cluster Name'), + allowBlank: false, + name: 'clustername' + }, + { + xtype: 'proxmoxNetworkSelector', + fieldLabel: Ext.String.format(gettext('Link {0}'), 0), + emptyText: gettext("Optional, defaults to IP resolved by node's hostname"), + name: 'link0', + autoSelect: false, + valueField: 'address', + displayField: 'address', + skipEmptyText: true + }], + advancedItems: [{ + xtype: 'proxmoxNetworkSelector', + fieldLabel: Ext.String.format(gettext('Link {0}'), 1), + emptyText: gettext("Optional second link for redundancy"), + name: 'link1', + autoSelect: false, + valueField: 'address', + displayField: 'address', + skipEmptyText: true + }] + } +}); + +Ext.define('PVE.ClusterInfoWindow', { + extend: 'Ext.window.Window', + xtype: 'pveClusterInfoWindow', + mixins: ['Proxmox.Mixin.CBind'], + + width: 800, + modal: true, + resizable: false, + title: gettext('Cluster Join Information'), + + joinInfo: { + ipAddress: undefined, + fingerprint: undefined, + totem: {} + }, + + items: [ + { + xtype: 'component', + border: false, + padding: '10 10 10 10', + html: gettext("Copy the Join Information here and use it on the node you want to add.") + }, + { + xtype: 'container', + layout: 'form', + border: false, + padding: '0 10 10 10', + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('IP Address'), + cbind: { value: '{joinInfo.ipAddress}' }, + editable: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Fingerprint'), + cbind: { value: '{joinInfo.fingerprint}' }, + editable: false + }, + { + xtype: 'textarea', + inputId: 'pveSerializedClusterInfo', + fieldLabel: gettext('Join Information'), + grow: true, + cbind: { joinInfo: '{joinInfo}' }, + editable: false, + listeners: { + afterrender: function(field) { + if (!field.joinInfo) { + return; + } + var jsons = Ext.JSON.encode(field.joinInfo); + var base64s = Ext.util.Base64.encode(jsons); + field.setValue(base64s); + } + } + } + ] + } + ], + dockedItems: [{ + dock: 'bottom', + xtype: 'toolbar', + items: [{ + xtype: 'button', + handler: function(b) { + var el = document.getElementById('pveSerializedClusterInfo'); + el.select(); + document.execCommand("copy"); + }, + text: gettext('Copy Information') + }] + }] +}); + +Ext.define('PVE.ClusterJoinNodeWindow', { + extend: 'Proxmox.window.Edit', + xtype: 'pveClusterJoinNodeWindow', + + title: gettext('Cluster Join'), + width: 800, + + method: 'POST', + url: '/cluster/config/join', + + defaultFocus: 'textarea[name=serializedinfo]', + isCreate: true, + submitText: gettext('Join'), + showTaskViewer: true, + + onlineHelp: 'chapter_pvecm', + + viewModel: { + parent: null, + data: { + info: { + fp: '', + ip: '', + ring0Needed: false, + ring1Possible: false, + ring1Needed: false + } + }, + formulas: { + ring0EmptyText: function(get) { + if (get('info.ring0Needed')) { + return gettext("Cannot use default address safely"); + } else { + return gettext("Default: IP resolved by node's hostname"); + } + } + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + '#': { + close: function() { + delete PVE.Utils.silenceAuthFailures; + } + }, + 'proxmoxcheckbox[name=assistedEntry]': { + change: 'onInputTypeChange' + }, + 'textarea[name=serializedinfo]': { + change: 'recomputeSerializedInfo', + enable: 'resetField' + }, + 'proxmoxtextfield[name=ring1_addr]': { + enable: 'ring1Needed' + }, + 'textfield': { + disable: 'resetField' + } + }, + resetField: function(field) { + field.reset(); + }, + ring1Needed: function(f) { + var vm = this.getViewModel(); + f.allowBlank = !vm.get('info.ring1Needed'); + }, + onInputTypeChange: function(field, assistedInput) { + var vm = this.getViewModel(); + if (!assistedInput) { + vm.set('info.ring1Possible', true); + } + }, + recomputeSerializedInfo: function(field, value) { + var vm = this.getViewModel(); + var jsons = Ext.util.Base64.decode(value); + var joinInfo = Ext.JSON.decode(jsons, true); + + var info = { + fp: '', + ring1Needed: false, + ring1Possible: false, + ip: '' + }; + + var totem = {}; + if (!(joinInfo && joinInfo.totem)) { + field.valid = false; + } else { + var ring0Needed = false; + if (joinInfo.ring_addr !== undefined) { + ring0Needed = joinInfo.ring_addr[0] !== joinInfo.ipAddress; + } + + info = { + ip: joinInfo.ipAddress, + fp: joinInfo.fingerprint, + ring0Needed: ring0Needed, + ring1Possible: !!joinInfo.totem['interface']['1'], + ring1Needed: !!joinInfo.totem['interface']['1'] + }; + totem = joinInfo.totem; + field.valid = true; + } + + vm.set('info', info); + } + }, + + submit: function() { + // joining may produce temporarily auth failures, ignore as long the task runs + PVE.Utils.silenceAuthFailures = true; + this.callParent(); + }, + + taskDone: function(success) { + delete PVE.Utils.silenceAuthFailures; + if (success) { + var txt = gettext('Cluster join task finished, node certificate may have changed, reload GUI!'); + // ensure user cannot do harm + Ext.getBody().mask(txt, ['pve-static-mask']); + // TaskView may hide above mask, so tell him directly + Ext.Msg.show({ + title: gettext('Join Task Finished'), + icon: Ext.Msg.INFO, + msg: txt + }); + // reload always (if user wasn't faster), but wait a bit for pveproxy + Ext.defer(function() { + window.location.reload(true); + }, 5000); + } + }, + + items: [{ + xtype: 'proxmoxcheckbox', + reference: 'assistedEntry', + name: 'assistedEntry', + submitValue: false, + value: true, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Select if join information should be extracted from pasted cluster information, deselect for manual entering') + }, + boxLabel: gettext('Assisted join: Paste encoded cluster join information and enter password.') + }, + { + xtype: 'textarea', + name: 'serializedinfo', + submitValue: false, + allowBlank: false, + fieldLabel: gettext('Information'), + emptyText: gettext('Paste encoded Cluster Information here'), + validator: function(val) { + return val === '' || this.valid || + gettext('Does not seem like a valid encoded Cluster Information!'); + }, + bind: { + disabled: '{!assistedEntry.checked}', + hidden: '{!assistedEntry.checked}' + }, + value: '' + }, + { + xtype: 'inputpanel', + column1: [ + { + xtype: 'textfield', + fieldLabel: gettext('Peer Address'), + allowBlank: false, + bind: { + value: '{info.ip}', + readOnly: '{assistedEntry.checked}' + }, + name: 'hostname' + }, + { + xtype: 'textfield', + inputType: 'password', + emptyText: gettext("Peer's root password"), + fieldLabel: gettext('Password'), + allowBlank: false, + name: 'password' + } + ], + column2: [ + { + xtype: 'proxmoxNetworkSelector', + fieldLabel: Ext.String.format(gettext('Link {0}'), 0), + bind: { + emptyText: '{ring0EmptyText}', + allowBlank: '{!info.ring0Needed}' + }, + skipEmptyText: true, + autoSelect: false, + valueField: 'address', + displayField: 'address', + name: 'link0' + }, + { + xtype: 'proxmoxNetworkSelector', + fieldLabel: Ext.String.format(gettext('Link {0}'), 1), + skipEmptyText: true, + autoSelect: false, + valueField: 'address', + displayField: 'address', + bind: { + disabled: '{!info.ring1Possible}', + allowBlank: '{!info.ring1Needed}', + }, + name: 'link1' + } + ], + columnB: [ + { + xtype: 'textfield', + fieldLabel: gettext('Fingerprint'), + allowBlank: false, + bind: { + value: '{info.fp}', + readOnly: '{assistedEntry.checked}' + }, + name: 'fingerprint' + } + ] + }] +}); +/* + * Workspace base class + * + * popup login window when auth fails (call onLogin handler) + * update (re-login) ticket every 15 minutes + * + */ + +Ext.define('PVE.Workspace', { + extend: 'Ext.container.Viewport', + + title: 'Proxmox Virtual Environment', + + loginData: null, // Data from last login call + + onLogin: function(loginData) {}, + + // private + updateLoginData: function(loginData) { + var me = this; + me.loginData = loginData; + Proxmox.Utils.setAuthData(loginData); + + var rt = me.down('pveResourceTree'); + rt.setDatacenterText(loginData.clustername); + + if (loginData.cap) { + Ext.state.Manager.set('GuiCap', loginData.cap); + } + me.response401count = 0; + + me.onLogin(loginData); + }, + + // private + showLogin: function() { + var me = this; + + Proxmox.Utils.authClear(); + Proxmox.UserName = null; + me.loginData = null; + + if (!me.login) { + me.login = Ext.create('PVE.window.LoginWindow', { + handler: function(data) { + me.login = null; + me.updateLoginData(data); + Proxmox.Utils.checked_command(function() {}); // display subscription status + } + }); + } + me.onLogin(null); + me.login.show(); + }, + + initComponent : function() { + var me = this; + + Ext.tip.QuickTipManager.init(); + + // fixme: what about other errors + Ext.Ajax.on('requestexception', function(conn, response, options) { + if (response.status == 401 && !PVE.Utils.silenceAuthFailures) { // auth failure + // don't immediately show as logged out to cope better with some big + // upgrades, which may temporarily produce a false positive 401 err + me.response401count++; + if (me.response401count > 5) { + me.showLogin(); + } + } + }); + + me.callParent(); + + if (!Proxmox.Utils.authOK()) { + me.showLogin(); + } else { + if (me.loginData) { + me.onLogin(me.loginData); + } + } + + Ext.TaskManager.start({ + run: function() { + var ticket = Proxmox.Utils.authOK(); + if (!ticket || !Proxmox.UserName) { + return; + } + + Ext.Ajax.request({ + params: { + username: Proxmox.UserName, + password: ticket + }, + url: '/api2/json/access/ticket', + method: 'POST', + success: function(response, opts) { + var obj = Ext.decode(response.responseText); + me.updateLoginData(obj.data); + } + }); + }, + interval: 15*60*1000 + }); + + } +}); + +Ext.define('PVE.StdWorkspace', { + extend: 'PVE.Workspace', + + alias: ['widget.pveStdWorkspace'], + + // private + setContent: function(comp) { + var me = this; + + var cont = me.child('#content'); + + var lay = cont.getLayout(); + + var cur = lay.getActiveItem(); + + if (comp) { + Proxmox.Utils.setErrorMask(cont, false); + comp.border = false; + cont.add(comp); + if (cur !== null && lay.getNext()) { + lay.next(); + var task = Ext.create('Ext.util.DelayedTask', function(){ + cont.remove(cur); + }); + task.delay(10); + } + } + else { + // helper for cleaning the content when logging out + cont.removeAll(); + } + }, + + selectById: function(nodeid) { + var me = this; + var tree = me.down('pveResourceTree'); + tree.selectById(nodeid); + }, + + onLogin: function(loginData) { + var me = this; + + me.updateUserInfo(); + + if (loginData) { + PVE.data.ResourceStore.startUpdate(); + + Proxmox.Utils.API2Request({ + url: '/version', + method: 'GET', + success: function(response) { + PVE.VersionInfo = response.result.data; + me.updateVersionInfo(); + } + }); + } + }, + + updateUserInfo: function() { + var me = this; + var ui = me.query('#userinfo')[0]; + ui.setText(Proxmox.UserName || ''); + ui.updateLayout(); + }, + + updateVersionInfo: function() { + var me = this; + + var ui = me.query('#versioninfo')[0]; + + if (PVE.VersionInfo) { + var version = PVE.VersionInfo.version; + ui.update('Virtual Environment ' + version); + } else { + ui.update('Virtual Environment'); + } + ui.updateLayout(); + }, + + initComponent : function() { + var me = this; + + Ext.History.init(); + + var sprovider = Ext.create('PVE.StateProvider'); + Ext.state.Manager.setProvider(sprovider); + + var selview = Ext.create('PVE.form.ViewSelector'); + + var rtree = Ext.createWidget('pveResourceTree', { + viewFilter: selview.getViewFilter(), + flex: 1, + selModel: { + selType: 'treemodel', + listeners: { + selectionchange: function(sm, selected) { + if (selected.length > 0) { + var n = selected[0]; + var tlckup = { + root: 'PVE.dc.Config', + node: 'PVE.node.Config', + qemu: 'PVE.qemu.Config', + lxc: 'PVE.lxc.Config', + storage: 'PVE.storage.Browser', + pool: 'pvePoolConfig' + }; + var comp = { + xtype: tlckup[n.data.type || 'root'] || + 'pvePanelConfig', + showSearch: (n.data.id === 'root') || + Ext.isDefined(n.data.groupbyid), + pveSelNode: n, + workspace: me, + viewFilter: selview.getViewFilter() + }; + PVE.curSelectedNode = n; + me.setContent(comp); + } + } + } + } + }); + + selview.on('select', function(combo, records) { + if (records) { + var view = combo.getViewFilter(); + rtree.setViewFilter(view); + } + }); + + var caps = sprovider.get('GuiCap'); + + var createVM = Ext.createWidget('button', { + pack: 'end', + margin: '3 5 0 0', + baseCls: 'x-btn', + iconCls: 'fa fa-desktop', + text: gettext("Create VM"), + disabled: !caps.vms['VM.Allocate'], + handler: function() { + var wiz = Ext.create('PVE.qemu.CreateWizard', {}); + wiz.show(); + } + }); + + var createCT = Ext.createWidget('button', { + pack: 'end', + margin: '3 5 0 0', + baseCls: 'x-btn', + iconCls: 'fa fa-cube', + text: gettext("Create CT"), + disabled: !caps.vms['VM.Allocate'], + handler: function() { + var wiz = Ext.create('PVE.lxc.CreateWizard', {}); + wiz.show(); + } + }); + + sprovider.on('statechange', function(sp, key, value) { + if (key === 'GuiCap' && value) { + caps = value; + createVM.setDisabled(!caps.vms['VM.Allocate']); + createCT.setDisabled(!caps.vms['VM.Allocate']); + } + }); + + Ext.apply(me, { + layout: { type: 'border' }, + border: false, + items: [ + { + region: 'north', + layout: { + type: 'hbox', + align: 'middle' + }, + baseCls: 'x-plain', + defaults: { + baseCls: 'x-plain' + }, + border: false, + margin: '2 0 2 5', + items: [ + { + html: '' + + '' + }, + { + minWidth: 150, + id: 'versioninfo', + html: 'Virtual Environment' + }, + { + xtype: 'pveGlobalSearchField', + tree: rtree + }, + { + flex: 1 + }, + { + xtype: 'proxmoxHelpButton', + hidden: false, + baseCls: 'x-btn', + iconCls: 'fa fa-book x-btn-icon-el-default-toolbar-small ', + listenToGlobalEvent: false, + onlineHelp: 'pve_documentation_index', + text: gettext('Documentation'), + margin: '0 5 0 0' + }, + createVM, + createCT, + { + pack: 'end', + margin: '0 5 0 0', + id: 'userinfo', + xtype: 'button', + baseCls: 'x-btn', + style: { + // proxmox dark grey p light grey as border + backgroundColor: '#464d4d', + borderColor: '#ABBABA' + }, + iconCls: 'fa fa-user', + menu: [ + { + iconCls: 'fa fa-gear', + text: gettext('My Settings'), + handler: function() { + var win = Ext.create('PVE.window.Settings'); + win.show(); + } + }, + { + text: gettext('Password'), + iconCls: 'fa fa-fw fa-key', + handler: function() { + var win = Ext.create('Proxmox.window.PasswordEdit', { + userid: Proxmox.UserName + }); + win.show(); + } + }, + { + text: 'TFA', + iconCls: 'fa fa-fw fa-lock', + handler: function(btn, event, rec) { + var win = Ext.create('PVE.window.TFAEdit',{ + userid: Proxmox.UserName + }); + win.show(); + } + }, + '-', + { + iconCls: 'fa fa-fw fa-sign-out', + text: gettext("Logout"), + handler: function() { + PVE.data.ResourceStore.loadData([], false); + me.showLogin(); + me.setContent(null); + var rt = me.down('pveResourceTree'); + rt.setDatacenterText(undefined); + rt.clearTree(); + + // empty the stores of the StatusPanel child items + var statusPanels = Ext.ComponentQuery.query('pveStatusPanel grid'); + Ext.Array.forEach(statusPanels, function(comp) { + if (comp.getStore()) { + comp.getStore().loadData([], false); + } + }); + } + } + ] + } + ] + }, + { + region: 'center', + stateful: true, + stateId: 'pvecenter', + minWidth: 100, + minHeight: 100, + id: 'content', + xtype: 'container', + layout: { type: 'card' }, + border: false, + margin: '0 5 0 0', + items: [] + }, + { + region: 'west', + stateful: true, + stateId: 'pvewest', + itemId: 'west', + xtype: 'container', + border: false, + layout: { type: 'vbox', align: 'stretch' }, + margin: '0 0 0 5', + split: true, + width: 200, + items: [ selview, rtree ], + listeners: { + resize: function(panel, width, height) { + var viewWidth = me.getSize().width; + if (width > viewWidth - 100) { + panel.setWidth(viewWidth - 100); + } + } + } + }, + { + xtype: 'pveStatusPanel', + stateful: true, + stateId: 'pvesouth', + itemId: 'south', + region: 'south', + margin:'0 5 5 5', + title: gettext('Logs'), + collapsible: true, + header: false, + height: 200, + split:true, + listeners: { + resize: function(panel, width, height) { + var viewHeight = me.getSize().height; + if (height > (viewHeight - 150)) { + panel.setHeight(viewHeight - 150); + } + } + } + } + ] + }); + + me.callParent(); + + me.updateUserInfo(); + + // on resize, center all modal windows + Ext.on('resize', function(){ + var wins = Ext.ComponentQuery.query('window[modal]'); + if (wins.length > 0) { + wins.forEach(function(win){ + win.alignTo(me, 'c-c'); + }); + } + }); + } +}); + diff --git a/serverside/jsmod/6.0-4/pvemanagerlib.js.original b/serverside/jsmod/6.0-4/pvemanagerlib.js.original new file mode 100644 index 0000000..0ba8b5c --- /dev/null +++ b/serverside/jsmod/6.0-4/pvemanagerlib.js.original @@ -0,0 +1,39779 @@ +var pveOnlineHelpInfo = { + "ceph_rados_block_devices" : { + "link" : "/pve-docs/chapter-pvesm.html#ceph_rados_block_devices", + "title" : "Ceph RADOS Block Devices (RBD)" + }, + "chapter_ha_manager" : { + "link" : "/pve-docs/chapter-ha-manager.html#chapter_ha_manager", + "title" : "High Availability" + }, + "chapter_lvm" : { + "link" : "/pve-docs/chapter-sysadmin.html#chapter_lvm", + "title" : "Logical Volume Manager (LVM)" + }, + "chapter_pct" : { + "link" : "/pve-docs/chapter-pct.html#chapter_pct", + "title" : "Proxmox Container Toolkit" + }, + "chapter_pve_firewall" : { + "link" : "/pve-docs/chapter-pve-firewall.html#chapter_pve_firewall", + "title" : "Proxmox VE Firewall" + }, + "chapter_pveceph" : { + "link" : "/pve-docs/chapter-pveceph.html#chapter_pveceph", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "chapter_pvecm" : { + "link" : "/pve-docs/chapter-pvecm.html#chapter_pvecm", + "title" : "Cluster Manager" + }, + "chapter_pvesr" : { + "link" : "/pve-docs/chapter-pvesr.html#chapter_pvesr", + "title" : "Storage Replication" + }, + "chapter_storage" : { + "link" : "/pve-docs/chapter-pvesm.html#chapter_storage", + "title" : "Proxmox VE Storage" + }, + "chapter_system_administration" : { + "link" : "/pve-docs/chapter-sysadmin.html#chapter_system_administration", + "title" : "Host System Administration" + }, + "chapter_user_management" : { + "link" : "/pve-docs/chapter-pveum.html#chapter_user_management", + "title" : "User Management" + }, + "chapter_virtual_machines" : { + "link" : "/pve-docs/chapter-qm.html#chapter_virtual_machines", + "title" : "Qemu/KVM Virtual Machines" + }, + "chapter_vzdump" : { + "link" : "/pve-docs/chapter-vzdump.html#chapter_vzdump", + "title" : "Backup and Restore" + }, + "chapter_zfs" : { + "link" : "/pve-docs/chapter-sysadmin.html#chapter_zfs", + "title" : "ZFS on Linux" + }, + "datacenter_configuration_file" : { + "link" : "/pve-docs/pve-admin-guide.html#datacenter_configuration_file", + "title" : "Datacenter Configuration" + }, + "getting_help" : { + "link" : "/pve-docs/pve-admin-guide.html#getting_help", + "title" : "Getting Help" + }, + "gui_my_settings" : { + "link" : "/pve-docs/chapter-pve-gui.html#gui_my_settings", + "subtitle" : "My Settings", + "title" : "Graphical User Interface" + }, + "ha_manager_fencing" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_fencing", + "subtitle" : "Fencing", + "title" : "High Availability" + }, + "ha_manager_groups" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_groups", + "subtitle" : "Groups", + "title" : "High Availability" + }, + "ha_manager_resource_config" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resource_config", + "subtitle" : "Resources", + "title" : "High Availability" + }, + "ha_manager_resources" : { + "link" : "/pve-docs/chapter-ha-manager.html#ha_manager_resources", + "subtitle" : "Resources", + "title" : "High Availability" + }, + "pct_configuration" : { + "link" : "/pve-docs/chapter-pct.html#pct_configuration", + "subtitle" : "Configuration", + "title" : "Proxmox Container Toolkit" + }, + "pct_container_images" : { + "link" : "/pve-docs/chapter-pct.html#pct_container_images", + "subtitle" : "Container Images", + "title" : "Proxmox Container Toolkit" + }, + "pct_container_network" : { + "link" : "/pve-docs/chapter-pct.html#pct_container_network", + "subtitle" : "Network", + "title" : "Proxmox Container Toolkit" + }, + "pct_container_storage" : { + "link" : "/pve-docs/chapter-pct.html#pct_container_storage", + "subtitle" : "Container Storage", + "title" : "Proxmox Container Toolkit" + }, + "pct_cpu" : { + "link" : "/pve-docs/chapter-pct.html#pct_cpu", + "subtitle" : "CPU", + "title" : "Proxmox Container Toolkit" + }, + "pct_general" : { + "link" : "/pve-docs/chapter-pct.html#pct_general", + "subtitle" : "General Settings", + "title" : "Proxmox Container Toolkit" + }, + "pct_memory" : { + "link" : "/pve-docs/chapter-pct.html#pct_memory", + "subtitle" : "Memory", + "title" : "Proxmox Container Toolkit" + }, + "pct_migration" : { + "link" : "/pve-docs/chapter-pct.html#pct_migration", + "subtitle" : "Migration", + "title" : "Proxmox Container Toolkit" + }, + "pct_options" : { + "link" : "/pve-docs/chapter-pct.html#pct_options", + "subtitle" : "Options", + "title" : "Proxmox Container Toolkit" + }, + "pct_snapshots" : { + "link" : "/pve-docs/chapter-pct.html#pct_snapshots", + "subtitle" : "Snapshots", + "title" : "Proxmox Container Toolkit" + }, + "pct_startup_and_shutdown" : { + "link" : "/pve-docs/chapter-pct.html#pct_startup_and_shutdown", + "subtitle" : "Automatic Start and Shutdown of Containers", + "title" : "Proxmox Container Toolkit" + }, + "pve_admin_guide" : { + "link" : "/pve-docs/pve-admin-guide.html", + "title" : "Proxmox VE Administration Guide" + }, + "pve_ceph_install" : { + "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_install", + "subtitle" : "Installation of Ceph Packages", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "pve_ceph_osds" : { + "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_osds", + "subtitle" : "Creating Ceph OSDs", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "pve_ceph_pools" : { + "link" : "/pve-docs/chapter-pveceph.html#pve_ceph_pools", + "subtitle" : "Creating Ceph Pools", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "pve_documentation_index" : { + "link" : "/pve-docs/index.html", + "title" : "Proxmox VE Documentation Index" + }, + "pve_firewall_cluster_wide_setup" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_cluster_wide_setup", + "subtitle" : "Cluster Wide Setup", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_host_specific_configuration" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_host_specific_configuration", + "subtitle" : "Host Specific Configuration", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_ip_aliases" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_aliases", + "subtitle" : "IP Aliases", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_ip_sets" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_ip_sets", + "subtitle" : "IP Sets", + "title" : "Proxmox VE Firewall" + }, + "pve_firewall_vm_container_configuration" : { + "link" : "/pve-docs/chapter-pve-firewall.html#pve_firewall_vm_container_configuration", + "subtitle" : "VM/Container Configuration", + "title" : "Proxmox VE Firewall" + }, + "pve_service_daemons" : { + "link" : "/pve-docs/index.html#_service_daemons", + "title" : "Service Daemons" + }, + "pveceph_fs" : { + "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs", + "subtitle" : "CephFS", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "pveceph_fs_create" : { + "link" : "/pve-docs/chapter-pveceph.html#pveceph_fs_create", + "subtitle" : "Create a CephFS", + "title" : "Manage Ceph Services on Proxmox VE Nodes" + }, + "pvecm_create_cluster" : { + "link" : "/pve-docs/chapter-pvecm.html#pvecm_create_cluster", + "subtitle" : "Create the Cluster", + "title" : "Cluster Manager" + }, + "pvesr_schedule_time_format" : { + "link" : "/pve-docs/chapter-pvesr.html#pvesr_schedule_time_format", + "subtitle" : "Schedule Format", + "title" : "Storage Replication" + }, + "pveum_authentication_realms" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_authentication_realms", + "subtitle" : "Authentication Realms", + "title" : "User Management" + }, + "pveum_groups" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_groups", + "subtitle" : "Groups", + "title" : "User Management" + }, + "pveum_permission_management" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_permission_management", + "subtitle" : "Permission Management", + "title" : "User Management" + }, + "pveum_pools" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_pools", + "subtitle" : "Pools", + "title" : "User Management" + }, + "pveum_roles" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_roles", + "subtitle" : "Roles", + "title" : "User Management" + }, + "pveum_tfa_auth" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_tfa_auth", + "subtitle" : "Two factor authentication", + "title" : "User Management" + }, + "pveum_users" : { + "link" : "/pve-docs/chapter-pveum.html#pveum_users", + "subtitle" : "Users", + "title" : "User Management" + }, + "qm_bios_and_uefi" : { + "link" : "/pve-docs/chapter-qm.html#qm_bios_and_uefi", + "subtitle" : "BIOS and UEFI", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_cloud_init" : { + "link" : "/pve-docs/chapter-qm.html#qm_cloud_init", + "title" : "Cloud-Init Support" + }, + "qm_copy_and_clone" : { + "link" : "/pve-docs/chapter-qm.html#qm_copy_and_clone", + "subtitle" : "Copies and Clones", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_cpu" : { + "link" : "/pve-docs/chapter-qm.html#qm_cpu", + "subtitle" : "CPU", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_general_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_general_settings", + "subtitle" : "General Settings", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_hard_disk" : { + "link" : "/pve-docs/chapter-qm.html#qm_hard_disk", + "subtitle" : "Hard Disk", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_memory" : { + "link" : "/pve-docs/chapter-qm.html#qm_memory", + "subtitle" : "Memory", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_migration" : { + "link" : "/pve-docs/chapter-qm.html#qm_migration", + "subtitle" : "Migration", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_network_device" : { + "link" : "/pve-docs/chapter-qm.html#qm_network_device", + "subtitle" : "Network Device", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_options" : { + "link" : "/pve-docs/chapter-qm.html#qm_options", + "subtitle" : "Options", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_os_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_os_settings", + "subtitle" : "OS Settings", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_pci_passthrough" : { + "link" : "/pve-docs/chapter-qm.html#qm_pci_passthrough", + "title" : "PCI(e) Passthrough" + }, + "qm_startup_and_shutdown" : { + "link" : "/pve-docs/chapter-qm.html#qm_startup_and_shutdown", + "subtitle" : "Automatic Start and Shutdown of Virtual Machines", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_system_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_system_settings", + "subtitle" : "System Settings", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_usb_passthrough" : { + "link" : "/pve-docs/chapter-qm.html#qm_usb_passthrough", + "subtitle" : "USB Passthrough", + "title" : "Qemu/KVM Virtual Machines" + }, + "qm_virtual_machines_settings" : { + "link" : "/pve-docs/chapter-qm.html#qm_virtual_machines_settings", + "subtitle" : "Virtual Machines Settings", + "title" : "Qemu/KVM Virtual Machines" + }, + "storage_cephfs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_cephfs", + "title" : "Ceph Filesystem (CephFS)" + }, + "storage_cifs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_cifs", + "title" : "CIFS Backend" + }, + "storage_directory" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_directory", + "title" : "Directory Backend" + }, + "storage_glusterfs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_glusterfs", + "title" : "GlusterFS Backend" + }, + "storage_lvm" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_lvm", + "title" : "LVM Backend" + }, + "storage_lvmthin" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_lvmthin", + "title" : "LVM thin Backend" + }, + "storage_nfs" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_nfs", + "title" : "NFS Backend" + }, + "storage_open_iscsi" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_open_iscsi", + "title" : "Open-iSCSI initiator" + }, + "storage_zfspool" : { + "link" : "/pve-docs/chapter-pvesm.html#storage_zfspool", + "title" : "Local ZFS Pool Backend" + }, + "sysadmin_certificate_management" : { + "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_certificate_management", + "title" : "Certificate Management" + }, + "sysadmin_network_configuration" : { + "link" : "/pve-docs/chapter-sysadmin.html#sysadmin_network_configuration", + "title" : "Network Configuration" + } +}; +Ext.ns('PVE'); + +// avoid errors related to Accessible Rich Internet Applications +// (access for people with disabilities) +// TODO reenable after all components are upgraded +Ext.enableAria = false; +Ext.enableAriaButtons = false; +Ext.enableAriaPanels = false; + +// avoid errors when running without development tools +if (!Ext.isDefined(Ext.global.console)) { + var console = { + log: function() {} + }; +} +console.log("Starting PVE Manager"); + +Ext.Ajax.defaultHeaders = { + 'Accept': 'application/json' +}; + +/*jslint confusion: true */ +Ext.define('PVE.Utils', { utilities: { + + // this singleton contains miscellaneous utilities + + toolkit: undefined, // (extjs|touch), set inside Toolkit.js + + bus_match: /^(ide|sata|virtio|scsi)\d+$/, + + log_severity_hash: { + 0: "panic", + 1: "alert", + 2: "critical", + 3: "error", + 4: "warning", + 5: "notice", + 6: "info", + 7: "debug" + }, + + support_level_hash: { + 'c': gettext('Community'), + 'b': gettext('Basic'), + 's': gettext('Standard'), + 'p': gettext('Premium') + }, + + noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit www.proxmox.com to get a list of available options.', + + kvm_ostypes: { + 'Linux': [ + { desc: '5.x - 2.6 Kernel', val: 'l26' }, + { desc: '2.4 Kernel', val: 'l24' } + ], + 'Microsoft Windows': [ + { desc: '10/2016', val: 'win10' }, + { desc: '8.x/2012/2012r2', val: 'win8' }, + { desc: '7/2008r2', val: 'win7' }, + { desc: 'Vista/2008', val: 'w2k8' }, + { desc: 'XP/2003', val: 'wxp' }, + { desc: '2000', val: 'w2k' } + ], + 'Solaris Kernel': [ + { desc: '-', val: 'solaris'} + ], + 'Other': [ + { desc: '-', val: 'other'} + ] + }, + + get_health_icon: function(state, circle) { + if (circle === undefined) { + circle = false; + } + + if (state === undefined) { + state = 'uknown'; + } + + var icon = 'faded fa-question'; + switch(state) { + case 'good': + icon = 'good fa-check'; + break; + case 'old': + icon = 'warning fa-refresh'; + break; + case 'warning': + icon = 'warning fa-exclamation'; + break; + case 'critical': + icon = 'critical fa-times'; + break; + default: break; + } + + if (circle) { + icon += '-circle'; + } + + return icon; + }, + + parse_ceph_version: function(service) { + if (service.ceph_version_short) { + return service.ceph_version_short; + } + + if (service.ceph_version) { + var match = service.ceph_version.match(/version (\d+(\.\d+)*)/); + if (match) { + return match[1]; + } + } + + return undefined; + }, + + compare_ceph_versions: function(a, b) { + if (a === b) { + return 0; + } + let avers = a.toString().split('.'); + let bvers = b.toString().split('.'); + + while (true) { + let av = avers.shift(); + let bv = bvers.shift(); + + if (av === undefined && bv === undefined) { + return 0; + } else if (av === undefined) { + return -1; + } else if (bv === undefined) { + return 1; + } else { + let diff = parseInt(av, 10) - parseInt(bv, 10); + if (diff != 0) return diff; + // else we need to look at the next parts + } + } + + }, + + get_ceph_icon_html: function(health, fw) { + var state = PVE.Utils.map_ceph_health[health]; + var cls = PVE.Utils.get_health_icon(state); + if (fw) { + cls += ' fa-fw'; + } + return " "; + }, + + map_ceph_health: { + 'HEALTH_OK':'good', + 'HEALTH_OLD':'old', + 'HEALTH_WARN':'warning', + 'HEALTH_ERR':'critical' + }, + + render_ceph_health: function(healthObj) { + var state = { + iconCls: PVE.Utils.get_health_icon(), + text: '' + }; + + if (!healthObj || !healthObj.status) { + return state; + } + + var health = PVE.Utils.map_ceph_health[healthObj.status]; + + state.iconCls = PVE.Utils.get_health_icon(health, true); + state.text = healthObj.status; + + return state; + }, + + render_zfs_health: function(value) { + if (typeof value == 'undefined'){ + return ""; + } + var iconCls = 'question-circle'; + switch (value) { + case 'AVAIL': + case 'ONLINE': + iconCls = 'check-circle good'; + break; + case 'REMOVED': + case 'DEGRADED': + iconCls = 'exclamation-circle warning'; + break; + case 'UNAVAIL': + case 'FAULTED': + case 'OFFLINE': + iconCls = 'times-circle critical'; + break; + default: //unknown + } + + return ' ' + value; + + }, + + get_kvm_osinfo: function(value) { + var info = { base: 'Other' }; // default + if (value) { + Ext.each(Object.keys(PVE.Utils.kvm_ostypes), function(k) { + Ext.each(PVE.Utils.kvm_ostypes[k], function(e) { + if (e.val === value) { + info = { desc: e.desc, base: k }; + } + }); + }); + } + return info; + }, + + render_kvm_ostype: function (value) { + var osinfo = PVE.Utils.get_kvm_osinfo(value); + if (osinfo.desc && osinfo.desc !== '-') { + return osinfo.base + ' ' + osinfo.desc; + } else { + return osinfo.base; + } + }, + + render_hotplug_features: function (value) { + var fa = []; + + if (!value || (value === '0')) { + return gettext('Disabled'); + } + + if (value === '1') { + value = 'disk,network,usb'; + } + + Ext.each(value.split(','), function(el) { + if (el === 'disk') { + fa.push(gettext('Disk')); + } else if (el === 'network') { + fa.push(gettext('Network')); + } else if (el === 'usb') { + fa.push('USB'); + } else if (el === 'memory') { + fa.push(gettext('Memory')); + } else if (el === 'cpu') { + fa.push(gettext('CPU')); + } else { + fa.push(el); + } + }); + + return fa.join(', '); + }, + + render_qga_features: function(value) { + if (!value) { + return Proxmox.Utils.defaultText + ' (' + Proxmox.Utils.disabledText + ')'; + } + var props = PVE.Parser.parsePropertyString(value, 'enabled'); + if (!PVE.Parser.parseBoolean(props.enabled)) { + return Proxmox.Utils.disabledText; + } + + delete props.enabled; + var agentstring = Proxmox.Utils.enabledText; + + Ext.Object.each(props, function(key, value) { + var keystring = '' ; + agentstring += ', ' + key + ': '; + + if (PVE.Parser.parseBoolean(value)) { + agentstring += Proxmox.Utils.enabledText; + } else { + agentstring += Proxmox.Utils.disabledText; + } + }); + + return agentstring; + }, + + render_qemu_machine: function(value) { + return value || (Proxmox.Utils.defaultText + ' (i440fx)'); + }, + + render_qemu_bios: function(value) { + if (!value) { + return Proxmox.Utils.defaultText + ' (SeaBIOS)'; + } else if (value === 'seabios') { + return "SeaBIOS"; + } else if (value === 'ovmf') { + return "OVMF (UEFI)"; + } else { + return value; + } + }, + + render_dc_ha_opts: function(value) { + if (!value) { + return Proxmox.Utils.defaultText; + } else { + return PVE.Parser.printPropertyString(value); + } + }, + render_as_property_string: function(value) { + return (!value) ? Proxmox.Utils.defaultText + : PVE.Parser.printPropertyString(value); + }, + + render_scsihw: function(value) { + if (!value) { + return Proxmox.Utils.defaultText + ' (LSI 53C895A)'; + } else if (value === 'lsi') { + return 'LSI 53C895A'; + } else if (value === 'lsi53c810') { + return 'LSI 53C810'; + } else if (value === 'megasas') { + return 'MegaRAID SAS 8708EM2'; + } else if (value === 'virtio-scsi-pci') { + return 'VirtIO SCSI'; + } else if (value === 'virtio-scsi-single') { + return 'VirtIO SCSI single'; + } else if (value === 'pvscsi') { + return 'VMware PVSCSI'; + } else { + return value; + } + }, + + // fixme: auto-generate this + // for now, please keep in sync with PVE::Tools::kvmkeymaps + kvm_keymaps: { + //ar: 'Arabic', + da: 'Danish', + de: 'German', + 'de-ch': 'German (Swiss)', + 'en-gb': 'English (UK)', + 'en-us': 'English (USA)', + es: 'Spanish', + //et: 'Estonia', + fi: 'Finnish', + //fo: 'Faroe Islands', + fr: 'French', + 'fr-be': 'French (Belgium)', + 'fr-ca': 'French (Canada)', + 'fr-ch': 'French (Swiss)', + //hr: 'Croatia', + hu: 'Hungarian', + is: 'Icelandic', + it: 'Italian', + ja: 'Japanese', + lt: 'Lithuanian', + //lv: 'Latvian', + mk: 'Macedonian', + nl: 'Dutch', + //'nl-be': 'Dutch (Belgium)', + no: 'Norwegian', + pl: 'Polish', + pt: 'Portuguese', + 'pt-br': 'Portuguese (Brazil)', + //ru: 'Russian', + sl: 'Slovenian', + sv: 'Swedish', + //th: 'Thai', + tr: 'Turkish' + }, + + kvm_vga_drivers: { + std: gettext('Standard VGA'), + vmware: gettext('VMware compatible'), + qxl: 'SPICE', + qxl2: 'SPICE dual monitor', + qxl3: 'SPICE three monitors', + qxl4: 'SPICE four monitors', + serial0: gettext('Serial terminal') + ' 0', + serial1: gettext('Serial terminal') + ' 1', + serial2: gettext('Serial terminal') + ' 2', + serial3: gettext('Serial terminal') + ' 3', + virtio: 'VirtIO-GPU', + none: Proxmox.Utils.noneText + }, + + render_kvm_language: function (value) { + if (!value || value === '__default__') { + return Proxmox.Utils.defaultText; + } + var text = PVE.Utils.kvm_keymaps[value]; + if (text) { + return text + ' (' + value + ')'; + } + return value; + }, + + kvm_keymap_array: function() { + var data = [['__default__', PVE.Utils.render_kvm_language('')]]; + Ext.Object.each(PVE.Utils.kvm_keymaps, function(key, value) { + data.push([key, PVE.Utils.render_kvm_language(value)]); + }); + + return data; + }, + + console_map: { + '__default__': Proxmox.Utils.defaultText + ' (xterm.js)', + 'vv': 'SPICE (remote-viewer)', + 'html5': 'HTML5 (noVNC)', + 'xtermjs': 'xterm.js' + }, + + render_console_viewer: function(value) { + value = value || '__default__'; + if (PVE.Utils.console_map[value]) { + return PVE.Utils.console_map[value]; + } + return value; + }, + + console_viewer_array: function() { + return Ext.Array.map(Object.keys(PVE.Utils.console_map), function(v) { + return [v, PVE.Utils.render_console_viewer(v)]; + }); + }, + + render_kvm_vga_driver: function (value) { + if (!value) { + return Proxmox.Utils.defaultText; + } + var vga = PVE.Parser.parsePropertyString(value, 'type'); + var text = PVE.Utils.kvm_vga_drivers[vga.type]; + if (!vga.type) { + text = Proxmox.Utils.defaultText; + } + if (text) { + return text + ' (' + value + ')'; + } + return value; + }, + + kvm_vga_driver_array: function() { + var data = [['__default__', PVE.Utils.render_kvm_vga_driver('')]]; + Ext.Object.each(PVE.Utils.kvm_vga_drivers, function(key, value) { + data.push([key, PVE.Utils.render_kvm_vga_driver(value)]); + }); + + return data; + }, + + render_kvm_startup: function(value) { + var startup = PVE.Parser.parseStartup(value); + + var res = 'order='; + if (startup.order === undefined) { + res += 'any'; + } else { + res += startup.order; + } + if (startup.up !== undefined) { + res += ',up=' + startup.up; + } + if (startup.down !== undefined) { + res += ',down=' + startup.down; + } + + return res; + }, + + extractFormActionError: function(action) { + var msg; + switch (action.failureType) { + case Ext.form.action.Action.CLIENT_INVALID: + msg = gettext('Form fields may not be submitted with invalid values'); + break; + case Ext.form.action.Action.CONNECT_FAILURE: + msg = gettext('Connection error'); + var resp = action.response; + if (resp.status && resp.statusText) { + msg += " " + resp.status + ": " + resp.statusText; + } + break; + case Ext.form.action.Action.LOAD_FAILURE: + case Ext.form.action.Action.SERVER_INVALID: + msg = Proxmox.Utils.extractRequestError(action.result, true); + break; + } + return msg; + }, + + format_duration_short: function(ut) { + + if (ut < 60) { + return ut.toFixed(1) + 's'; + } + + if (ut < 3600) { + var mins = ut / 60; + return mins.toFixed(1) + 'm'; + } + + if (ut < 86400) { + var hours = ut / 3600; + return hours.toFixed(1) + 'h'; + } + + var days = ut / 86400; + return days.toFixed(1) + 'd'; + }, + + contentTypes: { + 'images': gettext('Disk image'), + 'backup': gettext('VZDump backup file'), + 'vztmpl': gettext('Container template'), + 'iso': gettext('ISO image'), + 'rootdir': gettext('Container'), + 'snippets': gettext('Snippets') + }, + + storageSchema: { + dir: { + name: Proxmox.Utils.directoryText, + ipanel: 'DirInputPanel', + faIcon: 'folder' + }, + lvm: { + name: 'LVM', + ipanel: 'LVMInputPanel', + faIcon: 'folder' + }, + lvmthin: { + name: 'LVM-Thin', + ipanel: 'LvmThinInputPanel', + faIcon: 'folder' + }, + nfs: { + name: 'NFS', + ipanel: 'NFSInputPanel', + faIcon: 'building' + }, + cifs: { + name: 'CIFS', + ipanel: 'CIFSInputPanel', + faIcon: 'building' + }, + glusterfs: { + name: 'GlusterFS', + ipanel: 'GlusterFsInputPanel', + faIcon: 'building' + }, + iscsi: { + name: 'iSCSI', + ipanel: 'IScsiInputPanel', + faIcon: 'building' + }, + cephfs: { + name: 'CephFS', + ipanel: 'CephFSInputPanel', + faIcon: 'building' + }, + pvecephfs: { + name: 'CephFS (PVE)', + ipanel: 'CephFSInputPanel', + hideAdd: true, + faIcon: 'building' + }, + rbd: { + name: 'RBD', + ipanel: 'RBDInputPanel', + faIcon: 'building' + }, + pveceph: { + name: 'RBD (PVE)', + ipanel: 'RBDInputPanel', + hideAdd: true, + faIcon: 'building' + }, + zfs: { + name: 'ZFS over iSCSI', + ipanel: 'ZFSInputPanel', + faIcon: 'building' + }, + zfspool: { + name: 'ZFS', + ipanel: 'ZFSPoolInputPanel', + faIcon: 'folder' + }, + drbd: { + name: 'DRBD', + hideAdd: true + } + }, + + format_storage_type: function(value, md, record) { + if (value === 'rbd') { + value = (!record || record.get('monhost') ? 'rbd' : 'pveceph'); + } else if (value === 'cephfs') { + value = (!record || record.get('monhost') ? 'cephfs' : 'pvecephfs'); + } + + var schema = PVE.Utils.storageSchema[value]; + if (schema) { + return schema.name; + } + return Proxmox.Utils.unknownText; + }, + + format_ha: function(value) { + var text = Proxmox.Utils.noneText; + + if (value.managed) { + text = value.state || Proxmox.Utils.noneText; + + text += ', ' + Proxmox.Utils.groupText + ': '; + text += value.group || Proxmox.Utils.noneText; + } + + return text; + }, + + format_content_types: function(value) { + return value.split(',').sort().map(function(ct) { + return PVE.Utils.contentTypes[ct] || ct; + }).join(', '); + }, + + render_storage_content: function(value, metaData, record) { + var data = record.data; + if (Ext.isNumber(data.channel) && + Ext.isNumber(data.id) && + Ext.isNumber(data.lun)) { + return "CH " + + Ext.String.leftPad(data.channel,2, '0') + + " ID " + data.id + " LUN " + data.lun; + } + return data.volid.replace(/^.*:(.*\/)?/,''); + }, + + render_serverity: function (value) { + return PVE.Utils.log_severity_hash[value] || value; + }, + + render_cpu: function(value, metaData, record, rowIndex, colIndex, store) { + + if (!(record.data.uptime && Ext.isNumeric(value))) { + return ''; + } + + var maxcpu = record.data.maxcpu || 1; + + if (!Ext.isNumeric(maxcpu) && (maxcpu >= 1)) { + return ''; + } + + var per = value * 100; + + return per.toFixed(1) + '% of ' + maxcpu.toString() + (maxcpu > 1 ? 'CPUs' : 'CPU'); + }, + + render_size: function(value, metaData, record, rowIndex, colIndex, store) { + /*jslint confusion: true */ + + if (!Ext.isNumeric(value)) { + return ''; + } + + return Proxmox.Utils.format_size(value); + }, + + render_bandwidth: function(value) { + if (!Ext.isNumeric(value)) { + return ''; + } + + return Proxmox.Utils.format_size(value) + '/s'; + }, + + render_timestamp_human_readable: function(value) { + return Ext.Date.format(new Date(value * 1000), 'l d F Y H:i:s'); + }, + + render_duration: function(value) { + if (value === undefined) { + return '-'; + } + return PVE.Utils.format_duration_short(value); + }, + + calculate_mem_usage: function(data) { + if (!Ext.isNumeric(data.mem) || + data.maxmem === 0 || + data.uptime < 1) { + return -1; + } + + return (data.mem / data.maxmem); + }, + + render_mem_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) { + if (!Ext.isNumeric(value) || value === -1) { + return ''; + } + if (value > 1 ) { + // we got no percentage but bytes + var mem = value; + var maxmem = record.data.maxmem; + if (!record.data.uptime || + maxmem === 0 || + !Ext.isNumeric(mem)) { + return ''; + } + + return ((mem*100)/maxmem).toFixed(1) + " %"; + } + return (value*100).toFixed(1) + " %"; + }, + + render_mem_usage: function(value, metaData, record, rowIndex, colIndex, store) { + + var mem = value; + var maxmem = record.data.maxmem; + + if (!record.data.uptime) { + return ''; + } + + if (!(Ext.isNumeric(mem) && maxmem)) { + return ''; + } + + return PVE.Utils.render_size(value); + }, + + calculate_disk_usage: function(data) { + + if (!Ext.isNumeric(data.disk) || + data.type === 'qemu' || + (data.type === 'lxc' && data.uptime === 0) || + data.maxdisk === 0) { + return -1; + } + + return (data.disk / data.maxdisk); + }, + + render_disk_usage_percent: function(value, metaData, record, rowIndex, colIndex, store) { + if (!Ext.isNumeric(value) || value === -1) { + return ''; + } + + return (value * 100).toFixed(1) + " %"; + }, + + render_disk_usage: function(value, metaData, record, rowIndex, colIndex, store) { + + var disk = value; + var maxdisk = record.data.maxdisk; + var type = record.data.type; + + if (!Ext.isNumeric(disk) || + type === 'qemu' || + maxdisk === 0 || + (type === 'lxc' && record.data.uptime === 0)) { + return ''; + } + + return PVE.Utils.render_size(value); + }, + + get_object_icon_class: function(type, record) { + var status = ''; + var objType = type; + + if (type === 'type') { + // for folder view + objType = record.groupbyid; + } else if (record.template) { + // templates + objType = 'template'; + status = type; + } else { + // everything else + status = record.status + ' ha-' + record.hastate; + } + + if (record.lock) { + status += ' locked lock-' + record.lock; + } + + var defaults = PVE.tree.ResourceTree.typeDefaults[objType]; + if (defaults && defaults.iconCls) { + var retVal = defaults.iconCls + ' ' + status; + return retVal; + } + + return ''; + }, + + render_resource_type: function(value, metaData, record, rowIndex, colIndex, store) { + + var cls = PVE.Utils.get_object_icon_class(value,record.data); + + var fa = ' '; + return fa + value; + }, + + render_support_level: function(value, metaData, record) { + return PVE.Utils.support_level_hash[value] || '-'; + }, + + render_upid: function(value, metaData, record) { + var type = record.data.type; + var id = record.data.id; + + return Proxmox.Utils.format_task_description(type, id); + }, + + /* render functions for new status panel */ + + render_usage: function(val) { + return (val*100).toFixed(2) + '%'; + }, + + render_cpu_usage: function(val, max) { + return Ext.String.format(gettext('{0}% of {1}') + + ' ' + gettext('CPU(s)'), (val*100).toFixed(2), max); + }, + + render_size_usage: function(val, max) { + if (max === 0) { + return gettext('N/A'); + } + return (val*100/max).toFixed(2) + '% '+ '(' + + Ext.String.format(gettext('{0} of {1}'), + PVE.Utils.render_size(val), PVE.Utils.render_size(max)) + ')'; + }, + + /* this is different for nodes */ + render_node_cpu_usage: function(value, record) { + return PVE.Utils.render_cpu_usage(value, record.cpus); + }, + + /* this is different for nodes */ + render_node_size_usage: function(record) { + return PVE.Utils.render_size_usage(record.used, record.total); + }, + + render_optional_url: function(value) { + var match; + if (value && (match = value.match(/^https?:\/\//)) !== null) { + return '' + value + ''; + } + return value; + }, + + render_san: function(value) { + var names = []; + if (Ext.isArray(value)) { + value.forEach(function(val) { + if (!Ext.isNumber(val)) { + names.push(val); + } + }); + return names.join('
'); + } + return value; + }, + + render_full_name: function(firstname, metaData, record) { + var first = firstname || ''; + var last = record.data.lastname || ''; + return Ext.htmlEncode(first + " " + last); + }, + + render_u2f_error: function(error) { + var ErrorNames = { + '1': gettext('Other Error'), + '2': gettext('Bad Request'), + '3': gettext('Configuration Unsupported'), + '4': gettext('Device Ineligible'), + '5': gettext('Timeout') + }; + return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText; + }, + + windowHostname: function() { + return window.location.hostname.replace(Proxmox.Utils.IP6_bracket_match, + function(m, addr, offset, original) { return addr; }); + }, + + openDefaultConsoleWindow: function(consoles, vmtype, vmid, nodename, vmname, cmd) { + var dv = PVE.Utils.defaultViewer(consoles); + PVE.Utils.openConsoleWindow(dv, vmtype, vmid, nodename, vmname, cmd); + }, + + openConsoleWindow: function(viewer, vmtype, vmid, nodename, vmname, cmd) { + // kvm, lxc, shell, upgrade + + if (vmid == undefined && (vmtype === 'kvm' || vmtype === 'lxc')) { + throw "missing vmid"; + } + + if (!nodename) { + throw "no nodename specified"; + } + + if (viewer === 'html5') { + PVE.Utils.openVNCViewer(vmtype, vmid, nodename, vmname, cmd); + } else if (viewer === 'xtermjs') { + Proxmox.Utils.openXtermJsViewer(vmtype, vmid, nodename, vmname, cmd); + } else if (viewer === 'vv') { + var url; + var params = { proxy: PVE.Utils.windowHostname() }; + if (vmtype === 'kvm') { + url = '/nodes/' + nodename + '/qemu/' + vmid.toString() + '/spiceproxy'; + PVE.Utils.openSpiceViewer(url, params); + } else if (vmtype === 'lxc') { + url = '/nodes/' + nodename + '/lxc/' + vmid.toString() + '/spiceproxy'; + PVE.Utils.openSpiceViewer(url, params); + } else if (vmtype === 'shell') { + url = '/nodes/' + nodename + '/spiceshell'; + PVE.Utils.openSpiceViewer(url, params); + } else if (vmtype === 'upgrade') { + url = '/nodes/' + nodename + '/spiceshell'; + params.upgrade = 1; + PVE.Utils.openSpiceViewer(url, params); + } else if (vmtype === 'cmd') { + url = '/nodes/' + nodename + '/spiceshell'; + params.cmd = cmd; + PVE.Utils.openSpiceViewer(url, params); + } + } else { + throw "unknown viewer type"; + } + }, + + defaultViewer: function(consoles) { + + var allowSpice, allowXtermjs; + + if (consoles === true) { + allowSpice = true; + allowXtermjs = true; + } else if (typeof consoles === 'object') { + allowSpice = consoles.spice; + allowXtermjs = !!consoles.xtermjs; + } + var dv = PVE.VersionInfo.console || 'xtermjs'; + if (dv === 'vv' && !allowSpice) { + dv = (allowXtermjs) ? 'xtermjs' : 'html5'; + } else if (dv === 'xtermjs' && !allowXtermjs) { + dv = (allowSpice) ? 'vv' : 'html5'; + } + + return dv; + }, + + openVNCViewer: function(vmtype, vmid, nodename, vmname, cmd) { + var url = Ext.Object.toQueryString({ + console: vmtype, // kvm, lxc, upgrade or shell + novnc: 1, + vmid: vmid, + vmname: vmname, + node: nodename, + resize: 'off', + cmd: cmd + }); + var nw = window.open("?" + url, '_blank', "innerWidth=745,innerheight=427"); + if (nw) { + nw.focus(); + } + }, + + openSpiceViewer: function(url, params){ + + var downloadWithName = function(uri, name) { + var link = Ext.DomHelper.append(document.body, { + tag: 'a', + href: uri, + css : 'display:none;visibility:hidden;height:0px;' + }); + + // Note: we need to tell android the correct file name extension + // but we do not set 'download' tag for other environments, because + // It can have strange side effects (additional user prompt on firefox) + var andriod = navigator.userAgent.match(/Android/i) ? true : false; + if (andriod) { + link.download = name; + } + + if (link.fireEvent) { + link.fireEvent('onclick'); + } else { + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); + link.dispatchEvent(evt); + } + }; + + Proxmox.Utils.API2Request({ + url: url, + params: params, + method: 'POST', + failure: function(response, opts){ + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts){ + var raw = "[virt-viewer]\n"; + Ext.Object.each(response.result.data, function(k, v) { + raw += k + "=" + v + "\n"; + }); + var url = 'data:application/x-virt-viewer;charset=UTF-8,' + + encodeURIComponent(raw); + + downloadWithName(url, "pve-spice.vv"); + } + }); + }, + + openTreeConsole: function(tree, record, item, index, e) { + e.stopEvent(); + var nodename = record.data.node; + var vmid = record.data.vmid; + var vmname = record.data.name; + if (record.data.type === 'qemu' && !record.data.template) { + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts) { + let conf = response.result.data; + var consoles = { + spice: !!conf.spice, + xtermjs: !!conf.serial, + }; + PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname); + } + }); + } else if (record.data.type === 'lxc' && !record.data.template) { + PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname); + } + }, + + // test automation helper + call_menu_handler: function(menu, text) { + + var list = menu.query('menuitem'); + + Ext.Array.each(list, function(item) { + if (item.text === text) { + if (item.handler) { + item.handler(); + return 1; + } else { + return undefined; + } + } + }); + }, + + createCmdMenu: function(v, record, item, index, event) { + event.stopEvent(); + if (!(v instanceof Ext.tree.View)) { + v.select(record); + } + var menu; + var template = !!record.data.template; + var type = record.data.type; + + if (template) { + if (type === 'qemu' || type == 'lxc') { + menu = Ext.create('PVE.menu.TemplateMenu', { + pveSelNode: record + }); + } + } else if (type === 'qemu' || + type === 'lxc' || + type === 'node') { + menu = Ext.create('PVE.' + type + '.CmdMenu', { + pveSelNode: record, + nodename: record.data.node + }); + } else { + return; + } + + menu.showAt(event.getXY()); + return menu; + }, + + // helper for deleting field which are set to there default values + delete_if_default: function(values, fieldname, default_val, create) { + if (values[fieldname] === '' || values[fieldname] === default_val) { + if (!create) { + if (values['delete']) { + values['delete'] += ',' + fieldname; + } else { + values['delete'] = fieldname; + } + } + + delete values[fieldname]; + } + }, + + loadSSHKeyFromFile: function(file, callback) { + // ssh-keygen produces 740 bytes for an average 4096 bit rsa key, with + // a user@host comment, 1420 for 8192 bits; current max is 16kbit + // assume: 740*8 for max. 32kbit (5920 byte file) + // round upwards to nearest nice number => 8192 bytes, leaves lots of comment space + if (file.size > 8192) { + Ext.Msg.alert(gettext('Error'), gettext("Invalid file size: ") + file.size); + return; + } + /*global + FileReader + */ + var reader = new FileReader(); + reader.onload = function(evt) { + callback(evt.target.result); + }; + reader.readAsText(file); + }, + + bus_counts: { ide: 4, sata: 6, scsi: 16, virtio: 16 }, + + // types is either undefined (all busses), an array of busses, or a single bus + forEachBus: function(types, func) { + var busses = Object.keys(PVE.Utils.bus_counts); + var i, j, count, cont; + + if (Ext.isArray(types)) { + busses = types; + } else if (Ext.isDefined(types)) { + busses = [ types ]; + } + + // check if we only have valid busses + for (i = 0; i < busses.length; i++) { + if (!PVE.Utils.bus_counts[busses[i]]) { + throw "invalid bus: '" + busses[i] + "'"; + } + } + + for (i = 0; i < busses.length; i++) { + count = PVE.Utils.bus_counts[busses[i]]; + for (j = 0; j < count; j++) { + cont = func(busses[i], j); + if (!cont && cont !== undefined) { + return; + } + } + } + }, + + mp_counts: { mps: 256, unused: 256 }, + + forEachMP: function(func, includeUnused) { + var i, cont; + for (i = 0; i < PVE.Utils.mp_counts.mps; i++) { + cont = func('mp', i); + if (!cont && cont !== undefined) { + return; + } + } + + if (!includeUnused) { + return; + } + + for (i = 0; i < PVE.Utils.mp_counts.unused; i++) { + cont = func('unused', i); + if (!cont && cont !== undefined) { + return; + } + } + }, + + cleanEmptyObjectKeys: function (obj) { + var propName; + for (propName in obj) { + if (obj.hasOwnProperty(propName)) { + if (obj[propName] === null || obj[propName] === undefined) { + delete obj[propName]; + } + } + } + }, + + handleStoreErrorOrMask: function(me, store, regex, callback) { + + me.mon(store, 'load', function (proxy, response, success, operation) { + + if (success) { + Proxmox.Utils.setErrorMask(me, false); + return; + } + var msg; + + if (operation.error.statusText) { + if (operation.error.statusText.match(regex)) { + callback(me, operation.error); + return; + } else { + msg = operation.error.statusText + ' (' + operation.error.status + ')'; + } + } else { + msg = gettext('Connection error'); + } + Proxmox.Utils.setErrorMask(me, msg); + }); + }, + + showCephInstallOrMask: function(container, msg, nodename, callback){ + var regex = new RegExp("not (installed|initialized)", "i"); + if (msg.match(regex)) { + if (Proxmox.UserName === 'root@pam') { + container.el.mask(); + if (!container.down('pveCephInstallWindow')){ + var isInstalled = msg.match(/not initialized/i) ? true : false; + var win = Ext.create('PVE.ceph.Install', { + nodename: nodename + }); + win.getViewModel().set('isInstalled', isInstalled); + container.add(win); + win.show(); + callback(win); + } + } else { + container.mask(Ext.String.format(gettext('{0} not installed.') + + ' ' + gettext('Log in as root to install.'), 'Ceph'), ['pve-static-mask']); + } + return true; + } else { + return false; + } + } +}, + + singleton: true, + constructor: function() { + var me = this; + Ext.apply(me, me.utilities); + } + +}); + +// ExtJS related things + +Proxmox.Utils.toolkit = 'extjs'; + +// custom PVE specific VTypes +Ext.apply(Ext.form.field.VTypes, { + + QemuStartDate: function(v) { + return (/^(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)$/).test(v); + }, + QemuStartDateText: gettext('Format') + ': "now" or "2006-06-17T16:01:21" or "2006-06-17"', + IP64AddressList: function(v) { + var list = v.split(/[\ \,\;]+/); + var i; + for (i = 0; i < list.length; i++) { + if (list[i] == '') { + continue; + } + + if (!Proxmox.Utils.IP64_match.test(list[i])) { + return false; + } + } + + return true; + }, + IP64AddressListText: gettext('Example') + ': 192.168.1.1,192.168.1.2', + IP64AddressListMask: /[A-Fa-f0-9\,\:\.\;\ ]/ +}); + +Ext.define('PVE.form.field.Display', { + override: 'Ext.form.field.Display', + + setSubmitValue: function(value) { + // do nothing, this is only to allow generalized bindings for the: + // `me.isCreate ? 'textfield' : 'displayfield'` cases we have. + } +}); +// Some configuration values are complex strings - +// so we need parsers/generators for them. + +Ext.define('PVE.Parser', { statics: { + + // this class only contains static functions + + parseACME: function(value) { + if (!value) { + return; + } + + var res = {}; + var errors = false; + + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; //continue + } + + var match_res; + if ((match_res = p.match(/^(?:domains=)?((?:[a-zA-Z0-9\-\.]+[;, ]?)+)$/)) !== null) { + res.domains = match_res[1].split(/[;, ]/); + } else { + errors = true; + return false; + } + }); + + if (errors || !res) { + return; + } + + return res; + }, + + parseBoolean: function(value, default_value) { + if (!Ext.isDefined(value)) { + return default_value; + } + value = value.toLowerCase(); + return value === '1' || + value === 'on' || + value === 'yes' || + value === 'true'; + }, + + parsePropertyString: function(value, defaultKey) { + var res = {}, + error; + + Ext.Array.each(value.split(','), function(p) { + var kv = p.split('=', 2); + if (Ext.isDefined(kv[1])) { + res[kv[0]] = kv[1]; + } else if (Ext.isDefined(defaultKey)) { + if (Ext.isDefined(res[defaultKey])) { + error = 'defaultKey may be only defined once in propertyString'; + return false; // break + } + res[defaultKey] = kv[0]; + } else { + error = 'invalid propertyString, not a key=value pair and no defaultKey defined'; + return false; // break + } + }); + + if (error !== undefined) { + console.error(error); + return; + } + + return res; + }, + + printPropertyString: function(data, defaultKey) { + var stringparts = [], + gotDefaultKeyVal = false, + defaultKeyVal; + + Ext.Object.each(data, function(key, value) { + if (defaultKey !== undefined && key === defaultKey) { + gotDefaultKeyVal = true; + defaultKeyVal = value; + } else { + stringparts.push(key + '=' + value); + } + }); + + stringparts = stringparts.sort(); + if (gotDefaultKeyVal) { + stringparts.unshift(defaultKeyVal); + } + + return stringparts.join(','); + }, + + parseQemuNetwork: function(key, value) { + if (!(key && value)) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + var match_res; + + if ((match_res = p.match(/^(ne2k_pci|e1000|e1000-82540em|e1000-82544gc|e1000-82545em|vmxnet3|rtl8139|pcnet|virtio|ne2k_isa|i82551|i82557b|i82559er)(=([0-9a-f]{2}(:[0-9a-f]{2}){5}))?$/i)) !== null) { + res.model = match_res[1].toLowerCase(); + if (match_res[3]) { + res.macaddr = match_res[3]; + } + } else if ((match_res = p.match(/^bridge=(\S+)$/)) !== null) { + res.bridge = match_res[1]; + } else if ((match_res = p.match(/^rate=(\d+(\.\d+)?)$/)) !== null) { + res.rate = match_res[1]; + } else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) { + res.tag = match_res[1]; + } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) { + res.firewall = match_res[1]; + } else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) { + res.disconnect = match_res[1]; + } else if ((match_res = p.match(/^queues=(\d+)$/)) !== null) { + res.queues = match_res[1]; + } else if ((match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null) { + res.trunks = match_res[1]; + } else { + errors = true; + return false; // break + } + }); + + if (errors || !res.model) { + return; + } + + return res; + }, + + printQemuNetwork: function(net) { + + var netstr = net.model; + if (net.macaddr) { + netstr += "=" + net.macaddr; + } + if (net.bridge) { + netstr += ",bridge=" + net.bridge; + if (net.tag) { + netstr += ",tag=" + net.tag; + } + if (net.firewall) { + netstr += ",firewall=" + net.firewall; + } + } + if (net.rate) { + netstr += ",rate=" + net.rate; + } + if (net.queues) { + netstr += ",queues=" + net.queues; + } + if (net.disconnect) { + netstr += ",link_down=" + net.disconnect; + } + if (net.trunks) { + netstr += ",trunks=" + net.trunks; + } + return netstr; + }, + + parseQemuDrive: function(key, value) { + if (!(key && value)) { + return; + } + + var res = {}; + + var match_res = key.match(/^([a-z]+)(\d+)$/); + if (!match_res) { + return; + } + res['interface'] = match_res[1]; + res.index = match_res[2]; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + var match_res = p.match(/^([a-z_]+)=(\S+)$/); + if (!match_res) { + if (!p.match(/\=/)) { + res.file = p; + return; // continue + } + errors = true; + return false; // break + } + var k = match_res[1]; + if (k === 'volume') { + k = 'file'; + } + + if (Ext.isDefined(res[k])) { + errors = true; + return false; // break + } + + var v = match_res[2]; + + if (k === 'cache' && v === 'off') { + v = 'none'; + } + + res[k] = v; + }); + + if (errors || !res.file) { + return; + } + + return res; + }, + + printQemuDrive: function(drive) { + + var drivestr = drive.file; + + Ext.Object.each(drive, function(key, value) { + if (!Ext.isDefined(value) || key === 'file' || + key === 'index' || key === 'interface') { + return; // continue + } + drivestr += ',' + key + '=' + value; + }); + + return drivestr; + }, + + parseIPConfig: function(key, value) { + if (!(key && value)) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + var match_res; + if ((match_res = p.match(/^ip=(\S+)$/)) !== null) { + res.ip = match_res[1]; + } else if ((match_res = p.match(/^gw=(\S+)$/)) !== null) { + res.gw = match_res[1]; + } else if ((match_res = p.match(/^ip6=(\S+)$/)) !== null) { + res.ip6 = match_res[1]; + } else if ((match_res = p.match(/^gw6=(\S+)$/)) !== null) { + res.gw6 = match_res[1]; + } else { + errors = true; + return false; // break + } + }); + + if (errors) { + return; + } + + return res; + }, + + printIPConfig: function(cfg) { + var c = ""; + var str = ""; + if (cfg.ip) { + str += "ip=" + cfg.ip; + c = ","; + } + if (cfg.gw) { + str += c + "gw=" + cfg.gw; + c = ","; + } + if (cfg.ip6) { + str += c + "ip6=" + cfg.ip6; + c = ","; + } + if (cfg.gw6) { + str += c + "gw6=" + cfg.gw6; + c = ","; + } + return str; + }, + + parseOpenVZNetIf: function(value) { + if (!value) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(';'), function(item) { + if (!item || item.match(/^\s*$/)) { + return; // continue + } + + var data = {}; + Ext.Array.each(item.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + var match_res = p.match(/^(ifname|mac|bridge|host_ifname|host_mac|mac_filter)=(\S+)$/); + if (!match_res) { + errors = true; + return false; // break + } + if (match_res[1] === 'bridge'){ + var bridgevlanf = match_res[2]; + var bridge_res = bridgevlanf.match(/^(vmbr(\d+))(v(\d+))?(f)?$/); + if (!bridge_res) { + errors = true; + return false; // break + } + data.bridge = bridge_res[1]; + data.tag = bridge_res[4]; + /*jslint confusion: true*/ + data.firewall = bridge_res[5] ? 1 : 0; + /*jslint confusion: false*/ + } else { + data[match_res[1]] = match_res[2]; + } + }); + + if (errors || !data.ifname) { + errors = true; + return false; // break + } + + data.raw = item; + + res[data.ifname] = data; + }); + + return errors ? undefined: res; + }, + + printOpenVZNetIf: function(netif) { + var netarray = []; + + Ext.Object.each(netif, function(iface, data) { + var tmparray = []; + Ext.Array.each(['ifname', 'mac', 'bridge', 'host_ifname' , 'host_mac', 'mac_filter', 'tag', 'firewall'], function(key) { + var value = data[key]; + if (key === 'bridge'){ + if(data.tag){ + value = value + 'v' + data.tag; + } + if (data.firewall){ + value = value + 'f'; + } + } + if (value) { + tmparray.push(key + '=' + value); + } + + }); + netarray.push(tmparray.join(',')); + }); + + return netarray.join(';'); + }, + + parseLxcNetwork: function(value) { + if (!value) { + return; + } + + var data = {}; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + var match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|tag|rate)=(\S+)$/); + if (match_res) { + data[match_res[1]] = match_res[2]; + } else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) { + data.firewall = PVE.Parser.parseBoolean(match_res[1]); + } else { + // todo: simply ignore errors ? + return; // continue + } + }); + + return data; + }, + + printLxcNetwork: function(data) { + var tmparray = []; + Ext.Array.each(['bridge', 'hwaddr', 'mtu', 'name', 'ip', + 'gw', 'ip6', 'gw6', 'firewall', 'tag'], function(key) { + var value = data[key]; + if (value) { + tmparray.push(key + '=' + value); + } + }); + + /*jslint confusion: true*/ + if (data.rate > 0) { + tmparray.push('rate=' + data.rate); + } + /*jslint confusion: false*/ + return tmparray.join(','); + }, + + parseLxcMountPoint: function(value) { + if (!value) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + var match_res = p.match(/^([a-z_]+)=(.+)$/); + if (!match_res) { + if (!p.match(/\=/)) { + res.file = p; + return; // continue + } + errors = true; + return false; // break + } + var k = match_res[1]; + if (k === 'volume') { + k = 'file'; + } + + if (Ext.isDefined(res[k])) { + errors = true; + return false; // break + } + + var v = match_res[2]; + + res[k] = v; + }); + + if (errors || !res.file) { + return; + } + + var m = res.file.match(/^([a-z][a-z0-9\-\_\.]*[a-z0-9]):/i); + if (m) { + res.storage = m[1]; + res.type = 'volume'; + } else if (res.file.match(/^\/dev\//)) { + res.type = 'device'; + } else { + res.type = 'bind'; + } + + return res; + }, + + printLxcMountPoint: function(mp) { + var drivestr = mp.file; + + Ext.Object.each(mp, function(key, value) { + if (!Ext.isDefined(value) || key === 'file' || + key === 'type' || key === 'storage') { + return; // continue + } + drivestr += ',' + key + '=' + value; + }); + + return drivestr; + }, + + parseStartup: function(value) { + if (value === undefined) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + var match_res; + + if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) { + res.order = match_res[2]; + } else if ((match_res = p.match(/^up=(\d+)$/)) !== null) { + res.up = match_res[1]; + } else if ((match_res = p.match(/^down=(\d+)$/)) !== null) { + res.down = match_res[1]; + } else { + errors = true; + return false; // break + } + }); + + if (errors) { + return; + } + + return res; + }, + + printStartup: function(startup) { + var arr = []; + if (startup.order !== undefined && startup.order !== '') { + arr.push('order=' + startup.order); + } + if (startup.up !== undefined && startup.up !== '') { + arr.push('up=' + startup.up); + } + if (startup.down !== undefined && startup.down !== '') { + arr.push('down=' + startup.down); + } + + return arr.join(','); + }, + + parseQemuSmbios1: function(value) { + var res = value.split(',').reduce(function (accumulator, currentValue) { + var splitted = currentValue.split(new RegExp("=(.+)")); + accumulator[splitted[0]] = splitted[1]; + return accumulator; + }, {}); + + if (PVE.Parser.parseBoolean(res.base64, false)) { + Ext.Object.each(res, function(key, value) { + if (key === 'uuid') { return; } + res[key] = Ext.util.Base64.decode(value); + }); + } + + return res; + }, + + printQemuSmbios1: function(data) { + + var datastr = ''; + var base64 = false; + Ext.Object.each(data, function(key, value) { + if (value === '') { return; } + if (key === 'uuid') { + datastr += (datastr !== '' ? ',' : '') + key + '=' + value; + } else { + // values should be base64 encoded from now on, mark config strings correspondingly + if (!base64) { + base64 = true; + datastr += (datastr !== '' ? ',' : '') + 'base64=1'; + } + datastr += (datastr !== '' ? ',' : '') + key + '=' + Ext.util.Base64.encode(value); + } + }); + + return datastr; + }, + + parseTfaConfig: function(value) { + var res = {}; + + Ext.Array.each(value.split(','), function(p) { + var kva = p.split('=', 2); + res[kva[0]] = kva[1]; + }); + + return res; + }, + + parseTfaType: function(value) { + /*jslint confusion: true*/ + var match; + if (!value || !value.length) { + return undefined; + } else if (value === 'x!oath') { + return 'totp'; + } else if (!!(match = value.match(/^x!(.+)$/))) { + return match[1]; + } else { + return 1; + } + }, + + parseQemuCpu: function(value) { + if (!value) { + return {}; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + if (!p.match(/\=/)) { + if (Ext.isDefined(res.cpu)) { + errors = true; + return false; // break + } + res.cputype = p; + return; // continue + } + + var match_res = p.match(/^([a-z_]+)=(\S+)$/); + if (!match_res) { + errors = true; + return false; // break + } + + var k = match_res[1]; + if (Ext.isDefined(res[k])) { + errors = true; + return false; // break + } + + res[k] = match_res[2]; + }); + + if (errors || !res.cputype) { + return; + } + + return res; + }, + + printQemuCpu: function(cpu) { + var cpustr = cpu.cputype; + var optstr = ''; + + Ext.Object.each(cpu, function(key, value) { + if (!Ext.isDefined(value) || key === 'cputype') { + return; // continue + } + optstr += ',' + key + '=' + value; + }); + + if (!cpustr) { + if (optstr) { + return 'kvm64' + optstr; + } + return; + } + + return cpustr + optstr; + }, + + parseSSHKey: function(key) { + // |--- options can have quotes--| type key comment + var keyre = /^(?:((?:[^\s"]|\"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/; + var typere = /^(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)$/; + + var m = key.match(keyre); + if (!m) { + return null; + } + if (m.length < 3 || !m[2]) { // [2] is always either type or key + return null; + } + if (m[1] && m[1].match(typere)) { + return { + type: m[1], + key: m[2], + comment: m[3] + }; + } + if (m[2].match(typere)) { + return { + options: m[1], + type: m[2], + key: m[3], + comment: m[4] + }; + } + return null; + } +}}); +/* This state provider keeps part of the state inside + * the browser history. + * + * We compress (shorten) url using dictionary based compression + * i.e. use column separated list instead of url encoded hash: + * #v\d* version/format + * := indicates string values + * :\d+ lookup value in dictionary hash + * #v1:=value1:5:=value2:=value3:... +*/ + +Ext.define('PVE.StateProvider', { + extend: 'Ext.state.LocalStorageProvider', + + // private + setHV: function(name, newvalue, fireEvents) { + var me = this; + + var changes = false; + var oldtext = Ext.encode(me.UIState[name]); + var newtext = Ext.encode(newvalue); + if (newtext != oldtext) { + changes = true; + me.UIState[name] = newvalue; + //console.log("changed old " + name + " " + oldtext); + //console.log("changed new " + name + " " + newtext); + if (fireEvents) { + me.fireEvent("statechange", me, name, { value: newvalue }); + } + } + return changes; + }, + + // private + hslist: [ + // order is important for notifications + // [ name, default ] + ['view', 'server'], + ['rid', 'root'], + ['ltab', 'tasks'], + ['nodetab', ''], + ['storagetab', ''], + ['pooltab', ''], + ['kvmtab', ''], + ['lxctab', ''], + ['dctab', ''] + ], + + hprefix: 'v1', + + compDict: { + cloudinit: 52, + replication: 51, + system: 50, + monitor: 49, + 'ha-fencing': 48, + 'ha-groups': 47, + 'ha-resources': 46, + 'ceph-log': 45, + 'ceph-crushmap':44, + 'ceph-pools': 43, + 'ceph-osdtree': 42, + 'ceph-disklist': 41, + 'ceph-monlist': 40, + 'ceph-config': 39, + ceph: 38, + 'firewall-fwlog': 37, + 'firewall-options': 36, + 'firewall-ipset': 35, + 'firewall-aliases': 34, + 'firewall-sg': 33, + firewall: 32, + apt: 31, + members: 30, + snapshot: 29, + ha: 28, + support: 27, + pools: 26, + syslog: 25, + ubc: 24, + initlog: 23, + openvz: 22, + backup: 21, + resources: 20, + content: 19, + root: 18, + domains: 17, + roles: 16, + groups: 15, + users: 14, + time: 13, + dns: 12, + network: 11, + services: 10, + options: 9, + console: 8, + hardware: 7, + permissions: 6, + summary: 5, + tasks: 4, + clog: 3, + storage: 2, + folder: 1, + server: 0 + }, + + decodeHToken: function(token) { + var me = this; + + var state = {}; + if (!token) { + Ext.Array.each(me.hslist, function(rec) { + state[rec[0]] = rec[1]; + }); + return state; + } + + // return Ext.urlDecode(token); + + var items = token.split(':'); + var prefix = items.shift(); + + if (prefix != me.hprefix) { + return me.decodeHToken(); + } + + Ext.Array.each(me.hslist, function(rec) { + var value = items.shift(); + if (value) { + if (value[0] === '=') { + value = decodeURIComponent(value.slice(1)); + } else { + Ext.Object.each(me.compDict, function(key, cv) { + if (value == cv) { + value = key; + return false; + } + }); + } + } + state[rec[0]] = value; + }); + + return state; + }, + + encodeHToken: function(state) { + var me = this; + + // return Ext.urlEncode(state); + + var ctoken = me.hprefix; + Ext.Array.each(me.hslist, function(rec) { + var value = state[rec[0]]; + if (!Ext.isDefined(value)) { + value = rec[1]; + } + value = encodeURIComponent(value); + if (!value) { + ctoken += ':'; + } else { + var comp = me.compDict[value]; + if (Ext.isDefined(comp)) { + ctoken += ":" + comp; + } else { + ctoken += ":=" + value; + } + } + }); + + return ctoken; + }, + + constructor: function(config){ + var me = this; + + me.callParent([config]); + + me.UIState = me.decodeHToken(); // set default + + var history_change_cb = function(token) { + //console.log("HC " + token); + if (!token) { + var res = window.confirm(gettext('Are you sure you want to navigate away from this page?')); + if (res){ + // process text value and close... + Ext.History.back(); + } else { + Ext.History.forward(); + } + return; + } + + var newstate = me.decodeHToken(token); + Ext.Array.each(me.hslist, function(rec) { + if (typeof newstate[rec[0]] == "undefined") { + return; + } + me.setHV(rec[0], newstate[rec[0]], true); + }); + }; + + var start_token = Ext.History.getToken(); + if (start_token) { + history_change_cb(start_token); + } else { + var htext = me.encodeHToken(me.UIState); + Ext.History.add(htext); + } + + Ext.History.on('change', history_change_cb); + }, + + get: function(name, defaultValue){ + /*jslint confusion: true */ + var me = this; + var data; + + if (typeof me.UIState[name] != "undefined") { + data = { value: me.UIState[name] }; + } else { + data = me.callParent(arguments); + if (!data && name === 'GuiCap') { + data = { vms: {}, storage: {}, access: {}, nodes: {}, dc: {} }; + } + } + + //console.log("GET " + name + " " + Ext.encode(data)); + return data; + }, + + clear: function(name){ + var me = this; + + if (typeof me.UIState[name] != "undefined") { + me.UIState[name] = null; + } + + me.callParent(arguments); + }, + + set: function(name, value, fireevent){ + var me = this; + + //console.log("SET " + name + " " + Ext.encode(value)); + if (typeof me.UIState[name] != "undefined") { + var newvalue = value ? value.value : null; + if (me.setHV(name, newvalue, fireevent)) { + var htext = me.encodeHToken(me.UIState); + Ext.History.add(htext); + } + } else { + me.callParent(arguments); + } + } +}); +Ext.define('PVE.menu.Item', { + extend: 'Ext.menu.Item', + alias: 'widget.pveMenuItem', + + // set to wrap the handler callback in a confirm dialog showing this text + confirmMsg: false, + + // set to focus 'No' instead of 'Yes' button and show a warning symbol + dangerous: false, + + initComponent: function() { + var me = this; + + if (me.handler) { + me.setHandler(me.handler, me.scope); + } + + me.callParent(); + }, + + setHandler: function(fn, scope) { + var me = this; + me.scope = scope; + me.handler = function(button, e) { + var rec, msg; + if (me.confirmMsg) { + msg = me.confirmMsg; + Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: msg, + buttons: Ext.Msg.YESNO, + defaultFocus: me.dangerous ? 'no' : 'yes', + callback: function(btn) { + if (btn === 'yes') { + Ext.callback(fn, me.scope, [me, e], 0, me); + } + } + }); + } else { + Ext.callback(fn, me.scope, [me, e], 0, me); + } + }; + } +}); +Ext.define('PVE.menu.TemplateMenu', { + extend: 'Ext.menu.Menu', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var guestType = me.pveSelNode.data.type; + if (guestType !== 'qemu' && guestType != 'lxc') { + throw "invalid guest type"; + } + + var vmname = me.pveSelNode.data.name; + + var template = me.pveSelNode.data.template; + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + nodename + '/' + guestType + '/' + vmid + "/status/" + cmd, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + me.title = (guestType === 'qemu' ? 'VM ' : 'CT ') + vmid; + + me.items = [ + { + text: gettext('Migrate'), + iconCls: 'fa fa-fw fa-send-o', + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: guestType, + nodename: nodename, + vmid: vmid + }); + win.show(); + } + }, + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + handler: function() { + var win = Ext.create('PVE.window.Clone', { + nodename: nodename, + guestType: guestType, + vmid: vmid, + isTemplate: template + }); + win.show(); + } + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.button.ConsoleButton', { + extend: 'Ext.button.Split', + alias: 'widget.pveConsoleButton', + + consoleType: 'shell', // one of 'shell', 'kvm', 'lxc', 'upgrade', 'cmd' + + cmd: undefined, + + consoleName: undefined, + + iconCls: 'fa fa-terminal', + + enableSpice: true, + enableXtermjs: true, + + nodename: undefined, + + vmid: 0, + + text: gettext('Console'), + + setEnableSpice: function(enable){ + var me = this; + + me.enableSpice = enable; + me.down('#spicemenu').setDisabled(!enable); + }, + + setEnableXtermJS: function(enable){ + var me = this; + + me.enableXtermjs = enable; + me.down('#xtermjs').setDisabled(!enable); + }, + + handler: function() { + var me = this; + var consoles = { + spice: me.enableSpice, + xtermjs: me.enableXtermjs + }; + PVE.Utils.openDefaultConsoleWindow(consoles, me.consoleType, me.vmid, + me.nodename, me.consoleName, me.cmd); + }, + + menu: [ + { + xtype:'menuitem', + text: 'noVNC', + iconCls: 'pve-itype-icon-novnc', + type: 'html5', + handler: function(button) { + var me = this.up('button'); + PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd); + } + }, + { + xterm: 'menuitem', + itemId: 'spicemenu', + text: 'SPICE', + type: 'vv', + iconCls: 'pve-itype-icon-virt-viewer', + handler: function(button) { + var me = this.up('button'); + PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd); + } + }, + { + text: 'xterm.js', + itemId: 'xtermjs', + iconCls: 'pve-itype-icon-xtermjs', + type: 'xtermjs', + handler: function(button) { + var me = this.up('button'); + PVE.Utils.openConsoleWindow(button.type, me.consoleType, me.vmid, me.nodename, me.consoleName, me.cmd); + } + } + ], + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.callParent(); + } +}); +/* Button features: + * - observe selection changes to enable/disable the button using enableFn() + * - pop up confirmation dialog using confirmMsg() + * + * does this for the button and every menu item + */ +Ext.define('PVE.button.Split', { + extend: 'Ext.button.Split', + alias: 'widget.pveSplitButton', + + // the selection model to observe + selModel: undefined, + + // if 'false' handler will not be called (button disabled) + enableFn: function(record) { }, + + // function(record) or text + confirmMsg: false, + + // take special care in confirm box (select no as default). + dangerous: false, + + handlerWrapper: function(button, event) { + var me = this; + var rec, msg; + if (me.selModel) { + rec = me.selModel.getSelection()[0]; + if (!rec || (me.enableFn(rec) === false)) { + return; + } + } + + if (me.confirmMsg) { + msg = me.confirmMsg; + // confirMsg can be boolean or function + /*jslint confusion: true*/ + if (Ext.isFunction(me.confirmMsg)) { + msg = me.confirmMsg(rec); + } + /*jslint confusion: false*/ + Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: msg, + buttons: Ext.Msg.YESNO, + callback: function(btn) { + if (btn !== 'yes') { + return; + } + me.realHandler(button, event, rec); + } + }); + } else { + me.realHandler(button, event, rec); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + + var me = this; + + if (me.handler) { + me.realHandler = me.handler; + me.handler = me.handlerWrapper; + } + + if (me.menu && me.menu.items) { + me.menu.items.forEach(function(item) { + if (item.handler) { + item.realHandler = item.handler; + item.handler = me.handlerWrapper; + } + + if (item.selModel) { + me.mon(item.selModel, "selectionchange", function() { + var rec = item.selModel.getSelection()[0]; + if (!rec || (item.enableFn(rec) === false )) { + item.setDisabled(true); + } else { + item.setDisabled(false); + } + }); + } + }); + } + + me.callParent(); + + if (me.selModel) { + + me.mon(me.selModel, "selectionchange", function() { + var rec = me.selModel.getSelection()[0]; + if (!rec || (me.enableFn(rec) === false)) { + me.setDisabled(true); + } else { + me.setDisabled(false); + } + }); + } + } +}); +Ext.define('PVE.controller.StorageEdit', { + extend: 'Ext.app.ViewController', + alias: 'controller.storageEdit', + control: { + 'field[name=content]': { + change: function(field, value) { + var hasBackups = Ext.Array.contains(value, 'backup'); + var maxfiles = this.lookupReference('maxfiles'); + if (!maxfiles) { + return; + } + + if (!hasBackups) { + // clear values which will never be submitted + maxfiles.reset(); + } + maxfiles.setDisabled(!hasBackups); + } + } + } +}); +Ext.define('PVE.qemu.CmdMenu', { + extend: 'Ext.menu.Menu', + + showSeparator: false, + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var vmname = me.pveSelNode.data.name; + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + nodename + '/qemu/' + vmid + "/status/" + cmd, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + var caps = Ext.state.Manager.get('GuiCap'); + + var running = false; + var stopped = true; + var suspended = false; + var standalone = PVE.data.ResourceStore.getNodes().length < 2; + + switch (me.pveSelNode.data.status) { + case 'running': + running = true; + stopped = false; + break; + case 'suspended': + stopped = false; + suspended = true; + break; + case 'paused': + stopped = false; + suspended = true; + break; + default: break; + } + + me.title = "VM " + vmid; + + me.items = [ + { + text: gettext('Start'), + iconCls: 'fa fa-fw fa-play', + hidden: running || suspended, + disabled: running || suspended, + handler: function() { + vm_command('start'); + } + }, + { + text: gettext('Pause'), + iconCls: 'fa fa-fw fa-pause', + hidden: stopped || suspended, + disabled: stopped || suspended, + handler: function() { + var msg = Proxmox.Utils.format_task_description('qmpause', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + vm_command('suspend'); + }); + } + }, + { + text: gettext('Hibernate'), + iconCls: 'fa fa-fw fa-download', + hidden: stopped || suspended, + disabled: stopped || suspended, + tooltip: gettext('Suspend to disk'), + handler: function() { + var msg = Proxmox.Utils.format_task_description('qmsuspend', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + vm_command('suspend', { todisk: 1 }); + }); + } + }, + { + text: gettext('Resume'), + iconCls: 'fa fa-fw fa-play', + hidden: !suspended, + handler: function() { + vm_command('resume'); + } + }, + { + text: gettext('Shutdown'), + iconCls: 'fa fa-fw fa-power-off', + disabled: stopped || suspended, + handler: function() { + var msg = Proxmox.Utils.format_task_description('qmshutdown', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command('shutdown'); + }); + } + }, + { + text: gettext('Stop'), + iconCls: 'fa fa-fw fa-stop', + disabled: stopped, + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'), + handler: function() { + var msg = Proxmox.Utils.format_task_description('qmstop', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command("stop"); + }); + } + }, + { + xtype: 'menuseparator', + hidden: (standalone || !caps.vms['VM.Migrate']) && !caps.vms['VM.Allocate'] && !caps.vms['VM.Clone'] + }, + { + text: gettext('Migrate'), + iconCls: 'fa fa-fw fa-send-o', + hidden: standalone || !caps.vms['VM.Migrate'], + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'qemu', + nodename: nodename, + vmid: vmid + }); + win.show(); + } + }, + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: !caps.vms['VM.Clone'], + handler: function() { + PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'qemu'); + } + }, + { + text: gettext('Convert to template'), + iconCls: 'fa fa-fw fa-file-o', + hidden: !caps.vms['VM.Allocate'], + handler: function() { + var msg = Proxmox.Utils.format_task_description('qmtemplate', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/qemu/' + vmid + '/template', + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }); + } + }, + { xtype: 'menuseparator' }, + { + text: gettext('Console'), + iconCls: 'fa fa-fw fa-terminal', + handler: function() { + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/qemu/' + vmid + '/status/current', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts) { + var allowSpice = response.result.data.spice; + var allowXtermjs = response.result.data.serial; + var consoles = { + spice: allowSpice, + xtermjs: allowXtermjs + }; + PVE.Utils.openDefaultConsoleWindow(consoles, 'kvm', vmid, nodename, vmname); + } + }); + } + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.lxc.CmdMenu', { + extend: 'Ext.menu.Menu', + + showSeparator: false, + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no CT ID specified"; + } + var vmname = me.pveSelNode.data.name; + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + nodename + '/lxc/' + vmid + "/status/" + cmd, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + var caps = Ext.state.Manager.get('GuiCap'); + + var running = false; + var stopped = true; + var suspended = false; + var standalone = PVE.data.ResourceStore.getNodes().length < 2; + + switch (me.pveSelNode.data.status) { + case 'running': + running = true; + stopped = false; + break; + case 'paused': + stopped = false; + suspended = true; + break; + default: break; + } + + me.title = 'CT ' + vmid; + + me.items = [ + { + text: gettext('Start'), + iconCls: 'fa fa-fw fa-play', + disabled: running, + handler: function() { + vm_command('start'); + } + }, +// { +// text: gettext('Suspend'), +// iconCls: 'fa fa-fw fa-pause', +// hidde: suspended, +// disabled: stopped || suspended, +// handler: function() { +// var msg = Proxmox.Utils.format_task_description('vzsuspend', vmid); +// Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { +// if (btn !== 'yes') { +// return; +// } +// +// vm_command('suspend'); +// }); +// } +// }, +// { +// text: gettext('Resume'), +// iconCls: 'fa fa-fw fa-play', +// hidden: !suspended, +// handler: function() { +// vm_command('resume'); +// } +// }, + { + text: gettext('Shutdown'), + iconCls: 'fa fa-fw fa-power-off', + disabled: stopped || suspended, + handler: function() { + var msg = Proxmox.Utils.format_task_description('vzshutdown', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command('shutdown'); + }); + } + }, + { + text: gettext('Stop'), + iconCls: 'fa fa-fw fa-stop', + disabled: stopped, + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'), + handler: function() { + var msg = Proxmox.Utils.format_task_description('vzstop', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + vm_command("stop"); + }); + } + }, + { + xtype: 'menuseparator', + hidden: standalone || !caps.vms['VM.Migrate'] + }, + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: !caps.vms['VM.Clone'], + handler: function() { + PVE.window.Clone.wrap(nodename, vmid, me.isTemplate, 'lxc'); + } + }, + { + text: gettext('Migrate'), + iconCls: 'fa fa-fw fa-send-o', + hidden: standalone || !caps.vms['VM.Migrate'], + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'lxc', + nodename: nodename, + vmid: vmid + }); + win.show(); + } + }, + { + text: gettext('Convert to template'), + iconCls: 'fa fa-fw fa-file-o', + handler: function() { + var msg = Proxmox.Utils.format_task_description('vztemplate', vmid); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/lxc/' + vmid + '/template', + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }); + } + }, + { xtype: 'menuseparator' }, + { + text: gettext('Console'), + iconCls: 'fa fa-fw fa-terminal', + handler: function() { + PVE.Utils.openDefaultConsoleWindow(true, 'lxc', vmid, nodename, vmname); + } + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.node.CmdMenu', { + extend: 'Ext.menu.Menu', + xtype: 'nodeCmdMenu', + + showSeparator: false, + + items: [ + { + text: gettext('Create VM'), + itemId: 'createvm', + iconCls: 'fa fa-desktop', + handler: function() { + var me = this.up('menu'); + var wiz = Ext.create('PVE.qemu.CreateWizard', { + nodename: me.nodename + }); + wiz.show(); + } + }, + { + text: gettext('Create CT'), + itemId: 'createct', + iconCls: 'fa fa-cube', + handler: function() { + var me = this.up('menu'); + var wiz = Ext.create('PVE.lxc.CreateWizard', { + nodename: me.nodename + }); + wiz.show(); + } + }, + { xtype: 'menuseparator' }, + { + text: gettext('Bulk Start'), + itemId: 'bulkstart', + iconCls: 'fa fa-fw fa-play', + handler: function() { + var me = this.up('menu'); + var win = Ext.create('PVE.window.BulkAction', { + nodename: me.nodename, + title: gettext('Bulk Start'), + btnText: gettext('Start'), + action: 'startall' + }); + win.show(); + } + }, + { + text: gettext('Bulk Stop'), + itemId: 'bulkstop', + iconCls: 'fa fa-fw fa-stop', + handler: function() { + var me = this.up('menu'); + var win = Ext.create('PVE.window.BulkAction', { + nodename: me.nodename, + title: gettext('Bulk Stop'), + btnText: gettext('Stop'), + action: 'stopall' + }); + win.show(); + } + }, + { + text: gettext('Bulk Migrate'), + itemId: 'bulkmigrate', + iconCls: 'fa fa-fw fa-send-o', + handler: function() { + var me = this.up('menu'); + var win = Ext.create('PVE.window.BulkAction', { + nodename: me.nodename, + title: gettext('Bulk Migrate'), + btnText: gettext('Migrate'), + action: 'migrateall' + }); + win.show(); + } + }, + { xtype: 'menuseparator' }, + { + text: gettext('Shell'), + itemId: 'shell', + iconCls: 'fa fa-fw fa-terminal', + handler: function() { + var me = this.up('menu'); + PVE.Utils.openDefaultConsoleWindow(true, 'shell', undefined, me.nodename, undefined); + } + }, + { xtype: 'menuseparator' }, + { + text: gettext('Wake-on-LAN'), + itemId: 'wakeonlan', + iconCls: 'fa fa-fw fa-power-off', + handler: function() { + var me = this.up('menu'); + Proxmox.Utils.API2Request({ + param: {}, + url: '/nodes/' + me.nodename + '/wakeonlan', + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + Ext.Msg.show({ + title: 'Success', + icon: Ext.Msg.INFO, + msg: Ext.String.format(gettext("Wake on LAN packet send for '{0}': '{1}'"), me.nodename, response.result.data) + }); + } + }); + } + } + ], + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw 'no nodename specified'; + } + + me.title = gettext('Node') + " '" + me.nodename + "'"; + me.callParent(); + + var caps = Ext.state.Manager.get('GuiCap'); + // disable not allowed options + if (!caps.vms['VM.Allocate']) { + me.getComponent('createct').setDisabled(true); + me.getComponent('createvm').setDisabled(true); + } + + if (!caps.nodes['Sys.PowerMgmt']) { + me.getComponent('bulkstart').setDisabled(true); + me.getComponent('bulkstop').setDisabled(true); + me.getComponent('bulkmigrate').setDisabled(true); + me.getComponent('wakeonlan').setDisabled(true); + } + + if (!caps.nodes['Sys.Console']) { + me.getComponent('shell').setDisabled(true); + } + + if (me.pveSelNode.data.running) { + me.getComponent('wakeonlan').setDisabled(true); + } + } +}); +Ext.define('PVE.noVncConsole', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNoVncConsole', + + nodename: undefined, + + vmid: undefined, + + cmd: undefined, + + consoleType: undefined, // lxc, kvm, shell, cmd + + layout: 'fit', + + xtermjs: false, + + border: false, + + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.consoleType) { + throw "no console type specified"; + } + + if (!me.vmid && me.consoleType !== 'shell' && me.consoleType !== 'cmd') { + throw "no VM ID specified"; + } + + // always use same iframe, to avoid running several noVnc clients + // at same time (to avoid performance problems) + var box = Ext.create('Ext.ux.IFrame', { itemid : "vncconsole" }); + + var type = me.xtermjs ? 'xtermjs' : 'novnc'; + Ext.apply(me, { + items: box, + listeners: { + activate: function() { + var queryDict = { + console: me.consoleType, // kvm, lxc, upgrade or shell + vmid: me.vmid, + node: me.nodename, + cmd: me.cmd, + resize: 'scale' + }; + queryDict[type] = 1; + PVE.Utils.cleanEmptyObjectKeys(queryDict); + var url = '/?' + Ext.Object.toQueryString(queryDict); + box.load(url); + } + } + }); + + me.callParent(); + + me.on('afterrender', function() { + me.focus(); + }); + } +}); + +Ext.define('PVE.data.PermPathStore', { + extend: 'Ext.data.Store', + alias: 'store.pvePermPath', + fields: [ 'value' ], + autoLoad: false, + data: [ + {'value': '/'}, + {'value': '/access'}, + {'value': '/nodes'}, + {'value': '/pool'}, + {'value': '/storage'}, + {'value': '/vms'} + ], + + constructor: function(config) { + var me = this; + + config = config || {}; + + me.callParent([config]); + + me.suspendEvents(); + PVE.data.ResourceStore.each(function(record) { + switch (record.get('type')) { + case 'node': + me.add({value: '/nodes/' + record.get('text')}); + break; + + case 'qemu': + me.add({value: '/vms/' + record.get('vmid')}); + break; + + case 'lxc': + me.add({value: '/vms/' + record.get('vmid')}); + break; + + case 'storage': + me.add({value: '/storage/' + record.get('storage')}); + break; + case 'pool': + me.add({value: '/pool/' + record.get('pool')}); + break; + } + }); + me.resumeEvents(); + + me.fireEvent('refresh', me); + me.fireEvent('datachanged', me); + + me.sort({ + property: 'value', + direction: 'ASC' + }); + } +}); +Ext.define('PVE.data.ResourceStore', { + extend: 'Proxmox.data.UpdateStore', + singleton: true, + + findVMID: function(vmid) { + var me = this, i; + + return (me.findExact('vmid', parseInt(vmid, 10)) >= 0); + }, + + // returns the cached data from all nodes + getNodes: function() { + var me = this; + + var nodes = []; + me.each(function(record) { + if (record.get('type') == "node") { + nodes.push( record.getData() ); + } + }); + + return nodes; + }, + + storageIsShared: function(storage_path) { + var me = this; + + var index = me.findExact('id', storage_path); + + return me.getAt(index).data.shared; + }, + + guestNode: function(vmid) { + var me = this; + + var index = me.findExact('vmid', parseInt(vmid, 10)); + + return me.getAt(index).data.node; + }, + + constructor: function(config) { + // fixme: how to avoid those warnings + /*jslint confusion: true */ + + var me = this; + + config = config || {}; + + var field_defaults = { + type: { + header: gettext('Type'), + type: 'string', + renderer: PVE.Utils.render_resource_type, + sortable: true, + hideable: false, + width: 100 + }, + id: { + header: 'ID', + type: 'string', + hidden: true, + sortable: true, + width: 80 + }, + running: { + header: gettext('Online'), + type: 'boolean', + renderer: Proxmox.Utils.format_boolean, + hidden: true, + convert: function(value, record) { + var info = record.data; + return (Ext.isNumeric(info.uptime) && (info.uptime > 0)); + } + }, + text: { + header: gettext('Description'), + type: 'string', + sortable: true, + width: 200, + convert: function(value, record) { + var info = record.data; + var text; + + if (value) { + return value; + } + + if (Ext.isNumeric(info.vmid) && info.vmid > 0) { + text = String(info.vmid); + if (info.name) { + text += " (" + info.name + ')'; + } + } else { // node, pool, storage + text = info[info.type] || info.id; + if (info.node && info.type !== 'node') { + text += " (" + info.node + ")"; + } + } + + return text; + } + }, + vmid: { + header: 'VMID', + type: 'integer', + hidden: true, + sortable: true, + width: 80 + }, + name: { + header: gettext('Name'), + hidden: true, + sortable: true, + type: 'string' + }, + disk: { + header: gettext('Disk usage'), + type: 'integer', + renderer: PVE.Utils.render_disk_usage, + sortable: true, + width: 100, + hidden: true + }, + diskuse: { + header: gettext('Disk usage') + " %", + type: 'number', + sortable: true, + renderer: PVE.Utils.render_disk_usage_percent, + width: 100, + calculate: PVE.Utils.calculate_disk_usage, + sortType: 'asFloat' + }, + maxdisk: { + header: gettext('Disk size'), + type: 'integer', + renderer: PVE.Utils.render_size, + sortable: true, + hidden: true, + width: 100 + }, + mem: { + header: gettext('Memory usage'), + type: 'integer', + renderer: PVE.Utils.render_mem_usage, + sortable: true, + hidden: true, + width: 100 + }, + memuse: { + header: gettext('Memory usage') + " %", + type: 'number', + renderer: PVE.Utils.render_mem_usage_percent, + calculate: PVE.Utils.calculate_mem_usage, + sortType: 'asFloat', + sortable: true, + width: 100 + }, + maxmem: { + header: gettext('Memory size'), + type: 'integer', + renderer: PVE.Utils.render_size, + hidden: true, + sortable: true, + width: 100 + }, + cpu: { + header: gettext('CPU usage'), + type: 'float', + renderer: PVE.Utils.render_cpu, + sortable: true, + width: 100 + }, + maxcpu: { + header: gettext('maxcpu'), + type: 'integer', + hidden: true, + sortable: true, + width: 60 + }, + diskread: { + header: gettext('Total Disk Read'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100 + }, + diskwrite: { + header: gettext('Total Disk Write'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100 + }, + netin: { + header: gettext('Total NetIn'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100 + }, + netout: { + header: gettext('Total NetOut'), + type: 'integer', + hidden: true, + sortable: true, + renderer: Proxmox.Utils.format_size, + width: 100 + }, + template: { + header: gettext('Template'), + type: 'integer', + hidden: true, + sortable: true, + width: 60 + }, + uptime: { + header: gettext('Uptime'), + type: 'integer', + renderer: Proxmox.Utils.render_uptime, + sortable: true, + width: 110 + }, + node: { + header: gettext('Node'), + type: 'string', + hidden: true, + sortable: true, + width: 110 + }, + storage: { + header: gettext('Storage'), + type: 'string', + hidden: true, + sortable: true, + width: 110 + }, + pool: { + header: gettext('Pool'), + type: 'string', + hidden: true, + sortable: true, + width: 110 + }, + hastate: { + header: gettext('HA State'), + type: 'string', + defaultValue: 'unmanaged', + hidden: true, + sortable: true + }, + status: { + header: gettext('Status'), + type: 'string', + hidden: true, + sortable: true, + width: 110 + }, + lock: { + header: gettext('Lock'), + type: 'string', + hidden: true, + sortable: true, + width: 110 + } + }; + + var fields = []; + var fieldNames = []; + Ext.Object.each(field_defaults, function(key, value) { + var field = {name: key, type: value.type}; + if (Ext.isDefined(value.convert)) { + field.convert = value.convert; + } + + if (Ext.isDefined(value.calculate)) { + field.calculate = value.calculate; + } + + if (Ext.isDefined(value.defaultValue)) { + field.defaultValue = value.defaultValue; + } + + fields.push(field); + fieldNames.push(key); + }); + + Ext.define('PVEResources', { + extend: "Ext.data.Model", + fields: fields, + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/resources' + } + }); + + Ext.define('PVETree', { + extend: "Ext.data.Model", + fields: fields, + proxy: { type: 'memory' } + }); + + Ext.apply(config, { + storeid: 'PVEResources', + model: 'PVEResources', + defaultColumns: function() { + var res = []; + Ext.Object.each(field_defaults, function(field, info) { + var fi = Ext.apply({ dataIndex: field }, info); + res.push(fi); + }); + return res; + }, + fieldNames: fieldNames + }); + + me.callParent([config]); + } +}); +Ext.define('pve-domains', { + extend: "Ext.data.Model", + fields: [ + 'realm', 'type', 'comment', 'default', 'tfa', + { + name: 'descr', + // Note: We use this in the RealmComboBox.js (see Bug #125) + convert: function(value, record) { + if (value) { + return value; + } + + var info = record.data; + // return realm if there is no comment + var text = info.comment || info.realm; + + if (info.tfa) { + text += " (+ " + info.tfa + ")"; + } + + return Ext.String.htmlEncode(text); + } + } + ], + idProperty: 'realm', + proxy: { + type: 'proxmox', + url: "/api2/json/access/domains" + } +}); +Ext.define('pve-rrd-node', { + extend: 'Ext.data.Model', + fields: [ + { + name:'cpu', + // percentage + convert: function(value) { + return value*100; + } + }, + { + name:'iowait', + // percentage + convert: function(value) { + return value*100; + } + }, + 'loadavg', + 'maxcpu', + 'memtotal', + 'memused', + 'netin', + 'netout', + 'roottotal', + 'rootused', + 'swaptotal', + 'swapused', + { type: 'date', dateFormat: 'timestamp', name: 'time' } + ] +}); + +Ext.define('pve-rrd-guest', { + extend: 'Ext.data.Model', + fields: [ + { + name:'cpu', + // percentage + convert: function(value) { + return value*100; + } + }, + 'maxcpu', + 'netin', + 'netout', + 'mem', + 'maxmem', + 'disk', + 'maxdisk', + 'diskread', + 'diskwrite', + { type: 'date', dateFormat: 'timestamp', name: 'time' } + ] +}); + +Ext.define('pve-rrd-storage', { + extend: 'Ext.data.Model', + fields: [ + 'used', + 'total', + { type: 'date', dateFormat: 'timestamp', name: 'time' } + ] +}); +Ext.define('PVE.form.VlanField', { + extend: 'Ext.form.field.Number', + alias: ['widget.pveVlanField'], + + deleteEmpty: false, + + emptyText: 'no VLAN', + + fieldLabel: gettext('VLAN Tag'), + + allowBlank: true, + + getSubmitData: function() { + var me = this, + data = null, + val; + if (!me.disabled && me.submitValue) { + val = me.getSubmitValue(); + if (val) { + data = {}; + data[me.getName()] = val; + } else if (me.deleteEmpty) { + data = {}; + data['delete'] = me.getName(); + } + } + return data; + }, + + initComponent: function() { + var me = this; + + Ext.apply(me, { + minValue: 1, + maxValue: 4094 + }); + + me.callParent(); + } +}); +// boolean type including 'Default' (delete property from file) +Ext.define('PVE.form.Boolean', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.booleanfield'], + comboItems: [ + ['__default__', gettext('Default')], + [1, gettext('Yes')], + [0, gettext('No')] + ] +}); +Ext.define('PVE.form.CompressionSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveCompressionSelector'], + comboItems: [ + ['0', Proxmox.Utils.noneText], + ['lzo', 'LZO (' + gettext('fast') + ')'], + ['gzip', 'GZIP (' + gettext('good') + ')'] + ] +}); +Ext.define('PVE.form.PoolSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pvePoolSelector'], + + allowBlank: false, + valueField: 'poolid', + displayField: 'poolid', + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-pools', + sorters: 'poolid' + }); + + Ext.apply(me, { + store: store, + autoSelect: false, + listConfig: { + columns: [ + { + header: gettext('Pool'), + sortable: true, + dataIndex: 'poolid', + flex: 1 + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ] + } + }); + + me.callParent(); + + store.load(); + } + +}, function() { + + Ext.define('pve-pools', { + extend: 'Ext.data.Model', + fields: [ 'poolid', 'comment' ], + proxy: { + type: 'proxmox', + url: "/api2/json/pools" + }, + idProperty: 'poolid' + }); + +}); +Ext.define('PVE.form.PrivilegesSelector', { + extend: 'Proxmox.form.KVComboBox', + xtype: 'pvePrivilegesSelector', + + multiSelect: true, + + initComponent: function() { + var me = this; + + // So me.store is available. + me.callParent(); + + Proxmox.Utils.API2Request({ + url: '/access/roles/Administrator', + method: 'GET', + success: function(response, options) { + var data = [], key; + /*jslint forin: true */ + for (key in response.result.data) { + data.push([key, key]); + } + /*jslint forin: false */ + + me.store.setData(data); + + me.store.sort({ + property: 'key', + direction: 'ASC' + }); + }, + + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } +}); +Ext.define('pve-groups', { + extend: 'Ext.data.Model', + fields: [ 'groupid', 'comment' ], + proxy: { + type: 'proxmox', + url: "/api2/json/access/groups" + }, + idProperty: 'groupid' +}); + +Ext.define('PVE.form.GroupSelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pveGroupSelector', + + allowBlank: false, + autoSelect: false, + valueField: 'groupid', + displayField: 'groupid', + listConfig: { + columns: [ + { + header: gettext('Group'), + sortable: true, + dataIndex: 'groupid', + flex: 1 + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ] + }, + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-groups', + sorters: [{ + property: 'groupid' + }] + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + store.load(); + } +}); +Ext.define('PVE.form.UserSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveUserSelector'], + + allowBlank: false, + autoSelect: false, + valueField: 'userid', + displayField: 'userid', + + editable: true, + anyMatch: true, + forceSelection: true, + + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-users', + sorters: [{ + property: 'userid' + }] + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: gettext('User'), + sortable: true, + dataIndex: 'userid', + flex: 1 + }, + { + header: gettext('Name'), + sortable: true, + renderer: PVE.Utils.render_full_name, + dataIndex: 'firstname', + flex: 1 + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ] + } + }); + + me.callParent(); + + store.load({ params: { enabled: 1 }}); + } + +}, function() { + + Ext.define('pve-users', { + extend: 'Ext.data.Model', + fields: [ + 'userid', 'firstname', 'lastname' , 'email', 'comment', + { type: 'boolean', name: 'enable' }, + { type: 'date', dateFormat: 'timestamp', name: 'expire' } + ], + proxy: { + type: 'proxmox', + url: "/api2/json/access/users" + }, + idProperty: 'userid' + }); + +}); + + +Ext.define('PVE.form.RoleSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveRoleSelector'], + + allowBlank: false, + autoSelect: false, + valueField: 'roleid', + displayField: 'roleid', + initComponent: function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-roles', + sorters: [{ + property: 'roleid' + }] + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: gettext('Role'), + sortable: true, + dataIndex: 'roleid', + flex: 1 + } + ] + } + }); + + me.callParent(); + + store.load(); + } + +}, function() { + + Ext.define('pve-roles', { + extend: 'Ext.data.Model', + fields: [ 'roleid', 'privs' ], + proxy: { + type: 'proxmox', + url: "/api2/json/access/roles" + }, + idProperty: 'roleid' + }); + +}); +Ext.define('PVE.form.GuestIDSelector', { + extend: 'Ext.form.field.Number', + alias: 'widget.pveGuestIDSelector', + + allowBlank: false, + + minValue: 100, + + maxValue: 999999999, + + validateExists: undefined, + + loadNextFreeID: false, + + guestType: undefined, + + validator: function(value) { + var me = this; + + if (!Ext.isNumeric(value) || + value < me.minValue || + value > me.maxValue) { + // check is done by ExtJS + return true; + } + + if (me.validateExists === true && !me.exists) { + return me.unknownID; + } + + if (me.validateExists === false && me.exists) { + return me.inUseID; + } + + return true; + }, + + initComponent: function() { + var me = this; + var label = '{0} ID'; + var unknownID = gettext('This {0} ID does not exists'); + var inUseID = gettext('This {0} ID is already in use'); + var type = 'CT/VM'; + + if (me.guestType === 'lxc') { + type = 'CT'; + } else if (me.guestType === 'qemu') { + type = 'VM'; + } + + me.label = Ext.String.format(label, type); + me.unknownID = Ext.String.format(unknownID, type); + me.inUseID = Ext.String.format(inUseID, type); + + Ext.apply(me, { + fieldLabel: me.label, + listeners: { + 'change': function(field, newValue, oldValue) { + if (!Ext.isDefined(me.validateExists)) { + return; + } + Proxmox.Utils.API2Request({ + params: { vmid: newValue }, + url: '/cluster/nextid', + method: 'GET', + success: function(response, opts) { + me.exists = false; + me.validate(); + }, + failure: function(response, opts) { + me.exists = true; + me.validate(); + } + }); + } + } + }); + + me.callParent(); + + if (me.loadNextFreeID) { + Proxmox.Utils.API2Request({ + url: '/cluster/nextid', + method: 'GET', + success: function(response, opts) { + me.setRawValue(response.result.data); + } + }); + } + } +}); +Ext.define('PVE.form.MemoryField', { + extend: 'Ext.form.field.Number', + alias: 'widget.pveMemoryField', + + allowBlank: false, + + hotplug: false, + + minValue: 32, + + maxValue: 4178944, + + step: 32, + + value: '512', // qm default + + allowDecimals: false, + + allowExponential: false, + + computeUpDown: function(value) { + var me = this; + + if (!me.hotplug) { + return { up: value + me.step, down: value - me.step }; + } + + var dimm_size = 512; + var prev_dimm_size = 0; + var min_size = 1024; + var current_size = min_size; + var value_up = min_size; + var value_down = min_size; + var value_start = min_size; + + var i, j; + for (j = 0; j < 9; j++) { + for (i = 0; i < 32; i++) { + if ((value >= current_size) && (value < (current_size + dimm_size))) { + value_start = current_size; + value_up = current_size + dimm_size; + value_down = current_size - ((i === 0) ? prev_dimm_size : dimm_size); + } + current_size += dimm_size; + } + prev_dimm_size = dimm_size; + dimm_size = dimm_size*2; + } + + return { up: value_up, down: value_down, start: value_start }; + }, + + onSpinUp: function() { + var me = this; + if (!me.readOnly) { + var res = me.computeUpDown(me.getValue()); + me.setValue(Ext.Number.constrain(res.up, me.minValue, me.maxValue)); + } + }, + + onSpinDown: function() { + var me = this; + if (!me.readOnly) { + var res = me.computeUpDown(me.getValue()); + me.setValue(Ext.Number.constrain(res.down, me.minValue, me.maxValue)); + } + }, + + initComponent: function() { + var me = this; + + if (me.hotplug) { + me.minValue = 1024; + + me.on('blur', function(field) { + var value = me.getValue(); + var res = me.computeUpDown(value); + if (value === res.start || value === res.up || value === res.down) { + return; + } + field.setValue(res.up); + }); + } + + me.callParent(); + } +}); +Ext.define('PVE.form.NetworkCardSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: 'widget.pveNetworkCardSelector', + comboItems: [ + ['e1000', 'Intel E1000'], + ['virtio', 'VirtIO (' + gettext('paravirtualized') + ')'], + ['rtl8139', 'Realtek RTL8139'], + ['vmxnet3', 'VMware vmxnet3'] + ] +}); +Ext.define('PVE.form.DiskFormatSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: 'widget.pveDiskFormatSelector', + comboItems: [ + ['raw', gettext('Raw disk image') + ' (raw)'], + ['qcow2', gettext('QEMU image format') + ' (qcow2)'], + ['vmdk', gettext('VMware image format') + ' (vmdk)'] + ] +}); +Ext.define('PVE.form.DiskSelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pveDiskSelector', + + // can be + // undefined: all + // unused: only unused + // journal_disk: all disks with gpt + diskType: undefined, + + valueField: 'devpath', + displayField: 'devpath', + emptyText: gettext('No Disks unused'), + listConfig: { + width: 600, + columns: [ + { + header: gettext('Device'), + flex: 3, + sortable: true, + dataIndex: 'devpath' + }, + { + header: gettext('Size'), + flex: 2, + sortable: false, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + }, + { + header: gettext('Serial'), + flex: 5, + sortable: true, + dataIndex: 'serial' + } + ] + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + if (!nodename) { + throw "no node name specified"; + } + + var store = Ext.create('Ext.data.Store', { + filterOnLoad: true, + model: 'pve-disk-list', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/disks/list", + extraParams: { type: me.diskType } + }, + sorters: [ + { + property : 'devpath', + direction: 'ASC' + } + ] + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + store.load(); + } +}, function() { + + Ext.define('pve-disk-list', { + extend: 'Ext.data.Model', + fields: [ 'devpath', 'used', { name: 'size', type: 'number'}, + {name: 'osdid', type: 'number'}, + 'vendor', 'model', 'serial'], + idProperty: 'devpath' + }); +}); +Ext.define('PVE.form.BusTypeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: 'widget.pveBusSelector', + + noVirtIO: false, + + initComponent: function() { + var me = this; + + me.comboItems = [['ide', 'IDE'], ['sata', 'SATA']]; + + if (!me.noVirtIO) { + me.comboItems.push(['virtio', 'VirtIO Block']); + } + + me.comboItems.push(['scsi', 'SCSI']); + + me.callParent(); + } +}); +Ext.define('PVE.form.ControllerSelector', { + extend: 'Ext.form.FieldContainer', + alias: 'widget.pveControllerSelector', + + statics: { + maxIds: { + ide: 3, + sata: 5, + virtio: 15, + scsi: 13 + } + }, + + noVirtIO: false, + + vmconfig: {}, // used to check for existing devices + + sortByPreviousUsage: function(vmconfig, controllerList) { + + var usedControllers = Ext.clone(PVE.form.ControllerSelector.maxIds); + + var type; + for (type in usedControllers) { + if(usedControllers.hasOwnProperty(type)) { + usedControllers[type] = 0; + } + } + + var property; + for (property in vmconfig) { + if (vmconfig.hasOwnProperty(property)) { + if (property.match(PVE.Utils.bus_match) && !vmconfig[property].match(/media=cdrom/)) { + var foundController = property.match(PVE.Utils.bus_match)[1]; + usedControllers[foundController]++; + } + } + } + + var vmDefaults = PVE.qemu.OSDefaults[vmconfig.ostype]; + + var sortPriority = vmDefaults && vmDefaults.busPriority + ? vmDefaults.busPriority : PVE.qemu.OSDefaults.generic; + + var sortedList = Ext.clone(controllerList); + sortedList.sort(function(a,b) { + if (usedControllers[b] == usedControllers[a]) { + return sortPriority[b] - sortPriority[a]; + } + return usedControllers[b] - usedControllers[a]; + }); + + return sortedList; + }, + + setVMConfig: function(vmconfig, autoSelect) { + var me = this; + + me.vmconfig = Ext.apply({}, vmconfig); + + var clist = ['ide', 'virtio', 'scsi', 'sata']; + var bussel = me.down('field[name=controller]'); + var deviceid = me.down('field[name=deviceid]'); + + if (autoSelect === 'cdrom') { + clist = ['ide', 'scsi', 'sata']; + if (!Ext.isDefined(me.vmconfig.ide2)) { + bussel.setValue('ide'); + deviceid.setValue(2); + return; + } + } else { + // in most cases we want to add a disk to the same controller + // we previously used + clist = me.sortByPreviousUsage(me.vmconfig, clist); + } + + Ext.Array.each(clist, function(controller) { + var confid, i; + bussel.setValue(controller); + for (i = 0; i <= PVE.form.ControllerSelector.maxIds[controller]; i++) { + confid = controller + i.toString(); + if (!Ext.isDefined(me.vmconfig[confid])) { + deviceid.setValue(i); + return false; // break + } + } + }); + deviceid.validate(); + }, + + initComponent: function() { + var me = this; + + Ext.apply(me, { + fieldLabel: gettext('Bus/Device'), + layout: 'hbox', + defaults: { + hideLabel: true + }, + items: [ + { + xtype: 'pveBusSelector', + name: 'controller', + value: PVE.qemu.OSDefaults.generic.busType, + noVirtIO: me.noVirtIO, + allowBlank: false, + flex: 2, + listeners: { + change: function(t, value) { + if (!value) { + return; + } + var field = me.down('field[name=deviceid]'); + field.setMaxValue(PVE.form.ControllerSelector.maxIds[value]); + field.validate(); + } + } + }, + { + xtype: 'proxmoxintegerfield', + name: 'deviceid', + minValue: 0, + maxValue: PVE.form.ControllerSelector.maxIds.ide, + value: '0', + flex: 1, + allowBlank: false, + validator: function(value) { + /*jslint confusion: true */ + if (!me.rendered) { + return; + } + var field = me.down('field[name=controller]'); + var controller = field.getValue(); + var confid = controller + value; + if (Ext.isDefined(me.vmconfig[confid])) { + return "This device is already in use."; + } + return true; + } + } + ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.form.EmailNotificationSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveEmailNotificationSelector'], + comboItems: [ + ['always', gettext('Always')], + ['failure', gettext('On failure only')] + ] +}); +/*global Proxmox*/ +Ext.define('PVE.form.RealmComboBox', { + extend: 'Ext.form.field.ComboBox', + alias: ['widget.pveRealmComboBox'], + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.store.on('load', this.onLoad, view); + }, + + onLoad: function(store, records, success) { + if (!success) { + return; + } + var me = this; + var val = me.getValue(); + if (!val || !me.store.findRecord('realm', val)) { + var def = 'pam'; + Ext.each(records, function(rec) { + if (rec.data && rec.data['default']) { + def = rec.data.realm; + } + }); + me.setValue(def); + } + } + }, + + fieldLabel: gettext('Realm'), + name: 'realm', + queryMode: 'local', + allowBlank: false, + editable: false, + forceSelection: true, + autoSelect: false, + triggerAction: 'all', + valueField: 'realm', + displayField: 'descr', + getState: function() { + return { value: this.getValue() }; + }, + applyState : function(state) { + if (state && state.value) { + this.setValue(state.value); + } + }, + stateEvents: [ 'select' ], + stateful: true, // last chosen auth realm is saved between page reloads + id: 'pveloginrealm', // We need stable ids when using stateful, not autogenerated + stateID: 'pveloginrealm', + + needOTP: function(realm) { + var me = this; + // use exact match + var rec = me.store.findRecord('realm', realm, 0, false, false, true); + return rec && rec.data && rec.data.tfa ? rec.data.tfa : undefined; + }, + + store: { + model: 'pve-domains', + autoLoad: true + } +}); +/* + * Top left combobox, used to select a view of the underneath RessourceTree + */ +Ext.define('PVE.form.ViewSelector', { + extend: 'Ext.form.field.ComboBox', + alias: ['widget.pveViewSelector'], + + editable: false, + allowBlank: false, + forceSelection: true, + autoSelect: false, + valueField: 'key', + displayField: 'value', + hideLabel: true, + queryMode: 'local', + + initComponent: function() { + var me = this; + + var default_views = { + server: { + text: gettext('Server View'), + groups: ['node'] + }, + folder: { + text: gettext('Folder View'), + groups: ['type'] + }, + storage: { + text: gettext('Storage View'), + groups: ['node'], + filterfn: function(node) { + return node.data.type === 'storage' || node.data.type === 'node'; + } + }, + pool: { + text: gettext('Pool View'), + groups: ['pool'], + // Pool View only lists VMs and Containers + filterfn: function(node) { + return node.data.type === 'qemu' || node.data.type === 'lxc' || node.data.type === 'openvz' || + node.data.type === 'pool'; + } + } + }; + + var groupdef = []; + Ext.Object.each(default_views, function(viewname, value) { + groupdef.push([viewname, value.text]); + }); + + var store = Ext.create('Ext.data.Store', { + model: 'KeyValue', + proxy: { + type: 'memory', + reader: 'array' + }, + data: groupdef, + autoload: true + }); + + Ext.apply(me, { + store: store, + value: groupdef[0][0], + getViewFilter: function() { + var view = me.getValue(); + return Ext.apply({ id: view }, default_views[view] || default_views.server); + }, + + getState: function() { + return { value: me.getValue() }; + }, + + applyState : function(state, doSelect) { + var view = me.getValue(); + if (state && state.value && (view != state.value)) { + var record = store.findRecord('key', state.value); + if (record) { + me.setValue(state.value, true); + if (doSelect) { + me.fireEvent('select', me, [record]); + } + } + } + }, + stateEvents: [ 'select' ], + stateful: true, + stateId: 'pveview', + id: 'view' + }); + + me.callParent(); + + var statechange = function(sp, key, value) { + if (key === me.id) { + me.applyState(value, true); + } + }; + + var sp = Ext.state.Manager.getProvider(); + me.mon(sp, 'statechange', statechange, me); + } +}); +Ext.define('PVE.form.NodeSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveNodeSelector'], + + // invalidate nodes which are offline + onlineValidator: false, + + selectCurNode: false, + + // do not allow those nodes (array) + disallowedNodes: undefined, + + // only allow those nodes (array) + allowedNodes: undefined, + // set default value to empty array, else it inits it with + // null and after the store load it is an empty array, + // triggering dirtychange + value: [], + valueField: 'node', + displayField: 'node', + store: { + fields: [ 'node', 'cpu', 'maxcpu', 'mem', 'maxmem', 'uptime' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes' + }, + sorters: [ + { + property : 'node', + direction: 'ASC' + }, + { + property : 'mem', + direction: 'DESC' + } + ] + }, + + listConfig: { + columns: [ + { + header: gettext('Node'), + dataIndex: 'node', + sortable: true, + hideable: false, + flex: 1 + }, + { + header: gettext('Memory usage') + " %", + renderer: PVE.Utils.render_mem_usage_percent, + sortable: true, + width: 100, + dataIndex: 'mem' + }, + { + header: gettext('CPU usage'), + renderer: PVE.Utils.render_cpu, + sortable: true, + width: 100, + dataIndex: 'cpu' + } + ] + }, + + validator: function(value) { + /*jslint confusion: true */ + var me = this; + if (!me.onlineValidator || (me.allowBlank && !value)) { + return true; + } + + var offline = []; + var notAllowed = []; + + Ext.Array.each(value.split(/\s*,\s*/), function(node) { + var rec = me.store.findRecord(me.valueField, node); + if (!(rec && rec.data) || rec.data.status !== 'online') { + offline.push(node); + } else if (me.allowedNodes && !Ext.Array.contains(me.allowedNodes, node)) { + notAllowed.push(node); + } + }); + + if (value && notAllowed.length !== 0) { + return "Node " + notAllowed.join(', ') + " is not allowed for this action!"; + } + + if (value && offline.length !== 0) { + return "Node " + offline.join(', ') + " seems to be offline!"; + } + return true; + }, + + initComponent: function() { + var me = this; + + if (me.selectCurNode && PVE.curSelectedNode && PVE.curSelectedNode.data.node) { + me.preferredValue = PVE.curSelectedNode.data.node; + } + + me.callParent(); + me.getStore().load(); + + // filter out disallowed nodes + me.getStore().addFilter(new Ext.util.Filter({ + filterFn: function(item) { + if (Ext.isArray(me.disallowedNodes)) { + return !Ext.Array.contains(me.disallowedNodes, item.data.node); + } else { + return true; + } + } + })); + + me.mon(me.getStore(), 'load', function(){ + me.isValid(); + }); + } +}); +Ext.define('PVE.form.FileSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveFileSelector', + + editable: true, + anyMatch: true, + forceSelection: true, + + listeners: { + afterrender: function() { + var me = this; + if (!me.disabled) { + me.setStorage(me.storage, me.nodename); + } + } + }, + + setStorage: function(storage, nodename) { + var me = this; + + var change = false; + if (storage && (me.storage !== storage)) { + me.storage = storage; + change = true; + } + + if (nodename && (me.nodename !== nodename)) { + me.nodename = nodename; + change = true; + } + + if (!(me.storage && me.nodename && change)) { + return; + } + + var url = '/api2/json/nodes/' + me.nodename + '/storage/' + me.storage + '/content'; + if (me.storageContent) { + url += '?content=' + me.storageContent; + } + + me.store.setProxy({ + type: 'proxmox', + url: url + }); + + me.store.removeAll(); + me.store.load(); + }, + + setNodename: function(nodename) { + this.setStorage(undefined, nodename); + }, + + store: { + model: 'pve-storage-content' + }, + + allowBlank: false, + autoSelect: false, + valueField: 'volid', + displayField: 'text', + + listConfig: { + width: 600, + columns: [ + { + header: gettext('Name'), + dataIndex: 'text', + hideable: false, + flex: 1 + }, + { + header: gettext('Format'), + width: 60, + dataIndex: 'format' + }, + { + header: gettext('Size'), + width: 100, + dataIndex: 'size', + renderer: Proxmox.Utils.format_size + } + ] + } +}); +Ext.define('PVE.form.StorageSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveStorageSelector', + + allowBlank: false, + valueField: 'storage', + displayField: 'storage', + listConfig: { + width: 450, + columns: [ + { + header: gettext('Name'), + dataIndex: 'storage', + hideable: false, + flex: 1 + }, + { + header: gettext('Type'), + width: 75, + dataIndex: 'type' + }, + { + header: gettext('Avail'), + width: 90, + dataIndex: 'avail', + renderer: Proxmox.Utils.format_size + }, + { + header: gettext('Capacity'), + width: 90, + dataIndex: 'total', + renderer: Proxmox.Utils.format_size + } + ] + }, + + reloadStorageList: function() { + var me = this; + if (!me.nodename) { + return; + } + + var params = { + format: 1 + }; + var url = '/api2/json/nodes/' + me.nodename + '/storage'; + if (me.storageContent) { + params.content = me.storageContent; + } + if (me.targetNode) { + params.target = me.targetNode; + params.enabled = 1; // skip disabled storages + } + me.store.setProxy({ + type: 'proxmox', + url: url, + extraParams: params + }); + + me.store.load(); + + }, + + setTargetNode: function(targetNode) { + var me = this; + + if (!targetNode || (me.targetNode === targetNode)) { + return; + } + + me.targetNode = targetNode; + + me.reloadStorageList(); + }, + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + + me.reloadStorageList(); + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + me.nodename = undefined; + + var store = Ext.create('Ext.data.Store', { + model: 'pve-storage-status', + sorters: { + property: 'storage', + order: 'DESC' + } + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + if (nodename) { + me.setNodename(nodename); + } + } +}, function() { + + Ext.define('pve-storage-status', { + extend: 'Ext.data.Model', + fields: [ 'storage', 'active', 'type', 'avail', 'total' ], + idProperty: 'storage' + }); + +}); +Ext.define('PVE.form.DiskStorageSelector', { + extend: 'Ext.container.Container', + alias: 'widget.pveDiskStorageSelector', + + layout: 'fit', + defaults: { + margin: '0 0 5 0' + }, + + // the fieldLabel for the storageselector + storageLabel: gettext('Storage'), + + // the content to show (e.g., images or rootdir) + storageContent: undefined, + + // if true, selects the first available storage + autoSelect: false, + + allowBlank: false, + emptyText: '', + + // hides the selection field + // this is always hidden on creation, + // and only shown when the storage needs a selection and + // hideSelection is not true + hideSelection: undefined, + + // hides the size field (e.g, for the efi disk dialog) + hideSize: false, + + // sets the initial size value + // string because else we get a type confusion + defaultSize: '32', + + changeStorage: function(f, value) { + var me = this; + var formatsel = me.getComponent('diskformat'); + var hdfilesel = me.getComponent('hdimage'); + var hdsizesel = me.getComponent('disksize'); + + // initial store load, and reset/deletion of the storage + if (!value) { + hdfilesel.setDisabled(true); + hdfilesel.setVisible(false); + + formatsel.setDisabled(true); + return; + } + + var rec = f.store.getById(value); + // if the storage is not defined, or valid, + // we cannot know what to enable/disable + if (!rec) { + return; + } + + var selectformat = false; + if (rec.data.format) { + var format = rec.data.format[0]; // 0 is the formats, 1 the default in the backend + delete format.subvol; // we never need subvol in the gui + selectformat = (Ext.Object.getSize(format) > 1); + } + + var select = !!rec.data.select_existing && !me.hideSelection; + + formatsel.setDisabled(!selectformat); + formatsel.setValue(selectformat ? 'qcow2' : 'raw'); + + hdfilesel.setDisabled(!select); + hdfilesel.setVisible(select); + if (select) { + hdfilesel.setStorage(value); + } + + hdsizesel.setDisabled(select || me.hideSize); + hdsizesel.setVisible(!select && !me.hideSize); + }, + + setNodename: function(nodename) { + var me = this; + var hdstorage = me.getComponent('hdstorage'); + var hdfilesel = me.getComponent('hdimage'); + + hdstorage.setNodename(nodename); + hdfilesel.setNodename(nodename); + }, + + setDisabled: function(value) { + var me = this; + var hdstorage = me.getComponent('hdstorage'); + + // reset on disable + if (value) { + hdstorage.setValue(); + } + hdstorage.setDisabled(value); + + // disabling does not always fire this event and we do not need + // the value of the validity + hdstorage.fireEvent('validitychange'); + }, + + initComponent: function() { + var me = this; + + me.items = [ + { + xtype: 'pveStorageSelector', + itemId: 'hdstorage', + name: 'hdstorage', + reference: 'hdstorage', + fieldLabel: me.storageLabel, + nodename: me.nodename, + storageContent: me.storageContent, + disabled: me.disabled, + autoSelect: me.autoSelect, + allowBlank: me.allowBlank, + emptyText: me.emptyText, + listeners: { + change: { + fn: me.changeStorage, + scope: me + } + } + }, + { + xtype: 'pveFileSelector', + name: 'hdimage', + reference: 'hdimage', + itemId: 'hdimage', + fieldLabel: gettext('Disk image'), + nodename: me.nodename, + disabled: true, + hidden: true + }, + { + xtype: 'numberfield', + itemId: 'disksize', + reference: 'disksize', + name: 'disksize', + fieldLabel: gettext('Disk size') + ' (GiB)', + hidden: me.hideSize, + disabled: me.hideSize, + minValue: 0.001, + maxValue: 128*1024, + decimalPrecision: 3, + value: me.defaultSize, + allowBlank: false + }, + { + xtype: 'pveDiskFormatSelector', + itemId: 'diskformat', + reference: 'diskformat', + name: 'diskformat', + fieldLabel: gettext('Format'), + nodename: me.nodename, + disabled: true, + hidden: me.storageContent === 'rootdir', + value: 'qcow2', + allowBlank: false + } + ]; + + // use it to disable the children but not ourself + me.disabled = false; + + me.callParent(); + } +}); +Ext.define('PVE.form.BridgeSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.PVE.form.BridgeSelector'], + + bridgeType: 'any_bridge', // bridge, OVSBridge or any_bridge + + store: { + fields: [ 'iface', 'active', 'type' ], + filterOnLoad: true, + sorters: [ + { + property : 'iface', + direction: 'ASC' + } + ] + }, + valueField: 'iface', + displayField: 'iface', + listConfig: { + columns: [ + { + header: gettext('Bridge'), + dataIndex: 'iface', + hideable: false, + width: 100 + }, + { + header: gettext('Active'), + width: 60, + dataIndex: 'active', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('Comment'), + dataIndex: 'comments', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ] + }, + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/network?type=' + + me.bridgeType + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + me.nodename = undefined; + + me.callParent(); + + me.setNodename(nodename); + } +}); + +Ext.define('PVE.form.PCISelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pvePCISelector', + + store: { + fields: [ 'id','vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev' ], + filterOnLoad: true, + sorters: [ + { + property : 'id', + direction: 'ASC' + } + ] + }, + + autoSelect: false, + valueField: 'id', + displayField: 'id', + + // can contain a load callback for the store + // useful to determine the state of the IOMMU + onLoadCallBack: undefined, + + listConfig: { + width: 800, + columns: [ + { + header: 'ID', + dataIndex: 'id', + width: 80 + }, + { + header: gettext('IOMMU Group'), + dataIndex: 'iommugroup', + width: 50 + }, + { + header: gettext('Vendor'), + dataIndex: 'vendor_name', + flex: 2 + }, + { + header: gettext('Device'), + dataIndex: 'device_name', + flex: 6 + }, + { + header: gettext('Mediated Devices'), + dataIndex: 'mdev', + flex: 1, + renderer: function(val) { + return Proxmox.Utils.format_boolean(!!val); + } + } + ] + }, + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/hardware/pci' + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + me.nodename = undefined; + + me.callParent(); + + if (me.onLoadCallBack !== undefined) { + me.mon(me.getStore(), 'load', me.onLoadCallBack); + } + + me.setNodename(nodename); + } +}); + +Ext.define('PVE.form.MDevSelector', { + extend: 'Proxmox.form.ComboGrid', + xtype: 'pveMDevSelector', + + store: { + fields: [ 'type','available', 'description' ], + filterOnLoad: true, + sorters: [ + { + property : 'type', + direction: 'ASC' + } + ] + }, + autoSelect: false, + valueField: 'type', + displayField: 'type', + listConfig: { + columns: [ + { + header: gettext('Type'), + dataIndex: 'type', + flex: 1 + }, + { + header: gettext('Available'), + dataIndex: 'available', + width: 80 + }, + { + header: gettext('Description'), + dataIndex: 'description', + flex: 1, + renderer: function(value) { + if (!value) { + return ''; + } + + return value.split('\n').join('
'); + } + } + ] + }, + + setPciID: function(pciid, force) { + var me = this; + + if (!force && (!pciid || (me.pciid === pciid))) { + return; + } + + me.pciid = pciid; + me.updateProxy(); + }, + + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + me.updateProxy(); + }, + + updateProxy: function() { + var me = this; + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/hardware/pci/' + me.pciid + '/mdev' + }); + me.store.load(); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw 'no node name specified'; + } + + me.callParent(); + + if (me.pciid) { + me.setPciID(me.pciid, true); + } + } +}); + +Ext.define('PVE.form.SecurityGroupsSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveSecurityGroupsSelector'], + + valueField: 'group', + displayField: 'group', + initComponent: function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: [ 'group', 'comment' ], + idProperty: 'group', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/firewall/groups" + }, + sorters: { + property: 'group', + order: 'DESC' + } + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: gettext('Security Group'), + dataIndex: 'group', + hideable: false, + width: 100 + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ] + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.form.IPRefSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveIPRefSelector'], + + base_url: undefined, + + preferredValue: '', // hack: else Form sets dirty flag? + + ref_type: undefined, // undefined = any [undefined, 'ipset' or 'alias'] + + valueField: 'ref', + displayField: 'ref', + + initComponent: function() { + var me = this; + + if (!me.base_url) { + throw "no base_url specified"; + } + + var url = "/api2/json" + me.base_url; + if (me.ref_type) { + url += "?type=" + me.ref_type; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: [ 'type', 'name', 'ref', 'comment' ], + idProperty: 'ref', + proxy: { + type: 'proxmox', + url: url + }, + sorters: { + property: 'ref', + order: 'DESC' + } + }); + + var disable_query_for_ips = function(f, value) { + if (value === null || + value.match(/^\d/)) { // IP address starts with \d + f.queryDelay = 9999999999; // hack: disable with long delay + } else { + f.queryDelay = 10; + } + }; + + var columns = []; + + if (!me.ref_type) { + columns.push({ + header: gettext('Type'), + dataIndex: 'type', + hideable: false, + width: 60 + }); + } + + columns.push( + { + header: gettext('Name'), + dataIndex: 'ref', + hideable: false, + width: 140 + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ); + + Ext.apply(me, { + store: store, + listConfig: { columns: columns } + }); + + me.on('change', disable_query_for_ips); + + me.callParent(); + } +}); + +Ext.define('PVE.form.IPProtocolSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveIPProtocolSelector'], + valueField: 'p', + displayField: 'p', + listConfig: { + columns: [ + { + header: gettext('Protocol'), + dataIndex: 'p', + hideable: false, + sortable: false, + width: 100 + }, + { + header: gettext('Number'), + dataIndex: 'n', + hideable: false, + sortable: false, + width: 50 + }, + { + header: gettext('Description'), + dataIndex: 'd', + hideable: false, + sortable: false, + flex: 1 + } + ] + }, + store: { + fields: [ 'p', 'd', 'n'], + data: [ + { p: 'tcp', n: 6, d: 'Transmission Control Protocol' }, + { p: 'udp', n: 17, d: 'User Datagram Protocol' }, + { p: 'icmp', n: 1, d: 'Internet Control Message Protocol' }, + { p: 'igmp', n: 2, d: 'Internet Group Management' }, + { p: 'ggp', n: 3, d: 'gateway-gateway protocol' }, + { p: 'ipencap', n: 4, d: 'IP encapsulated in IP' }, + { p: 'st', n: 5, d: 'ST datagram mode' }, + { p: 'egp', n: 8, d: 'exterior gateway protocol' }, + { p: 'igp', n: 9, d: 'any private interior gateway (Cisco)' }, + { p: 'pup', n: 12, d: 'PARC universal packet protocol' }, + { p: 'hmp', n: 20, d: 'host monitoring protocol' }, + { p: 'xns-idp', n: 22, d: 'Xerox NS IDP' }, + { p: 'rdp', n: 27, d: '"reliable datagram" protocol' }, + { p: 'iso-tp4', n: 29, d: 'ISO Transport Protocol class 4 [RFC905]' }, + { p: 'dccp', n: 33, d: 'Datagram Congestion Control Prot. [RFC4340]' }, + { p: 'xtp', n: 36, d: 'Xpress Transfer Protocol' }, + { p: 'ddp', n: 37, d: 'Datagram Delivery Protocol' }, + { p: 'idpr-cmtp', n: 38, d: 'IDPR Control Message Transport' }, + { p: 'ipv6', n: 41, d: 'Internet Protocol, version 6' }, + { p: 'ipv6-route', n: 43, d: 'Routing Header for IPv6' }, + { p: 'ipv6-frag', n: 44, d: 'Fragment Header for IPv6' }, + { p: 'idrp', n: 45, d: 'Inter-Domain Routing Protocol' }, + { p: 'rsvp', n: 46, d: 'Reservation Protocol' }, + { p: 'gre', n: 47, d: 'General Routing Encapsulation' }, + { p: 'esp', n: 50, d: 'Encap Security Payload [RFC2406]' }, + { p: 'ah', n: 51, d: 'Authentication Header [RFC2402]' }, + { p: 'skip', n: 57, d: 'SKIP' }, + { p: 'ipv6-icmp', n: 58, d: 'ICMP for IPv6' }, + { p: 'ipv6-nonxt', n: 59, d: 'No Next Header for IPv6' }, + { p: 'ipv6-opts', n: 60, d: 'Destination Options for IPv6' }, + { p: 'vmtp', n: 81, d: 'Versatile Message Transport' }, + { p: 'eigrp', n: 88, d: 'Enhanced Interior Routing Protocol (Cisco)' }, + { p: 'ospf', n: 89, d: 'Open Shortest Path First IGP' }, + { p: 'ax.25', n: 93, d: 'AX.25 frames' }, + { p: 'ipip', n: 94, d: 'IP-within-IP Encapsulation Protocol' }, + { p: 'etherip', n: 97, d: 'Ethernet-within-IP Encapsulation [RFC3378]' }, + { p: 'encap', n: 98, d: 'Yet Another IP encapsulation [RFC1241]' }, + { p: 'pim', n: 103, d: 'Protocol Independent Multicast' }, + { p: 'ipcomp', n: 108, d: 'IP Payload Compression Protocol' }, + { p: 'vrrp', n: 112, d: 'Virtual Router Redundancy Protocol [RFC5798]' }, + { p: 'l2tp', n: 115, d: 'Layer Two Tunneling Protocol [RFC2661]' }, + { p: 'isis', n: 124, d: 'IS-IS over IPv4' }, + { p: 'sctp', n: 132, d: 'Stream Control Transmission Protocol' }, + { p: 'fc', n: 133, d: 'Fibre Channel' }, + { p: 'mobility-header', n: 135, d: 'Mobility Support for IPv6 [RFC3775]' }, + { p: 'udplite', n: 136, d: 'UDP-Lite [RFC3828]' }, + { p: 'mpls-in-ip', n: 137, d: 'MPLS-in-IP [RFC4023]' }, + { p: 'hip', n: 139, d: 'Host Identity Protocol' }, + { p: 'shim6', n: 140, d: 'Shim6 Protocol [RFC5533]' }, + { p: 'wesp', n: 141, d: 'Wrapped Encapsulating Security Payload' }, + { p: 'rohc', n: 142, d: 'Robust Header Compression' } + ] + } +}); +Ext.define('PVE.form.CPUModelSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.CPUModelSelector'], + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (kvm64)'], + ['486', '486'], + ['athlon', 'athlon'], + ['core2duo', 'core2duo'], + ['coreduo', 'coreduo'], + ['kvm32', 'kvm32'], + ['kvm64', 'kvm64'], + ['pentium', 'pentium'], + ['pentium2', 'pentium2'], + ['pentium3', 'pentium3'], + ['phenom', 'phenom'], + ['qemu32', 'qemu32'], + ['qemu64', 'qemu64'], + ['Conroe', 'Conroe'], + ['Penryn', 'Penryn'], + ['Nehalem', 'Nehalem'], + ['Westmere', 'Westmere'], + ['SandyBridge', 'SandyBridge'], + ['IvyBridge', 'IvyBridge'], + ['Haswell', 'Haswell'], + ['Haswell-noTSX','Haswell-noTSX'], + ['Broadwell', 'Broadwell'], + ['Broadwell-noTSX','Broadwell-noTSX'], + ['Skylake-Client','Skylake-Client'], + ['Opteron_G1', 'Opteron_G1'], + ['Opteron_G2', 'Opteron_G2'], + ['Opteron_G3', 'Opteron_G3'], + ['Opteron_G4', 'Opteron_G4'], + ['Opteron_G5', 'Opteron_G5'], + ['EPYC', 'EPYC'], + ['host', 'host'] + + ] +}); +Ext.define('PVE.form.VNCKeyboardSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.VNCKeyboardSelector'], + comboItems: PVE.Utils.kvm_keymap_array() +}); +Ext.define('PVE.form.CacheTypeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.CacheTypeSelector'], + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + " (" + gettext('No cache') + ")"], + ['directsync', 'Direct sync'], + ['writethrough', 'Write through'], + ['writeback', 'Write back'], + ['unsafe', 'Write back (' + gettext('unsafe') + ')'], + ['none', gettext('No cache')] + ] +}); +Ext.define('PVE.form.SnapshotSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.PVE.form.SnapshotSelector'], + + valueField: 'name', + displayField: 'name', + + loadStore: function(nodename, vmid) { + var me = this; + + if (!nodename) { + return; + } + + me.nodename = nodename; + + if (!vmid) { + return; + } + + me.vmid = vmid; + + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid +'/snapshot' + }); + + me.store.load(); + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.guestType) { + throw "no guest type specified"; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'name'], + filterOnLoad: true + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: gettext('Snapshot'), + dataIndex: 'name', + hideable: false, + flex: 1 + } + ] + } + }); + + me.callParent(); + + me.loadStore(me.nodename, me.vmid); + } +}); +Ext.define('PVE.form.ContentTypeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveContentTypeSelector'], + + cts: undefined, + + initComponent: function() { + var me = this; + + me.comboItems = []; + + if (me.cts === undefined) { + me.cts = ['images', 'iso', 'vztmpl', 'backup', 'rootdir', 'snippets']; + } + + Ext.Array.each(me.cts, function(ct) { + me.comboItems.push([ct, PVE.Utils.format_content_types(ct)]); + }); + + me.callParent(); + } +}); +Ext.define('PVE.form.HotplugFeatureSelector', { + extend: 'Ext.form.CheckboxGroup', + alias: 'widget.pveHotplugFeatureSelector', + + columns: 1, + vertical: true, + + defaults: { + name: 'hotplug', + submitValue: false + }, + items: [ + { + boxLabel: gettext('Disk'), + inputValue: 'disk', + checked: true + }, + { + boxLabel: gettext('Network'), + inputValue: 'network', + checked: true + }, + { + boxLabel: 'USB', + inputValue: 'usb', + checked: true + }, + { + boxLabel: gettext('Memory'), + inputValue: 'memory' + }, + { + boxLabel: gettext('CPU'), + inputValue: 'cpu' + } + ], + + setValue: function(value) { + var me = this; + var newVal = []; + if (value === '1') { + newVal = ['disk', 'network', 'usb']; + } else if (value !== '0') { + newVal = value.split(','); + } + me.callParent([{ hotplug: newVal }]); + }, + + // override framework function to + // assemble the hotplug value + getSubmitData: function() { + var me = this, + boxes = me.getBoxes(), + data = []; + Ext.Array.forEach(boxes, function(box){ + if (box.getValue()) { + data.push(box.inputValue); + } + }); + + /* because above is hotplug an array */ + /*jslint confusion: true*/ + if (data.length === 0) { + return { 'hotplug':'0' }; + } else { + return { 'hotplug': data.join(',') }; + } + } + +}); +Ext.define('PVE.form.AgentFeatureSelector', { + extend: 'Proxmox.panel.InputPanel', + alias: ['widget.pveAgentFeatureSelector'], + + initComponent: function() { + var me = this; + me.items= [ + { + xtype: 'proxmoxcheckbox', + boxLabel: gettext('Qemu Agent'), + name: 'enabled', + uncheckedValue: 0, + listeners: { + change: function(f, value, old) { + var gtcb = me.down('proxmoxcheckbox[name=fstrim_cloned_disks]'); + if (value) { + gtcb.setDisabled(false); + } else { + gtcb.setDisabled(true); + } + } + } + }, + { + xtype: 'proxmoxcheckbox', + boxLabel: gettext('Run guest-trim after clone disk'), + name: 'fstrim_cloned_disks', + disabled: true + } + ]; + me.callParent(); + }, + + onGetValues: function(values) { + var agentstr = PVE.Parser.printPropertyString(values, 'enabled'); + return { agent: agentstr }; + }, + + setValues: function(values) { + var agent = values.agent || ''; + var res = PVE.Parser.parsePropertyString(agent, 'enabled'); + this.callParent([res]); + } +}); +Ext.define('PVE.form.iScsiProviderSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveiScsiProviderSelector'], + comboItems: [ + ['comstar', 'Comstar'], + [ 'istgt', 'istgt'], + [ 'iet', 'IET'], + [ 'LIO', 'LIO'] + ] +}); +Ext.define('PVE.form.DayOfWeekSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveDayOfWeekSelector'], + comboItems:[], + initComponent: function(){ + var me = this; + me.comboItems = [ + ['mon', Ext.util.Format.htmlDecode(Ext.Date.dayNames[1])], + ['tue', Ext.util.Format.htmlDecode(Ext.Date.dayNames[2])], + ['wed', Ext.util.Format.htmlDecode(Ext.Date.dayNames[3])], + ['thu', Ext.util.Format.htmlDecode(Ext.Date.dayNames[4])], + ['fri', Ext.util.Format.htmlDecode(Ext.Date.dayNames[5])], + ['sat', Ext.util.Format.htmlDecode(Ext.Date.dayNames[6])], + ['sun', Ext.util.Format.htmlDecode(Ext.Date.dayNames[0])] + ]; + this.callParent(); + } +}); +Ext.define('PVE.form.BackupModeSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveBackupModeSelector'], + comboItems: [ + ['snapshot', gettext('Snapshot')], + ['suspend', gettext('Suspend')], + ['stop', gettext('Stop')] + ] +}); +Ext.define('PVE.form.ScsiHwSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveScsiHwSelector'], + comboItems: [ + ['__default__', PVE.Utils.render_scsihw('')], + ['lsi', PVE.Utils.render_scsihw('lsi')], + ['lsi53c810', PVE.Utils.render_scsihw('lsi53c810')], + ['megasas', PVE.Utils.render_scsihw('megasas')], + ['virtio-scsi-pci', PVE.Utils.render_scsihw('virtio-scsi-pci')], + ['virtio-scsi-single', PVE.Utils.render_scsihw('virtio-scsi-single')], + ['pvscsi', PVE.Utils.render_scsihw('pvscsi')] + ] +}); +Ext.define('PVE.form.FirewallPolicySelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveFirewallPolicySelector'], + comboItems: [ + ['ACCEPT', 'ACCEPT'], + ['REJECT', 'REJECT'], + [ 'DROP', 'DROP'] + ] +}); +/* + * This is a global search field + * it loads the /cluster/resources on focus + * and displays the result in a floating grid + * + * it filters and sorts the objects by the algorithm in + * the customFilter function + * + * also it does accept key up/down and enter for input + * and it opens to ctrl+shift+f and ctrl+space + */ +Ext.define('PVE.form.GlobalSearchField', { + extend: 'Ext.form.field.Text', + alias: 'widget.pveGlobalSearchField', + + emptyText: gettext('Search'), + enableKeyEvents: true, + selectOnFocus: true, + padding: '0 5 0 5', + + grid: { + xtype: 'gridpanel', + focusOnToFront: false, + floating: true, + emptyText: Proxmox.Utils.noneText, + width: 600, + height: 400, + scrollable: { + xtype: 'scroller', + y: true, + x:false + }, + store: { + model: 'PVEResources', + proxy:{ + type: 'proxmox', + url: '/api2/extjs/cluster/resources' + } + }, + plugins: { + ptype: 'bufferedrenderer', + trailingBufferZone: 20, + leadingBufferZone: 20 + }, + + hideMe: function() { + var me = this; + if (typeof me.ctxMenu !== 'undefined' && me.ctxMenu.isVisible()) { + return; + } + me.hasFocus = false; + if (!me.textfield.hasFocus) { + me.hide(); + } + }, + + setFocus: function() { + var me = this; + me.hasFocus = true; + }, + + listeners: { + rowclick: function(grid, record) { + var me = this; + me.textfield.selectAndHide(record.id); + }, + itemcontextmenu: function(v, record, item, index, event) { + var me = this; + me.ctxMenu = PVE.Utils.createCmdMenu(v, record, item, index, event); + }, + /* because of lint */ + focusleave: { + fn: 'hideMe' + }, + focusenter: 'setFocus' + }, + + columns: [ + { + text: gettext('Type'), + dataIndex: 'type', + width: 100, + renderer: PVE.Utils.render_resource_type + }, + { + text: gettext('Description'), + flex: 1, + dataIndex: 'text' + }, + { + text: gettext('Node'), + dataIndex: 'node' + }, + { + text: gettext('Pool'), + dataIndex: 'pool' + } + ] + }, + + customFilter: function(item) { + var me = this; + var match = 0; + var fieldArr = []; + var i,j, fields; + + // different types of objects have different fields to search + // for example, a node will never have a pool and vice versa + switch (item.data.type) { + case 'pool': fieldArr = ['type', 'pool', 'text']; break; + case 'node': fieldArr = ['type', 'node', 'text']; break; + case 'storage': fieldArr = ['type', 'pool', 'node', 'storage']; break; + default: fieldArr = ['name', 'type', 'node', 'pool', 'vmid']; + } + if (me.filterVal === '') { + item.data.relevance = 0; + return true; + } + + // all text is case insensitive and each word is + // searched alone + // for every partial match, the row gets + // 1 match point, for every exact match + // it gets 2 points + // + // results gets sorted by points (descending) + fields = me.filterVal.split(/\s+/); + for(i = 0; i < fieldArr.length; i++) { + var v = item.data[fieldArr[i]]; + if (v !== undefined) { + v = v.toString().toLowerCase(); + for(j = 0; j < fields.length; j++) { + if (v.indexOf(fields[j]) !== -1) { + match++; + if(v === fields[j]) { + match++; + } + } + } + } + } + // give the row the 'relevance' value + item.data.relevance = match; + return (match > 0); + }, + + updateFilter: function(field, newValue, oldValue) { + var me = this; + // parse input and filter store, + // show grid + me.grid.store.filterVal = newValue.toLowerCase().trim(); + me.grid.store.clearFilter(true); + me.grid.store.filterBy(me.customFilter); + me.grid.getSelectionModel().select(0); + }, + + selectAndHide: function(id) { + var me = this; + me.tree.selectById(id); + me.grid.hide(); + me.setValue(''); + me.blur(); + }, + + onKey: function(field, e) { + var me = this; + var key = e.getKey(); + + switch(key) { + case Ext.event.Event.ENTER: + // go to first entry if there is one + if (me.grid.store.getCount() > 0) { + me.selectAndHide(me.grid.getSelection()[0].data.id); + } + break; + case Ext.event.Event.UP: + me.grid.getSelectionModel().selectPrevious(); + break; + case Ext.event.Event.DOWN: + me.grid.getSelectionModel().selectNext(); + break; + case Ext.event.Event.ESC: + me.grid.hide(); + me.blur(); + break; + } + }, + + loadValues: function(field) { + var me = this; + var records = []; + + me.hasFocus = true; + me.grid.textfield = me; + me.grid.store.load(); + me.grid.showBy(me, 'tl-bl'); + }, + + hideGrid: function() { + var me = this; + + me.hasFocus = false; + if (!me.grid.hasFocus) { + me.grid.hide(); + } + }, + + listeners: { + change: { + fn: 'updateFilter', + buffer: 250 + }, + specialkey: 'onKey', + focusenter: 'loadValues', + focusleave: { + fn: 'hideGrid', + delay: 100 + } + }, + + toggleFocus: function() { + var me = this; + if (!me.hasFocus) { + me.focus(); + } else { + me.blur(); + } + }, + + initComponent: function() { + var me = this; + + if (!me.tree) { + throw "no tree given"; + } + + me.grid = Ext.create(me.grid); + + me.callParent(); + + /*jslint confusion: true*/ + /*because shift is also a function*/ + // bind ctrl+shift+f and ctrl+space + // to open/close the search + me.keymap = new Ext.KeyMap({ + target: Ext.get(document), + binding: [{ + key:'F', + ctrl: true, + shift: true, + fn: me.toggleFocus, + scope: me + },{ + key:' ', + ctrl: true, + fn: me.toggleFocus, + scope: me + }] + }); + + // always select first item and + // sort by relevance after load + me.mon(me.grid.store, 'load', function() { + me.grid.getSelectionModel().select(0); + me.grid.store.sort({ + property: 'relevance', + direction: 'DESC' + }); + }); + } + +}); +Ext.define('PVE.form.QemuBiosSelector', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveQemuBiosSelector'], + + initComponent: function() { + var me = this; + + me.comboItems = [ + ['__default__', PVE.Utils.render_qemu_bios('')], + ['seabios', PVE.Utils.render_qemu_bios('seabios')], + ['ovmf', PVE.Utils.render_qemu_bios('ovmf')] + ]; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +/* filter is a javascript builtin, but extjs calls it also filter */ +Ext.define('PVE.form.VMSelector', { + extend: 'Ext.grid.Panel', + alias: 'widget.vmselector', + + mixins: { + field: 'Ext.form.field.Field' + }, + + allowBlank: true, + selectAll: false, + isFormField: true, + + plugins: 'gridfilters', + + store: { + model: 'PVEResources', + autoLoad: true, + sorters: 'vmid', + filters: [{ + property: 'type', + value: /lxc|qemu/ + }] + }, + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 80, + filter: { + type: 'number' + } + }, + { + header: gettext('Node'), + dataIndex: 'node' + }, + { + header: gettext('Status'), + dataIndex: 'status', + filter: { + type: 'list' + } + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1, + filter: { + type: 'string' + } + }, + { + header: gettext('Pool'), + dataIndex: 'pool', + filter: { + type: 'list' + } + }, + { + header: gettext('Type'), + dataIndex: 'type', + width: 120, + renderer: function(value) { + if (value === 'qemu') { + return gettext('Virtual Machine'); + } else if (value === 'lxc') { + return gettext('LXC Container'); + } + + return ''; + }, + filter: { + type: 'list', + store: { + data: [ + {id: 'qemu', text: gettext('Virtual Machine')}, + {id: 'lxc', text: gettext('LXC Container')} + ], + // due to EXTJS-18711 + // we have to do a static list via a store + // but to avoid creating an object, + // we have to have a pseudo un function + un: function(){} + } + } + }, + { + header: 'HA ' + gettext('Status'), + dataIndex: 'hastate', + flex: 1, + filter: { + type: 'list' + } + } + ], + + selModel: { + selType: 'checkboxmodel', + mode: 'SIMPLE' + }, + + checkChangeEvents: [ + 'selectionchange', + 'change' + ], + + listeners: { + selectionchange: function() { + // to trigger validity and error checks + this.checkChange(); + } + }, + + getValue: function() { + var me = this; + var sm = me.getSelectionModel(); + var selection = sm.getSelection(); + var values = []; + var store = me.getStore(); + selection.forEach(function(item) { + // only add if not filtered + if (store.findExact('vmid', item.data.vmid) !== -1) { + values.push(item.data.vmid); + } + }); + return values; + }, + + setValue: function(value) { + console.log(value); + var me = this; + var sm = me.getSelectionModel(); + if (!Ext.isArray(value)) { + value = value.split(','); + } + var selection = []; + var store = me.getStore(); + + value.forEach(function(item) { + var rec = store.findRecord('vmid',item, 0, false, true, true); + console.log(store); + + if (rec) { + console.log(rec); + selection.push(rec); + } + }); + + sm.select(selection); + + return me.mixins.field.setValue.call(me, value); + }, + + getErrors: function(value) { + var me = this; + if (me.allowBlank === false && + me.getSelectionModel().getCount() === 0) { + me.addBodyCls(['x-form-trigger-wrap-default','x-form-trigger-wrap-invalid']); + return [gettext('No VM selected')]; + } + + me.removeBodyCls(['x-form-trigger-wrap-default','x-form-trigger-wrap-invalid']); + return []; + }, + + initComponent: function() { + var me = this; + + me.callParent(); + + if (me.nodename) { + me.store.filters.add({ + property: 'node', + exactMatch: true, + value: me.nodename + }); + } + + // only show the relevant guests by default + if (me.action) { + var statusfilter = ''; + switch (me.action) { + case 'startall': + statusfilter = 'stopped'; + break; + case 'stopall': + statusfilter = 'running'; + break; + } + if (statusfilter !== '') { + me.store.filters.add({ + property: 'template', + value: 0 + },{ + id: 'x-gridfilter-status', + operator: 'in', + property: 'status', + value: [statusfilter] + }); + } + } + + var store = me.getStore(); + var sm = me.getSelectionModel(); + + if (me.selectAll) { + me.mon(store,'load', function(){ + me.getSelectionModel().selectAll(false); + }); + } + } +}); + + +Ext.define('PVE.form.VMComboSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.vmComboSelector', + + valueField: 'vmid', + displayField: 'vmid', + + autoSelect: false, + editable: true, + anyMatch: true, + forceSelection: true, + + store: { + model: 'PVEResources', + autoLoad: true, + sorters: 'vmid', + filters: [{ + property: 'type', + value: /lxc|qemu/ + }] + }, + + listConfig: { + width: 600, + plugins: 'gridfilters', + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 80, + filter: { + type: 'number' + } + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1, + filter: { + type: 'string' + } + }, + { + header: gettext('Node'), + dataIndex: 'node' + }, + { + header: gettext('Status'), + dataIndex: 'status', + filter: { + type: 'list' + } + }, + { + header: gettext('Pool'), + dataIndex: 'pool', + hidden: true, + filter: { + type: 'list' + } + }, + { + header: gettext('Type'), + dataIndex: 'type', + width: 120, + renderer: function(value) { + if (value === 'qemu') { + return gettext('Virtual Machine'); + } else if (value === 'lxc') { + return gettext('LXC Container'); + } + + return ''; + }, + filter: { + type: 'list', + store: { + data: [ + {id: 'qemu', text: gettext('Virtual Machine')}, + {id: 'lxc', text: gettext('LXC Container')} + ], + un: function(){} // due to EXTJS-18711 + } + } + }, + { + header: 'HA ' + gettext('Status'), + dataIndex: 'hastate', + hidden: true, + flex: 1, + filter: { + type: 'list' + } + } + ] + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.form.VMCPUFlagSelector', { + extend: 'Ext.grid.Panel', + alias: 'widget.vmcpuflagselector', + + mixins: { + field: 'Ext.form.field.Field' + }, + + disableSelection: true, + columnLines: false, + selectable: false, + hideHeaders: true, + + scrollable: 'y', + height: 200, + + unkownFlags: [], + + store: { + type: 'store', + fields: ['flag', { name: 'state', defaultValue: '=' }, 'desc'], + data: [ + // FIXME: let qemu-server host this and autogenerate or get from API call?? + { flag: 'md-clear', desc: 'Required to let the guest OS know if MDS is mitigated correctly' }, + { flag: 'pcid', desc: 'Meltdown fix cost reduction on Westmere, Sandy-, and IvyBridge Intel CPUs' }, + { flag: 'spec-ctrl', desc: 'Allows improved Spectre mitigation with Intel CPUs' }, + { flag: 'ssbd', desc: 'Protection for "Speculative Store Bypass" for Intel models' }, + { flag: 'ibpb', desc: 'Allows improved Spectre mitigation with AMD CPUs' }, + { flag: 'virt-ssbd', desc: 'Basis for "Speculative Store Bypass" protection for AMD models' }, + { flag: 'amd-ssbd', desc: 'Improves Spectre mitigation performance with AMD CPUs, best used with "virt-ssbd"' }, + { flag: 'amd-no-ssb', desc: 'Notifies guest OS that host is not vulnerable for Spectre on AMD CPUs' }, + { flag: 'pdpe1gb', desc: 'Allow guest OS to use 1GB size pages, if host HW supports it' } + ], + listeners: { + update: function() { + this.commitChanges(); + } + } + }, + + getValue: function() { + var me = this; + var store = me.getStore(); + var flags = ''; + + // ExtJS does not has a nice getAllRecords interface for stores :/ + store.queryBy(Ext.returnTrue).each(function(rec) { + var s = rec.get('state'); + if (s && s !== '=') { + var f = rec.get('flag'); + if (flags === '') { + flags = s + f; + } else { + flags += ';' + s + f; + } + } + }); + + flags += me.unkownFlags.join(';'); + + return flags; + }, + + setValue: function(value) { + var me = this; + var store = me.getStore(); + + me.value = value || ''; + + me.unkownFlags = []; + + me.getStore().queryBy(Ext.returnTrue).each(function(rec) { + rec.set('state', '='); + }); + + var flags = value ? value.split(';') : []; + flags.forEach(function(flag) { + var sign = flag.substr(0, 1); + flag = flag.substr(1); + + var rec = store.findRecord('flag', flag); + if (rec !== null) { + rec.set('state', sign); + } else { + me.unkownFlags.push(flag); + } + }); + store.reload(); + + var res = me.mixins.field.setValue.call(me, value); + + return res; + }, + columns: [ + { + dataIndex: 'state', + renderer: function(v) { + switch(v) { + case '=': return 'Default'; + case '-': return 'Off'; + case '+': return 'On'; + default: return 'Unknown'; + } + }, + width: 65 + }, + { + xtype: 'widgetcolumn', + dataIndex: 'state', + width: 95, + onWidgetAttach: function (column, widget, record) { + var val = record.get('state') || '='; + widget.down('[inputValue=' + val + ']').setValue(true); + // TODO: disable if selected CPU model and flag are incompatible + }, + widget: { + xtype: 'radiogroup', + hideLabel: true, + layout: 'hbox', + validateOnChange: false, + value: '=', + listeners: { + change: function(f, value) { + var v = Object.values(value)[0]; + f.getWidgetRecord().set('state', v); + + var view = this.up('grid'); + view.dirty = view.getValue() !== view.originalValue; + view.checkDirty(); + //view.checkChange(); + } + }, + items: [ + { + boxLabel: '-', + boxLabelAlign: 'before', + inputValue: '-' + }, + { + checked: true, + inputValue: '=' + }, + { + boxLabel: '+', + inputValue: '+' + } + ] + } + }, + { + dataIndex: 'flag', + width: 100 + }, + { + dataIndex: 'desc', + cellWrap: true, + flex: 1 + } + ], + + initComponent: function() { + var me = this; + + // static class store, thus gets not recreated, so ensure defaults are set! + me.getStore().data.forEach(function(v) { + v.state = '='; + }); + + me.value = me.originalValue = ''; + + me.callParent(arguments); + } +}); +Ext.define('PVE.form.USBSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveUSBSelector'], + allowBlank: false, + autoSelect: false, + displayField: 'usbid', + valueField: 'usbid', + editable: true, + + getUSBValue: function() { + var me = this; + var rec = me.store.findRecord('usbid', me.value); + var val = 'host='+ me.value; + if (rec && rec.data.speed === "5000") { + val = 'host=' + me.value + ",usb3=1"; + } + return val; + }, + + validator: function(value) { + var me = this; + if (me.type === 'device') { + return (/^[a-f0-9]{4}\:[a-f0-9]{4}$/i).test(value); + } else if (me.type === 'port') { + return (/^[0-9]+\-[0-9]+(\.[0-9]+)*$/).test(value); + } + return false; + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + + if (!nodename) { + throw "no nodename specified"; + } + + if (me.type !== 'device' && me.type !== 'port') { + throw "no valid type specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-usb-' + me.type, + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/scan/usb" + }, + filters: [ + function (item) { + return !!item.data.usbpath && !!item.data.prodid && item.data['class'] != 9; + } + ] + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: (me.type === 'device')?gettext('Device'):gettext('Port'), + sortable: true, + dataIndex: 'usbid', + width: 80 + }, + { + header: gettext('Manufacturer'), + sortable: true, + dataIndex: 'manufacturer', + width: 100 + }, + { + header: gettext('Product'), + sortable: true, + dataIndex: 'product', + flex: 1 + }, + { + header: gettext('Speed'), + width: 70, + sortable: true, + dataIndex: 'speed', + renderer: function(value) { + if (value === "5000") { + return "USB 3.0"; + } else if (value === "480") { + return "USB 2.0"; + } else { + return "USB 1.x"; + } + } + } + ] + } + }); + + me.callParent(); + + store.load(); + } + +}, function() { + + Ext.define('pve-usb-device', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'usbid', + convert: function(val, data) { + if (val) { + return val; + } + return data.get('vendid') + ':' + data.get('prodid'); + } + }, + 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath', + { name: 'port' , type: 'number' }, + { name: 'level' , type: 'number' }, + { name: 'class' , type: 'number' }, + { name: 'devnum' , type: 'number' }, + { name: 'busnum' , type: 'number' } + ] + }); + + Ext.define('pve-usb-port', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'usbid', + convert: function(val,data) { + if (val) { + return val; + } + return data.get('busnum') + '-' + data.get('usbpath'); + } + }, + 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath', + { name: 'port' , type: 'number' }, + { name: 'level' , type: 'number' }, + { name: 'class' , type: 'number' }, + { name: 'devnum' , type: 'number' }, + { name: 'busnum' , type: 'number' } + ] + }); +}); +Ext.define('PVE.form.CalendarEvent', { + extend: 'Ext.form.field.ComboBox', + xtype: 'pveCalendarEvent', + + editable: true, + + valueField: 'value', + displayField: 'text', + queryMode: 'local', + + store: { + field: [ 'value', 'text'], + data: [ + { value: '*/30', text: Ext.String.format(gettext("Every {0} minutes"), 30) }, + { value: '*/2:00', text: gettext("Every two hours")}, + { value: '2,22:30', text: gettext("Every day") + " 02:30, 22:30"}, + { value: 'mon..fri', text: gettext("Monday to Friday") + " 00:00"}, + { value: 'mon..fri */1:00', text: gettext("Monday to Friday") + ': ' + gettext("hourly")}, + { value: 'sun 01:00', text: gettext("Sunday") + " 01:00"} + ] + }, + + tpl: [ + '
    ', + '
  • {text}
  • ', + '
' + ], + + displayTpl: [ + '', + '{value}', + '' + ] + +}); +Ext.define('PVE.form.CephPoolSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCephPoolSelector', + + allowBlank: false, + valueField: 'pool_name', + displayField: 'pool_name', + editable: false, + queryMode: 'local', + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['name'], + sorters: 'name', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/ceph/pools' + } + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + store.load({ + callback: function(rec, op, success){ + if (success && rec.length > 0) { + me.select(rec[0]); + } + } + }); + } + +}); +Ext.define('PVE.form.PermPathSelector', { + extend: 'Ext.form.field.ComboBox', + xtype: 'pvePermPathSelector', + + valueField: 'value', + displayField: 'value', + typeAhead: true, + queryMode: 'local', + store: { + type: 'pvePermPath' + } +}); +/* This class defines the "Tasks" tab of the bottom status panel + * Tasks are jobs with a start, end and log output + */ + +Ext.define('PVE.dc.Tasks', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveClusterTasks'], + + initComponent : function() { + var me = this; + + var taskstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-cluster-tasks', + model: 'proxmox-tasks', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/tasks' + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: taskstore, + sortAfterUpdate: true, + appendAtStart: true, + sorters: [ + { + property : 'pid', + direction: 'DESC' + }, + { + property : 'starttime', + direction: 'DESC' + } + ] + + }); + + var run_task_viewer = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: rec.data.upid + }); + win.show(); + }; + + Ext.apply(me, { + store: store, + stateful: false, + + viewConfig: { + trackOver: false, + stripeRows: true, // does not work with getRowClass() + + getRowClass: function(record, index) { + var status = record.get('status'); + + if (status && status != 'OK') { + return "proxmox-invalid-row"; + } + } + }, + sortableColumns: false, + columns: [ + { + header: gettext("Start Time"), + dataIndex: 'starttime', + width: 150, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("End Time"), + dataIndex: 'endtime', + width: 150, + renderer: function(value, metaData, record) { + if (record.data.pid) { + if (record.data.type == "vncproxy" || + record.data.type == "vncshell" || + record.data.type == "spiceproxy") { + metaData.tdCls = "x-grid-row-console"; + } else { + metaData.tdCls = "x-grid-row-loading"; + } + return ""; + } + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("Node"), + dataIndex: 'node', + width: 100 + }, + { + header: gettext("User name"), + dataIndex: 'user', + width: 150 + }, + { + header: gettext("Description"), + dataIndex: 'upid', + flex: 1, + renderer: Proxmox.Utils.render_upid + }, + { + header: gettext("Status"), + dataIndex: 'status', + width: 200, + renderer: function(value, metaData, record) { + if (record.data.pid) { + if (record.data.type != "vncproxy") { + metaData.tdCls = "x-grid-row-loading"; + } + return ""; + } + if (value == 'OK') { + return 'OK'; + } + // metaData.attr = 'style="color:red;"'; + return Proxmox.Utils.errorText + ': ' + value; + } + } + ], + listeners: { + itemdblclick: run_task_viewer, + show: taskstore.startUpdate, + destroy: taskstore.stopUpdate + } + }); + + me.callParent(); + } +}); +/* This class defines the "Cluster log" tab of the bottom status panel + * A log entry is a timestamp associated with an action on a cluster + */ + +Ext.define('PVE.dc.Log', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveClusterLog'], + + initComponent : function() { + var me = this; + + var logstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-cluster-log', + model: 'proxmox-cluster-log', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/log' + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: logstore, + appendAtStart: true + }); + + Ext.apply(me, { + store: store, + stateful: false, + + viewConfig: { + trackOver: false, + stripeRows: true, + + getRowClass: function(record, index) { + var pri = record.get('pri'); + + if (pri && pri <= 3) { + return "proxmox-invalid-row"; + } + } + }, + sortableColumns: false, + columns: [ + { + header: gettext("Time"), + dataIndex: 'time', + width: 150, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + } + }, + { + header: gettext("Node"), + dataIndex: 'node', + width: 150 + }, + { + header: gettext("Service"), + dataIndex: 'tag', + width: 100 + }, + { + header: "PID", + dataIndex: 'pid', + width: 100 + }, + { + header: gettext("User name"), + dataIndex: 'user', + width: 150 + }, + { + header: gettext("Severity"), + dataIndex: 'pri', + renderer: PVE.Utils.render_serverity, + width: 100 + }, + { + header: gettext("Message"), + dataIndex: 'msg', + flex: 1 + } + ], + listeners: { + activate: logstore.startUpdate, + deactivate: logstore.stopUpdate, + destroy: logstore.stopUpdate + } + }); + + me.callParent(); + } +}); +/* + * This class describes the bottom panel + */ +Ext.define('PVE.panel.StatusPanel', { + extend: 'Ext.tab.Panel', + alias: 'widget.pveStatusPanel', + + + //title: "Logs", + //tabPosition: 'bottom', + + initComponent: function() { + var me = this; + + var stateid = 'ltab'; + var sp = Ext.state.Manager.getProvider(); + + var state = sp.get(stateid); + if (state && state.value) { + me.activeTab = state.value; + } + + Ext.apply(me, { + listeners: { + tabchange: function() { + var atab = me.getActiveTab().itemId; + var state = { value: atab }; + sp.set(stateid, state); + } + }, + items: [ + { + itemId: 'tasks', + title: gettext('Tasks'), + xtype: 'pveClusterTasks' + }, + { + itemId: 'clog', + title: gettext('Cluster log'), + xtype: 'pveClusterLog' + } + ] + }); + + me.callParent(); + + me.items.get(0).fireEvent('show', me.items.get(0)); + + var statechange = function(sp, key, state) { + if (key === stateid) { + var atab = me.getActiveTab().itemId; + var ntab = state.value; + if (state && ntab && (atab != ntab)) { + me.setActiveTab(ntab); + } + } + }; + + sp.on('statechange', statechange); + me.on('destroy', function() { + sp.un('statechange', statechange); + }); + + } +}); +Ext.define('PVE.panel.StatusView', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveStatusView', + + layout: { + type: 'column' + }, + + title: gettext('Status'), + + getRecordValue: function(key, store) { + if (!key) { + throw "no key given"; + } + var me = this; + + if (store === undefined) { + store = me.getStore(); + } + + var rec = store.getById(key); + if (rec) { + return rec.data.value; + } + + return ''; + }, + + fieldRenderer: function(val,max) { + if (max === undefined) { + return val; + } + + if (!Ext.isNumeric(max) || max === 1) { + return PVE.Utils.render_usage(val); + } + return PVE.Utils.render_size_usage(val,max); + }, + + fieldCalculator: function(used, max) { + if (!Ext.isNumeric(max) && Ext.isNumeric(used)) { + return used; + } else if(!Ext.isNumeric(used)) { + /* we come here if the field is from a node + * where the records are not mem and maxmem + * but mem.used and mem.total + */ + if (used.used !== undefined && + used.total !== undefined) { + return used.used/used.total; + } + } + + return used/max; + }, + + updateField: function(field) { + var me = this; + var text = ''; + var renderer = me.fieldRenderer; + if (Ext.isFunction(field.renderer)) { + renderer = field.renderer; + } + if (field.multiField === true) { + field.updateValue(renderer.call(field, me.getStore().getRecord())); + } else if (field.textField !== undefined) { + field.updateValue(renderer.call(field, me.getRecordValue(field.textField))); + } else if(field.valueField !== undefined) { + var used = me.getRecordValue(field.valueField); + /*jslint confusion: true*/ + /* string and int */ + var max = field.maxField !== undefined ? me.getRecordValue(field.maxField) : 1; + + var calculate = me.fieldCalculator; + + if (Ext.isFunction(field.calculate)) { + calculate = field.calculate; + } + field.updateValue(renderer.call(field, used,max), calculate(used,max)); + } + }, + + getStore: function() { + var me = this; + if (!me.rstore) { + throw "there is no rstore"; + } + + return me.rstore; + }, + + updateTitle: function() { + var me = this; + me.setTitle(me.getRecordValue('name')); + }, + + updateValues: function(store, records, success) { + if (!success) { + return; // do not update if store load was not successful + } + var me = this; + var itemsToUpdate = me.query('pveInfoWidget'); + + itemsToUpdate.forEach(me.updateField, me); + + me.updateTitle(store); + }, + + initComponent: function() { + var me = this; + + if (!me.rstore) { + throw "no rstore given"; + } + + if (!me.title) { + throw "no title given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + me.callParent(); + + me.mon(me.rstore, 'load', 'updateValues'); + } + +}); +Ext.define('PVE.panel.GuestStatusView', { + extend: 'PVE.panel.StatusView', + alias: 'widget.pveGuestStatusView', + mixins: ['Proxmox.Mixin.CBind'], + + height: 300, + + cbindData: function (initialConfig) { + var me = this; + return { + isQemu: me.pveSelNode.data.type === 'qemu', + isLxc: me.pveSelNode.data.type === 'lxc' + }; + }, + + layout: { + type: 'vbox', + align: 'stretch' + }, + + defaults: { + xtype: 'pveInfoWidget', + padding: '2 25' + }, + items: [ + { + xtype: 'box', + height: 20 + }, + { + itemId: 'status', + title: gettext('Status'), + iconCls: 'fa fa-info fa-fw', + printBar: false, + multiField: true, + renderer: function(record) { + var me = this; + var text = record.data.status; + var qmpstatus = record.data.qmpstatus; + if (qmpstatus && qmpstatus !== record.data.status) { + text += ' (' + qmpstatus + ')'; + } + return text; + } + }, + { + itemId: 'hamanaged', + iconCls: 'fa fa-heartbeat fa-fw', + title: gettext('HA State'), + printBar: false, + textField: 'ha', + renderer: PVE.Utils.format_ha + }, + { + xtype: 'pveInfoWidget', + itemId: 'node', + iconCls: 'fa fa-building fa-fw', + title: gettext('Node'), + cbind: { + text: '{pveSelNode.data.node}' + }, + printBar: false + }, + { + xtype: 'box', + height: 15 + }, + { + itemId: 'cpu', + iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon', + title: gettext('CPU usage'), + valueField: 'cpu', + maxField: 'cpus', + renderer: PVE.Utils.render_cpu_usage, + // in this specific api call + // we already have the correct value for the usage + calculate: Ext.identityFn + }, + { + itemId: 'memory', + iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon', + title: gettext('Memory usage'), + valueField: 'mem', + maxField: 'maxmem' + }, + { + itemId: 'swap', + xtype: 'pveInfoWidget', + iconCls: 'fa fa-refresh fa-fw', + title: gettext('SWAP usage'), + valueField: 'swap', + maxField: 'maxswap', + cbind: { + hidden: '{isQemu}', + disabled: '{isQemu}' + } + }, + { + itemId: 'rootfs', + iconCls: 'fa fa-hdd-o fa-fw', + title: gettext('Bootdisk size'), + valueField: 'disk', + maxField: 'maxdisk', + printBar: false, + renderer: function(used, max) { + var me = this; + me.setPrintBar(used > 0); + if (used === 0) { + return PVE.Utils.render_size(max); + } else { + return PVE.Utils.render_size_usage(used,max); + } + } + }, + { + xtype: 'box', + height: 15 + }, + { + itemId: 'ips', + xtype: 'pveAgentIPView', + cbind: { + rstore: '{rstore}', + pveSelNode: '{pveSelNode}', + hidden: '{isLxc}', + disabled: '{isLxc}' + } + } + ], + + updateTitle: function() { + var me = this; + var uptime = me.getRecordValue('uptime'); + + var text = ""; + if (Number(uptime) > 0) { + text = " (" + gettext('Uptime') + ': ' + Proxmox.Utils.format_duration_long(uptime) + + ')'; + } + + me.setTitle(me.getRecordValue('name') + text); + } +}); +/* + * This is a running chart widget + * you add time datapoints to it, + * and we only show the last x of it + * used for ceph performance charts + */ +Ext.define('PVE.widget.RunningChart', { + extend: 'Ext.container.Container', + alias: 'widget.pveRunningChart', + + layout: { + type: 'hbox', + align: 'center' + }, + items: [ + { + width: 80, + xtype: 'box', + itemId: 'title', + data: { + title: '' + }, + tpl: '

{title}:

' + }, + { + flex: 1, + xtype: 'cartesian', + height: '100%', + itemId: 'chart', + border: false, + axes: [ + { + type: 'numeric', + position: 'left', + hidden: true, + minimum: 0 + }, + { + type: 'numeric', + position: 'bottom', + hidden: true + } + ], + + store: { + data: {} + }, + + sprites: [{ + id: 'valueSprite', + type: 'text', + text: '0 B/s', + textAlign: 'end', + textBaseline: 'middle', + fontSize: 14 + }], + + series: [{ + type: 'line', + xField: 'time', + yField: 'val', + fill: 'true', + colors: ['#cfcfcf'], + tooltip: { + trackMouse: true, + renderer: function( tooltip, record, ctx) { + var me = this.getChart(); + var date = new Date(record.data.time); + var value = me.up().renderer(record.data.val); + tooltip.setHtml( + me.up().title + ': ' + value + '
' + + Ext.Date.format(date, 'H:i:s') + ); + } + }, + style: { + lineWidth: 1.5, + opacity: 0.60 + }, + marker: { + opacity: 0, + scaling: 0.01, + fx: { + duration: 200, + easing: 'easeOut' + } + }, + highlightCfg: { + opacity: 1, + scaling: 1.5 + } + }] + } + ], + + // the renderer for the tooltip and last value, + // default just the value + renderer: Ext.identityFn, + + // show the last x seconds + // default is 5 minutes + timeFrame: 5*60, + + addDataPoint: function(value, time) { + var me = this.chart; + var panel = me.up(); + var now = new Date(); + var begin = new Date(now.getTime() - (1000*panel.timeFrame)); + + me.store.add({ + time: time || now.getTime(), + val: value || 0 + }); + + // delete all old records when we have 20 times more datapoints + // than seconds in our timeframe (so even a subsecond graph does + // not trigger this often) + // + // records in the store do not take much space, but like this, + // we prevent a memory leak when someone has the site open for a long time + // with minimal graphical glitches + if (me.store.count() > panel.timeFrame * 20) { + var oldData = me.store.getData().createFiltered(function(item) { + return item.data.time < begin.getTime(); + }); + + me.store.remove(oldData.getRange()); + } + + me.timeaxis.setMinimum(begin.getTime()); + me.timeaxis.setMaximum(now.getTime()); + me.valuesprite.setText(panel.renderer(value || 0).toString()); + me.valuesprite.setAttributes({ + x: me.getWidth() - 15, + y: me.getHeight()/2 + }, true); + me.redraw(); + }, + + setTitle: function(title) { + this.title = title; + var me = this.getComponent('title'); + me.update({title: title}); + }, + + initComponent: function(){ + var me = this; + me.callParent(); + + if (me.title) { + me.getComponent('title').update({title: me.title}); + } + me.chart = me.getComponent('chart'); + me.chart.timeaxis = me.chart.getAxes()[1]; + me.chart.valuesprite = me.chart.getSurface('chart').get('valueSprite'); + if (me.color) { + me.chart.series[0].setStyle({ + fill: me.color, + stroke: me.color + }); + } + } +}); +Ext.define('PVE.widget.Info',{ + extend: 'Ext.container.Container', + alias: 'widget.pveInfoWidget', + + layout: { + type: 'vbox', + align: 'stretch' + }, + + value: 0, + maximum: 1, + printBar: true, + items: [ + { + xtype: 'component', + itemId: 'label', + data: { + title: '', + usage: '', + iconCls: undefined + }, + tpl: [ + '
', + '', + ' ', + '', + '{title}
 
{usage}
' + ] + }, + { + height: 2, + border: 0 + }, + { + xtype: 'progressbar', + itemId: 'progress', + height: 5, + value: 0, + animate: true + } + ], + + warningThreshold: 0.6, + criticalThreshold: 0.9, + + setPrintBar: function(enable) { + var me = this; + me.printBar = enable; + me.getComponent('progress').setVisible(enable); + }, + + setIconCls: function(iconCls) { + var me = this; + me.getComponent('label').data.iconCls = iconCls; + }, + + updateValue: function(text, usage) { + var me = this; + var label = me.getComponent('label'); + label.update(Ext.apply(label.data, {title: me.title, usage:text})); + + if (usage !== undefined && + me.printBar && + Ext.isNumeric(usage) && + usage >= 0) { + var progressBar = me.getComponent('progress'); + progressBar.updateProgress(usage, ''); + if (usage > me.criticalThreshold) { + progressBar.removeCls('warning'); + progressBar.addCls('critical'); + } else if (usage > me.warningThreshold) { + progressBar.removeCls('critical'); + progressBar.addCls('warning'); + } else { + progressBar.removeCls('warning'); + progressBar.removeCls('critical'); + } + } + }, + + initComponent: function() { + var me = this; + + if (!me.title) { + throw "no title defined"; + } + + me.callParent(); + + me.getComponent('progress').setVisible(me.printBar); + + me.updateValue(me.text, me.value); + me.setIconCls(me.iconCls); + } + +}); +Ext.define('PVE.panel.TemplateStatusView',{ + extend: 'PVE.panel.StatusView', + alias: 'widget.pveTemplateStatusView', + + layout: { + type: 'vbox', + align: 'stretch' + }, + + defaults: { + xtype: 'pveInfoWidget', + printBar: false, + padding: '2 25' + }, + items: [ + { + xtype: 'box', + height: 20 + }, + { + itemId: 'hamanaged', + iconCls: 'fa fa-heartbeat fa-fw', + title: gettext('HA State'), + printBar: false, + textField: 'ha', + renderer: PVE.Utils.format_ha + }, + { + itemId: 'node', + iconCls: 'fa fa-fw fa-building', + title: gettext('Node') + }, + { + xtype: 'box', + height: 20 + }, + { + itemId: 'cpus', + iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon', + title: gettext('Processors'), + textField: 'cpus' + }, + { + itemId: 'memory', + iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon', + title: gettext('Memory'), + textField: 'maxmem', + renderer: PVE.Utils.render_size + }, + { + itemId: 'swap', + iconCls: 'fa fa-refresh fa-fw', + title: gettext('Swap'), + textField: 'maxswap', + renderer: PVE.Utils.render_size + }, + { + itemId: 'disk', + iconCls: 'fa fa-hdd-o fa-fw', + title: gettext('Bootdisk size'), + textField: 'maxdisk', + renderer: PVE.Utils.render_size + }, + { + xtype: 'box', + height: 20 + } + ], + + initComponent: function() { + var me = this; + + var name = me.pveSelNode.data.name; + if (!name) { + throw "no name specified"; + } + + me.title = name; + + me.callParent(); + if (me.pveSelNode.data.type !== 'lxc') { + me.remove(me.getComponent('swap')); + } + me.getComponent('node').updateValue(me.pveSelNode.data.node); + } +}); +Ext.define('PVE.widget.HealthWidget', { + extend: 'Ext.Component', + alias: 'widget.pveHealthWidget', + + data: { + iconCls: PVE.Utils.get_health_icon(undefined, true), + text: '', + title: '' + }, + + style: { + 'text-align':'center' + }, + + tpl: [ + '

{title}

', + '', + '

', + '{text}' + ], + + updateHealth: function(data) { + var me = this; + me.update(Ext.apply(me.data, data)); + }, + + initComponent: function(){ + var me = this; + + if (me.title) { + me.config.data.title = me.title; + } + + me.callParent(); + } + +}); +/*global u2f*/ +Ext.define('PVE.window.LoginWindow', { + extend: 'Ext.window.Window', + + controller: { + + xclass: 'Ext.app.ViewController', + + onLogon: function() { + var me = this; + + var form = this.lookupReference('loginForm'); + var unField = this.lookupReference('usernameField'); + var saveunField = this.lookupReference('saveunField'); + var view = this.getView(); + + if (!form.isValid()) { + return; + } + + view.el.mask(gettext('Please wait...'), 'x-mask-loading'); + + // set or clear username + var sp = Ext.state.Manager.getProvider(); + if (saveunField.getValue() === true) { + sp.set(unField.getStateId(), unField.getValue()); + } else { + sp.clear(unField.getStateId()); + } + sp.set(saveunField.getStateId(), saveunField.getValue()); + + form.submit({ + failure: function(f, resp){ + me.failure(resp); + }, + success: function(f, resp){ + view.el.unmask(); + + var data = resp.result.data; + if (Ext.isDefined(data.NeedTFA)) { + // Store first factor login information first: + data.LoggedOut = true; + Proxmox.Utils.setAuthData(data); + + if (Ext.isDefined(data.U2FChallenge)) { + me.perform_u2f(data); + } else { + me.perform_otp(); + } + } else { + me.success(data); + } + } + }); + + }, + failure: function(resp) { + var me = this; + var view = me.getView(); + view.el.unmask(); + var handler = function() { + var uf = me.lookupReference('usernameField'); + uf.focus(true, true); + }; + + Ext.MessageBox.alert(gettext('Error'), + gettext("Login failed. Please try again"), + handler); + }, + success: function(data) { + var me = this; + var view = me.getView(); + var handler = view.handler || Ext.emptyFn; + handler.call(me, data); + view.close(); + }, + + perform_otp: function() { + var me = this; + var win = Ext.create('PVE.window.TFALoginWindow', { + onLogin: function(value) { + me.finish_tfa(value); + }, + onCancel: function() { + Proxmox.LoggedOut = false; + Proxmox.Utils.authClear(); + me.getView().show(); + } + }); + win.show(); + }, + + perform_u2f: function(data) { + var me = this; + // Show the message: + var msg = Ext.Msg.show({ + title: 'U2F: '+gettext('Verification'), + message: gettext('Please press the button on your U2F Device'), + buttons: [] + }); + var chlg = data.U2FChallenge; + var key = { + version: chlg.version, + keyHandle: chlg.keyHandle + }; + u2f.sign(chlg.appId, chlg.challenge, [key], function(res) { + msg.close(); + if (res.errorCode) { + Proxmox.Utils.authClear(); + Ext.Msg.alert(gettext('Error'), PVE.Utils.render_u2f_error(res.errorCode)); + return; + } + delete res.errorCode; + me.finish_tfa(JSON.stringify(res)); + }); + }, + finish_tfa: function(res) { + var me = this; + var view = me.getView(); + view.el.mask(gettext('Please wait...'), 'x-mask-loading'); + var params = { response: res }; + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'POST', + timeout: 5000, // it'll delay both success & failure + success: function(resp, opts) { + view.el.unmask(); + // Fill in what we copy over from the 1st factor: + var data = resp.result.data; + data.CSRFPreventionToken = Proxmox.CSRFPreventionToken; + data.username = Proxmox.UserName; + // Finish logging in: + me.success(data); + }, + failure: function(resp, opts) { + Proxmox.Utils.authClear(); + me.failure(resp); + } + }); + }, + + control: { + 'field[name=username]': { + specialkey: function(f, e) { + if (e.getKey() === e.ENTER) { + var pf = this.lookupReference('passwordField'); + if (!pf.getValue()) { + pf.focus(false); + } + } + } + }, + 'field[name=lang]': { + change: function(f, value) { + var dt = Ext.Date.add(new Date(), Ext.Date.YEAR, 10); + Ext.util.Cookies.set('PVELangCookie', value, dt); + this.getView().mask(gettext('Please wait...'), 'x-mask-loading'); + window.location.reload(); + } + }, + 'button[reference=loginButton]': { + click: 'onLogon' + }, + '#': { + show: function() { + var sp = Ext.state.Manager.getProvider(); + var checkboxField = this.lookupReference('saveunField'); + var unField = this.lookupReference('usernameField'); + + var checked = sp.get(checkboxField.getStateId()); + checkboxField.setValue(checked); + + if(checked === true) { + var username = sp.get(unField.getStateId()); + unField.setValue(username); + var pwField = this.lookupReference('passwordField'); + pwField.focus(); + } + } + } + } + }, + + width: 400, + + modal: true, + + border: false, + + draggable: true, + + closable: false, + + resizable: false, + + layout: 'auto', + + title: gettext('Proxmox VE Login'), + + defaultFocus: 'usernameField', + + defaultButton: 'loginButton', + + items: [{ + xtype: 'form', + layout: 'form', + url: '/api2/extjs/access/ticket', + reference: 'loginForm', + + fieldDefaults: { + labelAlign: 'right', + allowBlank: false + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('User name'), + name: 'username', + itemId: 'usernameField', + reference: 'usernameField', + stateId: 'login-username' + }, + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Password'), + name: 'password', + reference: 'passwordField' + }, + { + xtype: 'pveRealmComboBox', + name: 'realm' + }, + { + xtype: 'proxmoxLanguageSelector', + fieldLabel: gettext('Language'), + value: Ext.util.Cookies.get('PVELangCookie') || Proxmox.defaultLang || 'en', + name: 'lang', + reference: 'langField', + submitValue: false + } + ], + buttons: [ + { + xtype: 'checkbox', + fieldLabel: gettext('Save User name'), + name: 'saveusername', + reference: 'saveunField', + stateId: 'login-saveusername', + labelWidth: 'auto', + labelAlign: 'right', + submitValue: false + }, + { + text: gettext('Login'), + reference: 'loginButton' + } + ] + }] + }); +Ext.define('PVE.window.TFALoginWindow', { + extend: 'Ext.window.Window', + + modal: true, + resizable: false, + title: 'Two-Factor Authentication', + layout: 'form', + defaultButton: 'loginButton', + defaultFocus: 'otpField', + + controller: { + xclass: 'Ext.app.ViewController', + login: function() { + var me = this; + var view = me.getView(); + view.onLogin(me.lookup('otpField').getValue()); + view.close(); + }, + cancel: function() { + var me = this; + var view = me.getView(); + view.onCancel(); + view.close(); + } + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Please enter your OTP verification code:'), + name: 'otp', + itemId: 'otpField', + reference: 'otpField', + allowBlank: false + } + ], + + buttons: [ + { + text: gettext('Login'), + reference: 'loginButton', + handler: 'login' + }, + { + text: gettext('Cancel'), + handler: 'cancel' + } + ] +}); +Ext.define('PVE.window.Wizard', { + extend: 'Ext.window.Window', + + activeTitle: '', // used for automated testing + + width: 700, + height: 510, + + modal: true, + border: false, + + draggable: true, + closable: true, + resizable: false, + + layout: 'border', + + getValues: function(dirtyOnly) { + var me = this; + + var values = {}; + + var form = me.down('form').getForm(); + + form.getFields().each(function(field) { + if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) { + Proxmox.Utils.assemble_field_data(values, field.getSubmitData()); + } + }); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly)); + }); + + return values; + }, + + initComponent: function() { + var me = this; + + var tabs = me.items || []; + delete me.items; + + /* + * Items may have the following functions: + * validator(): per tab custom validation + * onSubmit(): submit handler + * onGetValues(): overwrite getValues results + */ + + Ext.Array.each(tabs, function(tab) { + tab.disabled = true; + }); + tabs[0].disabled = false; + + var maxidx = 0; + var curidx = 0; + + var check_card = function(card) { + var valid = true; + var fields = card.query('field, fieldcontainer'); + if (card.isXType('fieldcontainer')) { + fields.unshift(card); + } + Ext.Array.each(fields, function(field) { + // Note: not all fielcontainer have isValid() + if (Ext.isFunction(field.isValid) && !field.isValid()) { + valid = false; + } + }); + + if (Ext.isFunction(card.validator)) { + return card.validator(); + } + + return valid; + }; + + var disable_at = function(card) { + var tp = me.down('#wizcontent'); + var idx = tp.items.indexOf(card); + for(;idx < tp.items.getCount();idx++) { + var nc = tp.items.getAt(idx); + if (nc) { + nc.disable(); + } + } + }; + + var tabchange = function(tp, newcard, oldcard) { + if (newcard.onSubmit) { + me.down('#next').setVisible(false); + me.down('#submit').setVisible(true); + } else { + me.down('#next').setVisible(true); + me.down('#submit').setVisible(false); + } + var valid = check_card(newcard); + me.down('#next').setDisabled(!valid); + me.down('#submit').setDisabled(!valid); + me.down('#back').setDisabled(tp.items.indexOf(newcard) == 0); + + var idx = tp.items.indexOf(newcard); + if (idx > maxidx) { + maxidx = idx; + } + curidx = idx; + + var next = idx + 1; + var ntab = tp.items.getAt(next); + if (valid && ntab && !newcard.onSubmit) { + ntab.enable(); + } + }; + + if (me.subject && !me.title) { + me.title = Proxmox.Utils.dialog_title(me.subject, true, false); + } + + var sp = Ext.state.Manager.getProvider(); + var advchecked = sp.get('proxmox-advanced-cb'); + + Ext.apply(me, { + items: [ + { + xtype: 'form', + region: 'center', + layout: 'fit', + border: false, + margins: '5 5 0 5', + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [{ + itemId: 'wizcontent', + xtype: 'tabpanel', + activeItem: 0, + bodyPadding: 10, + listeners: { + afterrender: function(tp) { + var atab = this.getActiveTab(); + tabchange(tp, atab); + }, + tabchange: function(tp, newcard, oldcard) { + tabchange(tp, newcard, oldcard); + } + }, + items: tabs + }] + } + ], + fbar: [ + { + xtype: 'proxmoxHelpButton', + itemId: 'help' + }, + '->', + { + xtype: 'proxmoxcheckbox', + boxLabelAlign: 'before', + boxLabel: gettext('Advanced'), + value: advchecked, + listeners: { + change: function(cb, val) { + var tp = me.down('#wizcontent'); + tp.query('inputpanel').forEach(function(ip) { + ip.setAdvancedVisible(val); + }); + + sp.set('proxmox-advanced-cb', val); + } + } + }, + { + text: gettext('Back'), + disabled: true, + itemId: 'back', + minWidth: 60, + handler: function() { + var tp = me.down('#wizcontent'); + var atab = tp.getActiveTab(); + var prev = tp.items.indexOf(atab) - 1; + if (prev < 0) { + return; + } + var ntab = tp.items.getAt(prev); + if (ntab) { + tp.setActiveTab(ntab); + } + } + }, + { + text: gettext('Next'), + disabled: true, + itemId: 'next', + minWidth: 60, + handler: function() { + + var form = me.down('form').getForm(); + + var tp = me.down('#wizcontent'); + var atab = tp.getActiveTab(); + if (!check_card(atab)) { + return; + } + + var next = tp.items.indexOf(atab) + 1; + var ntab = tp.items.getAt(next); + if (ntab) { + ntab.enable(); + tp.setActiveTab(ntab); + } + + } + }, + { + text: gettext('Finish'), + minWidth: 60, + hidden: true, + itemId: 'submit', + handler: function() { + var tp = me.down('#wizcontent'); + var atab = tp.getActiveTab(); + atab.onSubmit(); + } + } + ] + }); + me.callParent(); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + panel.setAdvancedVisible(advchecked); + }); + + Ext.Array.each(me.query('field'), function(field) { + var validcheck = function() { + var tp = me.down('#wizcontent'); + + // check tabs from current to the last enabled for validity + // since we might have changed a validity on a later one + var i; + for (i = curidx; i <= maxidx && i < tp.items.getCount(); i++) { + var tab = tp.items.getAt(i); + var valid = check_card(tab); + + // only set the buttons on the current panel + if (i === curidx) { + me.down('#next').setDisabled(!valid); + me.down('#submit').setDisabled(!valid); + } + + // if a panel is invalid, then disable it and all following, + // else enable it and go to the next + var ntab = tp.items.getAt(i + 1); + if (!valid) { + disable_at(ntab); + return; + } else if (ntab && !tab.onSubmit) { + ntab.enable(); + } + } + }; + field.on('change', validcheck); + field.on('validitychange', validcheck); + }); + } +}); +Ext.define('PVE.window.NotesEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.apply(me, { + title: gettext('Notes'), + width: 600, + height: '400px', + resizable: true, + layout: 'fit', + defaultButton: undefined, + items: { + xtype: 'textarea', + name: 'description', + height: '100%', + value: '', + hideLabel: true + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('PVE.window.Backup', { + extend: 'Ext.window.Window', + + resizable: false, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.vmtype) { + throw "no VM type specified"; + } + + var storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: me.nodename, + name: 'storage', + value: me.storage, + fieldLabel: gettext('Storage'), + storageContent: 'backup', + allowBlank: false + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [ + storagesel, + { + xtype: 'pveBackupModeSelector', + fieldLabel: gettext('Mode'), + value: 'snapshot', + name: 'mode' + }, + { + xtype: 'pveCompressionSelector', + name: 'compress', + value: 'lzo', + fieldLabel: gettext('Compression') + }, + { + xtype: 'textfield', + fieldLabel: gettext('Send email to'), + name: 'mailto', + emptyText: Proxmox.Utils.noneText + } + ] + }); + + var form = me.formPanel.getForm(); + + var submitBtn = Ext.create('Ext.Button', { + text: gettext('Backup'), + handler: function(){ + var storage = storagesel.getValue(); + var values = form.getValues(); + var params = { + storage: storage, + vmid: me.vmid, + mode: values.mode, + remove: 0 + }; + + if ( values.mailto ) { + params.mailto = values.mailto; + } + + if (values.compress) { + params.compress = values.compress; + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/vzdump', + params: params, + method: 'POST', + failure: function (response, opts) { + Ext.Msg.alert('Error',response.htmlStatus); + }, + success: function(response, options) { + // close later so we reload the grid + // after the task has completed + me.hide(); + + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + listeners: { + close: function() { + me.close(); + } + } + }); + win.show(); + } + }); + } + }); + + var helpBtn = Ext.create('Proxmox.button.Help', { + onlineHelp: 'chapter_vzdump', + listenToGlobalEvent: false, + hidden: false + }); + + var title = gettext('Backup') + " " + + ((me.vmtype === 'lxc') ? "CT" : "VM") + + " " + me.vmid; + + Ext.apply(me, { + title: title, + width: 350, + modal: true, + layout: 'auto', + border: false, + items: [ me.formPanel ], + buttons: [ helpBtn, '->', submitBtn ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.window.Restore', { + extend: 'Ext.window.Window', // fixme: Proxmox.window.Edit? + + resizable: false, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.volid) { + throw "no volume ID specified"; + } + + if (!me.vmtype) { + throw "no vmtype specified"; + } + + var storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: me.nodename, + name: 'storage', + value: '', + fieldLabel: gettext('Storage'), + storageContent: (me.vmtype === 'lxc') ? 'rootdir' : 'images', + allowBlank: true + }); + + var IDfield; + if (me.vmid) { + IDfield = Ext.create('Ext.form.field.Display', { + name: 'vmid', + value: me.vmid, + fieldLabel: (me.vmtype === 'lxc') ? 'CT' : 'VM' + }); + } else { + IDfield = Ext.create('PVE.form.GuestIDSelector', { + name: 'vmid', + guestType: me.vmtype, + loadNextFreeID: true, + validateExists: false + }); + } + + var items = [ + { + xtype: 'displayfield', + value: me.volidText || me.volid, + fieldLabel: gettext('Source') + }, + storagesel, + IDfield, + { + xtype: 'proxmoxintegerfield', + name: 'bwlimit', + fieldLabel: gettext('Read Limit (MiB/s)'), + minValue: 0, + emptyText: gettext('Defaults to target storage restore limit'), + autoEl: { + tag: 'div', + 'data-qtip': gettext("Use '0' to disable all bandwidth limits.") + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'unique', + fieldLabel: gettext('Unique'), + hidden: !!me.vmid, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Autogenerate unique properties, e.g., MAC addresses') + }, + checked: false + } + ]; + + /*jslint confusion: true*/ + if (me.vmtype === 'lxc') { + items.push({ + xtype: 'proxmoxcheckbox', + name: 'unprivileged', + value: true, + fieldLabel: gettext('Unprivileged container') + }); + } + /*jslint confusion: false*/ + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var doRestore = function(url, params) { + Proxmox.Utils.API2Request({ + url: url, + params: params, + method: 'POST', + waitMsgTarget: me, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + me.close(); + } + }); + }; + + var submitBtn = Ext.create('Ext.Button', { + text: gettext('Restore'), + handler: function(){ + var storage = storagesel.getValue(); + var values = form.getValues(); + + var params = { + storage: storage, + vmid: me.vmid || values.vmid, + force: me.vmid ? 1 : 0 + }; + if (values.unique) { params.unique = 1; } + + if (values.bwlimit !== undefined) { + params.bwlimit = values.bwlimit * 1024; + } + + var url; + var msg; + if (me.vmtype === 'lxc') { + url = '/nodes/' + me.nodename + '/lxc'; + params.ostemplate = me.volid; + params.restore = 1; + if (values.unprivileged) { params.unprivileged = 1; } + msg = Proxmox.Utils.format_task_description('vzrestore', params.vmid); + } else if (me.vmtype === 'qemu') { + url = '/nodes/' + me.nodename + '/qemu'; + params.archive = me.volid; + msg = Proxmox.Utils.format_task_description('qmrestore', params.vmid); + } else { + throw 'unknown VM type'; + } + + if (me.vmid) { + msg += '. ' + gettext('This will permanently erase current VM data.'); + Ext.Msg.confirm(gettext('Confirm'), msg, function(btn) { + if (btn !== 'yes') { + return; + } + doRestore(url, params); + }); + } else { + doRestore(url, params); + } + } + }); + + form.on('validitychange', function(f, valid) { + submitBtn.setDisabled(!valid); + }); + + var title = gettext('Restore') + ": " + ( + (me.vmtype === 'lxc') ? 'CT' : 'VM'); + + if (me.vmid) { + title += " " + me.vmid; + } + + Ext.apply(me, { + title: title, + width: 500, + modal: true, + layout: 'auto', + border: false, + items: [ me.formPanel ], + buttons: [ submitBtn ] + }); + + me.callParent(); + } +}); +/* Popup a message window + * where the user has to manually enter the resource ID + * to enable the destroy button + */ +Ext.define('PVE.window.SafeDestroy', { + extend: 'Ext.window.Window', + alias: 'widget.pveSafeDestroy', + + title: gettext('Confirm'), + modal: true, + buttonAlign: 'center', + bodyPadding: 10, + width: 450, + layout: { type:'hbox' }, + defaultFocus: 'confirmField', + showProgress: false, + + config: { + item: { + id: undefined, + type: undefined + }, + url: undefined, + params: {} + }, + + getParams: function() { + var me = this; + if (Ext.Object.isEmpty(me.params)) { + return ''; + } + return '?' + Ext.Object.toQueryString(me.params); + }, + + controller: { + + xclass: 'Ext.app.ViewController', + + control: { + 'field[name=confirm]': { + change: function(f, value) { + var view = this.getView(); + var removeButton = this.lookupReference('removeButton'); + if (value === view.getItem().id.toString()) { + removeButton.enable(); + } else { + removeButton.disable(); + } + }, + specialkey: function (field, event) { + var removeButton = this.lookupReference('removeButton'); + if (!removeButton.isDisabled() && event.getKey() == event.ENTER) { + removeButton.fireEvent('click', removeButton, event); + } + } + }, + 'button[reference=removeButton]': { + click: function() { + var view = this.getView(); + Proxmox.Utils.API2Request({ + url: view.getUrl() + view.getParams(), + method: 'DELETE', + waitMsgTarget: view, + failure: function(response, opts) { + view.close(); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var hasProgressBar = view.showProgress && + response.result.data ? true : false; + + if (hasProgressBar) { + // stay around so we can trigger our close events + // when background action is completed + view.hide(); + + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + listeners: { + destroy: function () { + view.close(); + } + } + }); + win.show(); + } else { + view.close(); + } + } + }); + } + } + } + }, + + items: [ + { + xtype: 'component', + cls: [ Ext.baseCSSPrefix + 'message-box-icon', + Ext.baseCSSPrefix + 'message-box-warning', + Ext.baseCSSPrefix + 'dlg-icon'] + }, + { + xtype: 'container', + flex: 1, + layout: { + type: 'vbox', + align: 'stretch' + }, + items: [ + { + xtype: 'component', + reference: 'messageCmp' + }, + { + itemId: 'confirmField', + reference: 'confirmField', + xtype: 'textfield', + name: 'confirm', + labelWidth: 300, + hideTrigger: true, + allowBlank: false + } + ] + } + ], + buttons: [ + { + reference: 'removeButton', + text: gettext('Remove'), + disabled: true + } + ], + + initComponent : function() { + var me = this; + + me.callParent(); + + var item = me.getItem(); + + if (!Ext.isDefined(item.id)) { + throw "no ID specified"; + } + + if (!Ext.isDefined(item.type)) { + throw "no VM type specified"; + } + + var messageCmp = me.lookupReference('messageCmp'); + var msg; + + if (item.type === 'VM') { + msg = Proxmox.Utils.format_task_description('qmdestroy', item.id); + } else if (item.type === 'CT') { + msg = Proxmox.Utils.format_task_description('vzdestroy', item.id); + } else if (item.type === 'CephPool') { + msg = Proxmox.Utils.format_task_description('cephdestroypool', item.id); + } else if (item.type === 'Image') { + msg = Proxmox.Utils.format_task_description('unknownimgdel', item.id); + } else { + throw "unknown item type specified"; + } + + messageCmp.setHtml(msg); + + var confirmField = me.lookupReference('confirmField'); + msg = gettext('Please enter the ID to confirm') + + ' (' + item.id + ')'; + confirmField.setFieldLabel(msg); + } +}); +Ext.define('PVE.window.BackupConfig', { + extend: 'Ext.window.Window', + title: gettext('Configuration'), + width: 600, + height: 400, + layout: 'fit', + modal: true, + items: { + xtype: 'component', + itemId: 'configtext', + autoScroll: true, + style: { + 'background-color': 'white', + 'white-space': 'pre', + 'font-family': 'monospace', + padding: '5px' + } + }, + + initComponent: function() { + var me = this; + + if (!me.volume) { + throw "no volume specified"; + } + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.callParent(); + + Proxmox.Utils.API2Request({ + url: "/nodes/" + nodename + "/vzdump/extractconfig", + method: 'GET', + params: { + volume: me.volume + }, + failure: function(response, opts) { + me.close(); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response,options) { + me.show(); + me.down('#configtext').update(Ext.htmlEncode(response.result.data)); + } + }); + } +}); +Ext.define('PVE.window.Settings', { + extend: 'Ext.window.Window', + + width: '800px', + title: gettext('My Settings'), + iconCls: 'fa fa-gear', + modal: true, + bodyPadding: 10, + resizable: false, + + buttons: [ + { + xtype: 'proxmoxHelpButton', + onlineHelp: 'gui_my_settings', + hidden: false + }, + '->', + { + text: gettext('Close'), + handler: function() { + this.up('window').close(); + } + } + ], + + layout: { + type: 'hbox', + align: 'top' + }, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + var me = this; + var sp = Ext.state.Manager.getProvider(); + + var username = sp.get('login-username') || Proxmox.Utils.noneText; + me.lookupReference('savedUserName').setValue(username); + + var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight']; + settings.forEach(function(setting) { + var val = localStorage.getItem('pve-xterm-' + setting); + if (val !== undefined && val !== null) { + var field = me.lookup(setting); + field.setValue(val); + field.resetOriginalValue(); + } + }); + }, + + set_button_status: function() { + var me = this; + + var form = me.lookup('xtermform'); + var valid = form.isValid(); + var dirty = form.isDirty(); + + var hasvalues = false; + var values = form.getValues(); + Ext.Object.eachValue(values, function(value) { + if (value) { + hasvalues = true; + return false; + } + }); + + me.lookup('xtermsave').setDisabled(!dirty || !valid); + me.lookup('xtermreset').setDisabled(!hasvalues); + }, + + control: { + '#xtermjs form': { + dirtychange: 'set_button_status', + validitychange: 'set_button_status' + }, + '#xtermjs button': { + click: function(button) { + var me = this; + var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight']; + settings.forEach(function(setting) { + var field = me.lookup(setting); + if (button.reference === 'xtermsave') { + var value = field.getValue(); + if (value) { + localStorage.setItem('pve-xterm-' + setting, value); + } else { + localStorage.removeItem('pve-xterm-' + setting); + } + } else if (button.reference === 'xtermreset') { + field.setValue(undefined); + localStorage.removeItem('pve-xterm-' + setting); + } + field.resetOriginalValue(); + }); + me.set_button_status(); + } + }, + 'button[name=reset]': { + click: function () { + var blacklist = ['GuiCap', 'login-username', 'dash-storages']; + var sp = Ext.state.Manager.getProvider(); + var state; + for (state in sp.state) { + if (sp.state.hasOwnProperty(state)) { + if (blacklist.indexOf(state) !== -1) { + continue; + } + + sp.clear(state); + } + } + + window.location.reload(); + } + }, + 'button[name=clear-username]': { + click: function () { + var me = this; + var usernamefield = me.lookupReference('savedUserName'); + var sp = Ext.state.Manager.getProvider(); + + usernamefield.setValue(Proxmox.Utils.noneText); + sp.clear('login-username'); + } + }, + 'grid[reference=dashboard-storages]': { + selectionchange: function(grid, selected) { + var me = this; + var sp = Ext.state.Manager.getProvider(); + + // saves the selected storageids as + // "id1,id2,id3,..." + // or clears the variable + if (selected.length > 0) { + sp.set('dash-storages', + Ext.Array.pluck(selected, 'id').join(',')); + } else { + sp.clear('dash-storages'); + } + }, + afterrender: function(grid) { + var me = grid; + var sp = Ext.state.Manager.getProvider(); + var store = me.getStore(); + var items = []; + me.suspendEvent('selectionchange'); + var storages = sp.get('dash-storages') || ''; + storages.split(',').forEach(function(storage){ + // we have to get the records + // to be able to select them + if (storage !== '') { + var item = store.getById(storage); + if (item) { + items.push(item); + } + } + }); + me.getSelectionModel().select(items); + me.resumeEvent('selectionchange'); + } + } + } + }, + + items: [{ + xtype: 'fieldset', + width: '50%', + title: gettext('Webinterface Settings'), + margin: '5', + layout: { + type: 'vbox', + align: 'left' + }, + defaults: { + width: '100%', + margin: '0 0 10 0' + }, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Dashboard Storages'), + labelAlign: 'left', + labelWidth: '50%' + }, + { + xtype: 'grid', + maxHeight: 150, + reference: 'dashboard-storages', + selModel: { + selType: 'checkboxmodel' + }, + columns: [{ + header: gettext('Name'), + dataIndex: 'storage', + flex: 1 + },{ + header: gettext('Node'), + dataIndex: 'node', + flex: 1 + }], + store: { + type: 'diff', + field: ['type', 'storage', 'id', 'node'], + rstore: PVE.data.ResourceStore, + filters: [{ + property: 'type', + value: 'storage' + }], + sorters: [ 'node','storage'] + } + }, + { + xtype: 'box', + autoEl: { tag: 'hr'} + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Saved User name'), + labelAlign: 'left', + labelWidth: '50%', + stateId: 'login-username', + reference: 'savedUserName', + value: '' + }, + { + xtype: 'button', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + text: gettext('Clear User name'), + width: 'auto', + name: 'clear-username' + }, + { + xtype: 'box', + autoEl: { tag: 'hr'} + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Layout'), + labelAlign: 'left', + labelWidth: '50%' + }, + { + xtype: 'button', + cls: 'x-btn-default-toolbar-small proxmox-inline-button', + text: gettext('Reset Layout'), + width: 'auto', + name: 'reset' + } + ] + },{ + xtype: 'fieldset', + itemId: 'xtermjs', + width: '50%', + margin: '5', + title: gettext('xterm.js Settings'), + items: [{ + xtype: 'form', + reference: 'xtermform', + border: false, + layout: { + type: 'vbox', + algin: 'left' + }, + defaults: { + width: '100%', + margin: '0 0 10 0' + }, + items: [ + { + xtype: 'textfield', + name: 'fontFamily', + reference: 'fontFamily', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Font-Family') + }, + { + xtype: 'proxmoxintegerfield', + emptyText: Proxmox.Utils.defaultText, + name: 'fontSize', + reference: 'fontSize', + minValue: 1, + fieldLabel: gettext('Font-Size') + }, + { + xtype: 'numberfield', + name: 'letterSpacing', + reference: 'letterSpacing', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Letter Spacing') + }, + { + xtype: 'numberfield', + name: 'lineHeight', + minValue: 0.1, + reference: 'lineHeight', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Line Height') + }, + { + xtype: 'container', + layout: { + type: 'hbox', + pack: 'end' + }, + items: [ + { + xtype: 'button', + reference: 'xtermreset', + disabled: true, + text: gettext('Reset') + }, + { + xtype: 'button', + reference: 'xtermsave', + disabled: true, + text: gettext('Save') + } + ] + } + ] + }] + }], + + onShow: function() { + var me = this; + me.callParent(); + } +}); +Ext.define('PVE.panel.StartupInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'qm_startup_and_shutdown', + + onGetValues: function(values) { + var me = this; + + var res = PVE.Parser.printStartup(values); + + if (res === undefined || res === '') { + return { 'delete': 'startup' }; + } + + return { startup: res }; + }, + + setStartup: function(value) { + var me = this; + + var startup = PVE.Parser.parseStartup(value); + if (startup) { + me.setValues(startup); + } + }, + + initComponent : function() { + var me = this; + + me.items = [ + { + xtype: 'textfield', + name: 'order', + defaultValue: '', + emptyText: 'any', + fieldLabel: gettext('Start/Shutdown order') + }, + { + xtype: 'textfield', + name: 'up', + defaultValue: '', + emptyText: 'default', + fieldLabel: gettext('Startup delay') + }, + { + xtype: 'textfield', + name: 'down', + defaultValue: '', + emptyText: 'default', + fieldLabel: gettext('Shutdown timeout') + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.window.StartupEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveWindowStartupEdit', + onlineHelp: undefined, + + initComponent : function() { + + var me = this; + var ipanelConfig = me.onlineHelp ? {onlineHelp: me.onlineHelp} : {}; + var ipanel = Ext.create('PVE.panel.StartupInputPanel', ipanelConfig); + + Ext.applyIf(me, { + subject: gettext('Start/Shutdown order'), + fieldDefaults: { + labelWidth: 120 + }, + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var i, confid; + me.vmconfig = response.result.data; + ipanel.setStartup(me.vmconfig.startup); + } + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.ceph.Install', { + extend: 'Ext.window.Window', + xtype: 'pveCephInstallWindow', + mixins: ['Proxmox.Mixin.CBind'], + + width: 220, + header: false, + resizable: false, + draggable: false, + modal: true, + nodename: undefined, + shadow: false, + border: false, + bodyBorder: false, + closable: false, + cls: 'install-mask', + bodyCls: 'install-mask', + layout: { + align: 'stretch', + pack: 'center', + type: 'vbox' + }, + viewModel: { + data: { + cephVersion: 'nautilus', + isInstalled: false + }, + formulas: { + buttonText: function (get){ + if (get('isInstalled')) { + return gettext('Configure Ceph'); + } else { + return gettext('Install Ceph-') + get('cephVersion'); + } + }, + windowText: function (get) { + if (get('isInstalled')) { + return '

' + + Ext.String.format(gettext('{0} is not initialized.'), 'Ceph') + ' '+ + gettext('You need to create a initial config once.') + '

'; + } else { + return '

' + + Ext.String.format(gettext('{0} is not installed on this node.'), 'Ceph') + '
' + + gettext('Would you like to install it now?') + '

'; + } + } + } + }, + items: [ + { + bind: { + html: '{windowText}' + }, + border: false, + padding: 5, + bodyCls: 'install-mask' + + }, + { + xtype: 'button', + bind: { + text: '{buttonText}' + }, + viewModel: {}, + cbind: { + nodename: '{nodename}' + }, + handler: function() { + var me = this.up('pveCephInstallWindow'); + var win = Ext.create('PVE.ceph.CephInstallWizard',{ + nodename: me.nodename + }); + win.getViewModel().set('isInstalled', this.getViewModel().get('isInstalled')); + win.show(); + me.mon(win,'beforeClose', function(){ + me.fireEvent("cephInstallWindowClosed"); + me.close(); + }); + + } + } + ] +}); +/*jslint confusion: true*/ +Ext.define('PVE.FirewallEnableEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveFirewallEnableEdit'], + mixins: ['Proxmox.Mixin.CBind'], + + subject: gettext('Firewall'), + cbindData: { + defaultValue: 0 + }, + width: 350, + + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + uncheckedValue: 0, + cbind: { + defaultValue: '{defaultValue}', + checked: '{defaultValue}' + }, + deleteDefaultValue: false, + fieldLabel: gettext('Firewall') + }, + { + xtype: 'displayfield', + name: 'warning', + userCls: 'pve-hint', + value: gettext('Warning: Firewall still disabled at datacenter level!'), + hidden: true + } + ], + + beforeShow: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/cluster/firewall/options', + method: 'GET', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + if (!response.result.data.enable) { + me.down('displayfield[name=warning]').setVisible(true); + } + } + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.FirewallLograteInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveFirewallLograteInputPanel', + + viewModel: {}, + + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + reference: 'enable', + fieldLabel: gettext('Enable'), + value: true + }, + { + layout: 'hbox', + border: false, + items: [ + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Log rate limit'), + minValue: 1, + maxValue: 99, + allowBlank: false, + flex: 2, + value: 1 + }, + { + xtype: 'box', + html: '
/
' + }, + { + xtype: 'proxmoxKVComboBox', + name: 'unit', + comboItems: [['second', 'second'], ['minute', 'minute'], + ['hour', 'hour'], ['day', 'day']], + allowBlank: false, + flex: 1, + value: 'second' + } + ] + }, + { + xtype: 'numberfield', + name: 'burst', + fieldLabel: gettext('Log burst limit'), + minValue: 1, + maxValue: 99, + value: 5 + } + ], + + onGetValues: function(values) { + var me = this; + + var vals = {}; + vals.enable = values.enable !== undefined ? 1 : 0; + vals.rate = values.rate + '/' + values.unit; + vals.burst = values.burst; + var properties = PVE.Parser.printPropertyString(vals, undefined); + if (properties == '') { + return { 'delete': 'log_ratelimit' }; + } + return { log_ratelimit: properties }; + }, + + setValues: function(values) { + var me = this; + + var properties = {}; + if (values.log_ratelimit !== undefined) { + properties = PVE.Parser.parsePropertyString(values.log_ratelimit, 'enable'); + if (properties.rate) { + var matches = properties.rate.match(/^(\d+)\/(second|minute|hour|day)$/); + if (matches) { + properties.rate = matches[1]; + properties.unit = matches[2]; + } + } + } + me.callParent([properties]); + } +}); + +Ext.define('PVE.FirewallLograteEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveFirewallLograteEdit', + + subject: gettext('Log rate limit'), + + items: [{ + xtype: 'pveFirewallLograteInputPanel' + }], + autoLoad: true +}); +Ext.define('PVE.panel.NotesView', { + extend: 'Ext.panel.Panel', + xtype: 'pveNotesView', + + title: gettext("Notes"), + bodyStyle: 'white-space:pre', + bodyPadding: 10, + scrollable: true, + + tbar: { + itemId: 'tbar', + hidden: true, + items: [ + { + text: gettext('Edit'), + handler: function() { + var me = this.up('panel'); + me.run_editor(); + } + } + ] + }, + + run_editor: function() { + var me = this; + var win = Ext.create('PVE.window.NotesEdit', { + pveSelNode: me.pveSelNode, + url: me.url + }); + win.show(); + win.on('destroy', me.load, me); + }, + + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + }, + success: function(response, opts) { + var data = response.result.data.description || ''; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + listeners: { + render: function(c) { + var me = this; + me.getEl().on('dblclick', me.run_editor, me); + } + }, + + tools: [{ + type: 'gear', + handler: function() { + var me = this.up('panel'); + me.run_editor(); + } + }], + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var type = me.pveSelNode.data.type; + if (!Ext.Array.contains(['node', 'qemu', 'lxc'], type)) { + throw 'invalid type specified'; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid && type !== 'node') { + throw "no VM ID specified"; + } + + me.url = '/api2/extjs/nodes/' + nodename + '/'; + + // add the type specific path if qemu/lxc + if (type === 'qemu' || type === 'lxc') { + me.url += type + '/' + vmid + '/'; + } + + me.url += 'config'; + + me.callParent(); + if (type === 'node') { + me.down('#tbar').setVisible(true); + } + me.load(); + } +}); +Ext.define('PVE.grid.ResourceGrid', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveResourceGrid'], + + border: false, + defaultSorter: { + property: 'type', + direction: 'ASC' + }, + initComponent : function() { + var me = this; + + var rstore = PVE.data.ResourceStore; + var sp = Ext.state.Manager.getProvider(); + + var coldef = rstore.defaultColumns(); + + var store = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: me.defaultSorter, + proxy: { type: 'memory' } + }); + + var textfilter = ''; + + var textfilter_match = function(item) { + var match = false; + Ext.each(['name', 'storage', 'node', 'type', 'text'], function(field) { + var v = item.data[field]; + if (v !== undefined) { + v = v.toLowerCase(); + if (v.indexOf(textfilter) >= 0) { + match = true; + return false; + } + } + }); + return match; + }; + + var updateGrid = function() { + + var filterfn = me.viewFilter ? me.viewFilter.filterfn : null; + + //console.log("START GRID UPDATE " + me.viewFilter); + + store.suspendEvents(); + + var nodeidx = {}; + var gather_child_nodes = function(cn) { + if (!cn) { + return; + } + var cs = cn.childNodes; + if (!cs) { + return; + } + var len = cs.length, i = 0, n, res; + + for (; i < len; i++) { + var child = cs[i]; + var orgnode = rstore.data.get(child.data.id); + if (orgnode) { + if ((!filterfn || filterfn(child)) && + (!textfilter || textfilter_match(child))) { + nodeidx[child.data.id] = orgnode; + } + } + gather_child_nodes(child); + } + }; + gather_child_nodes(me.pveSelNode); + + // remove vanished items + var rmlist = []; + store.each(function(olditem) { + var item = nodeidx[olditem.data.id]; + if (!item) { + //console.log("GRID REM UID: " + olditem.data.id); + rmlist.push(olditem); + } + }); + + if (rmlist.length) { + store.remove(rmlist); + } + + // add new items + var addlist = []; + var key; + for (key in nodeidx) { + if (nodeidx.hasOwnProperty(key)) { + var item = nodeidx[key]; + + // getById() use find(), which is slow (ExtJS4 DP5) + //var olditem = store.getById(item.data.id); + var olditem = store.data.get(item.data.id); + + if (!olditem) { + //console.log("GRID ADD UID: " + item.data.id); + var info = Ext.apply({}, item.data); + var child = Ext.create(store.model, info); + addlist.push(item); + continue; + } + // try to detect changes + var changes = false; + var fieldkeys = PVE.data.ResourceStore.fieldNames; + var fieldcount = fieldkeys.length; + var fieldind; + for (fieldind = 0; fieldind < fieldcount; fieldind++) { + var field = fieldkeys[fieldind]; + if (field != 'id' && item.data[field] != olditem.data[field]) { + changes = true; + //console.log("changed item " + item.id + " " + field + " " + item.data[field] + " != " + olditem.data[field]); + olditem.beginEdit(); + olditem.set(field, item.data[field]); + } + } + if (changes) { + olditem.endEdit(true); + olditem.commit(true); + } + } + } + + if (addlist.length) { + store.add(addlist); + } + + store.sort(); + + store.resumeEvents(); + + store.fireEvent('refresh', store); + + //console.log("END GRID UPDATE"); + }; + + var filter_task = new Ext.util.DelayedTask(function(){ + updateGrid(); + }); + + var load_cb = function() { + updateGrid(); + }; + + Ext.apply(me, { + store: store, + stateful: true, + stateId: 'grid-resource', + tbar: [ + '->', + gettext('Search') + ':', ' ', + { + xtype: 'textfield', + width: 200, + value: textfilter, + enableKeyEvents: true, + listeners: { + keyup: function(field, e) { + var v = field.getValue(); + textfilter = v.toLowerCase(); + filter_task.delay(500); + } + } + } + ], + viewConfig: { + stripeRows: true + }, + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + itemdblclick: function(v, record) { + var ws = me.up('pveStdWorkspace'); + ws.selectById(record.data.id); + }, + destroy: function() { + rstore.un("load", load_cb); + } + }, + columns: coldef + }); + me.callParent(); + updateGrid(); + rstore.on("load", load_cb); + } +}); +Ext.define('PVE.pool.AddVM', { + extend: 'Proxmox.window.Edit', + width: 600, + height: 400, + isAdd: true, + isCreate: true, + initComponent : function() { + + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + me.url = "/pools/" + me.pool; + me.method = 'PUT'; + + var vmsField = Ext.create('Ext.form.field.Text', { + name: 'vms', + hidden: true, + allowBlank: false + }); + + var vmStore = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: [ + { + property: 'vmid', + order: 'ASC' + } + ], + filters: [ + function(item) { + return ((item.data.type === 'lxc' || item.data.type === 'qemu') && item.data.pool === ''); + } + ] + }); + + var vmGrid = Ext.create('widget.grid',{ + store: vmStore, + border: true, + height: 300, + scrollable: true, + selModel: { + selType: 'checkboxmodel', + mode: 'SIMPLE', + listeners: { + selectionchange: function(model, selected, opts) { + var selectedVms = []; + selected.forEach(function(vm) { + selectedVms.push(vm.data.vmid); + }); + vmsField.setValue(selectedVms); + } + } + }, + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 60 + }, + { + header: gettext('Node'), + dataIndex: 'node' + }, + { + header: gettext('Status'), + dataIndex: 'uptime', + renderer: function(value) { + if (value) { + return Proxmox.Utils.runningText; + } else { + return Proxmox.Utils.stoppedText; + } + } + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + header: gettext('Type'), + dataIndex: 'type' + } + ] + }); + Ext.apply(me, { + subject: gettext('Virtual Machine'), + items: [ vmsField, vmGrid ] + }); + + me.callParent(); + vmStore.load(); + } +}); + +Ext.define('PVE.pool.AddStorage', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + me.isCreate = true; + me.isAdd = true; + me.url = "/pools/" + me.pool; + me.method = 'PUT'; + + Ext.apply(me, { + subject: gettext('Storage'), + width: 350, + items: [ + { + xtype: 'pveStorageSelector', + name: 'storage', + nodename: 'localhost', + autoSelect: false, + value: '', + fieldLabel: gettext("Storage") + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.grid.PoolMembers', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pvePoolMembers'], + + // fixme: dynamic status update ? + + stateful: true, + stateId: 'grid-pool-members', + + initComponent : function() { + var me = this; + + if (!me.pool) { + throw "no pool specified"; + } + + var store = Ext.create('Ext.data.Store', { + model: 'PVEResources', + sorters: [ + { + property : 'type', + direction: 'ASC' + } + ], + proxy: { + type: 'proxmox', + root: 'data.members', + url: "/api2/json/pools/" + me.pool + } + }); + + var coldef = PVE.data.ResourceStore.defaultColumns(); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: function (rec) { + return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.id + "'"); + }, + handler: function(btn, event, rec) { + var params = { 'delete': 1 }; + if (rec.data.type === 'storage') { + params.storage = rec.data.storage; + } else if (rec.data.type === 'qemu' || rec.data.type === 'lxc' || rec.data.type === 'openvz') { + params.vms = rec.data.vmid; + } else { + throw "unknown resource type"; + } + + Proxmox.Utils.API2Request({ + url: '/pools/' + me.pool, + method: 'PUT', + params: params, + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Virtual Machine'), + iconCls: 'pve-itype-icon-qemu', + handler: function() { + var win = Ext.create('PVE.pool.AddVM', { pool: me.pool }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('Storage'), + iconCls: 'pve-itype-icon-storage', + handler: function() { + var win = Ext.create('PVE.pool.AddStorage', { pool: me.pool }); + win.on('destroy', reload); + win.show(); + } + } + ] + }) + }, + remove_btn + ], + viewConfig: { + stripeRows: true + }, + columns: coldef, + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + itemdblclick: function(v, record) { + var ws = me.up('pveStdWorkspace'); + ws.selectById(record.data.id); + }, + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.form.FWMacroSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: 'widget.pveFWMacroSelector', + allowBlank: true, + autoSelect: false, + valueField: 'macro', + displayField: 'macro', + listConfig: { + columns: [ + { + header: gettext('Macro'), + dataIndex: 'macro', + hideable: false, + width: 100 + }, + { + header: gettext('Description'), + renderer: Ext.String.htmlEncode, + flex: 1, + dataIndex: 'descr' + } + ] + }, + initComponent: function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: true, + fields: [ 'macro', 'descr' ], + idProperty: 'macro', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/firewall/macros" + }, + sorters: { + property: 'macro', + order: 'DESC' + } + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.FirewallRulePanel', { + extend: 'Proxmox.panel.InputPanel', + + allow_iface: false, + + list_refs_url: undefined, + + onGetValues: function(values) { + var me = this; + + // hack: editable ComboGrid returns nothing when empty, so we need to set '' + // Also, disabled text fields return nothing, so we need to set '' + + Ext.Array.each(['source', 'dest', 'macro', 'proto', 'sport', 'dport', 'log'], function(key) { + if (values[key] === undefined) { + values[key] = ''; + } + }); + + delete values.modified_marker; + + return values; + }, + + initComponent : function() { + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + me.column1 = [ + { + // hack: we use this field to mark the form 'dirty' when the + // record has errors- so that the user can safe the unmodified + // form again. + xtype: 'hiddenfield', + name: 'modified_marker', + value: '' + }, + { + xtype: 'proxmoxKVComboBox', + name: 'type', + value: 'in', + comboItems: [['in', 'in'], ['out', 'out']], + fieldLabel: gettext('Direction'), + allowBlank: false + }, + { + xtype: 'proxmoxKVComboBox', + name: 'action', + value: 'ACCEPT', + comboItems: [['ACCEPT', 'ACCEPT'], ['DROP', 'DROP'], ['REJECT', 'REJECT']], + fieldLabel: gettext('Action'), + allowBlank: false + } + ]; + + if (me.allow_iface) { + me.column1.push({ + xtype: 'proxmoxtextfield', + name: 'iface', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('Interface') + }); + } else { + me.column1.push({ + xtype: 'displayfield', + fieldLabel: '', + value: '' + }); + } + + me.column1.push( + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '' + }, + { + xtype: 'pveIPRefSelector', + name: 'source', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('Source') + + }, + { + xtype: 'pveIPRefSelector', + name: 'dest', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('Destination') + } + ); + + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Enable') + }, + { + xtype: 'pveFWMacroSelector', + name: 'macro', + fieldLabel: gettext('Macro'), + editable: true, + allowBlank: true, + listeners: { + change: function(f, value) { + if (value === null) { + me.down('field[name=proto]').setDisabled(false); + me.down('field[name=sport]').setDisabled(false); + me.down('field[name=dport]').setDisabled(false); + } else { + me.down('field[name=proto]').setDisabled(true); + me.down('field[name=proto]').setValue(''); + me.down('field[name=sport]').setDisabled(true); + me.down('field[name=sport]').setValue(''); + me.down('field[name=dport]').setDisabled(true); + me.down('field[name=dport]').setValue(''); + } + } + } + }, + { + xtype: 'pveIPProtocolSelector', + name: 'proto', + autoSelect: false, + editable: true, + value: '', + fieldLabel: gettext('Protocol') + }, + { + xtype: 'displayfield', + fieldLabel: '', + height: 7, + value: '' + }, + { + xtype: 'textfield', + name: 'sport', + value: '', + fieldLabel: gettext('Source port') + }, + { + xtype: 'textfield', + name: 'dport', + value: '', + fieldLabel: gettext('Dest. port') + } + ]; + + me.advancedColumn1 = [ + { + xtype: 'pveFirewallLogLevels' + } + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.FirewallRuleEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + list_refs_url: undefined, + + allow_iface: false, + + initComponent : function() { + + var me = this; + + if (!me.base_url) { + throw "no base_url specified"; + } + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + me.isCreate = (me.rule_pos === undefined); + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString(); + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.FirewallRulePanel', { + isCreate: me.isCreate, + list_refs_url: me.list_refs_url, + allow_iface: me.allow_iface, + rule_pos: me.rule_pos + }); + + Ext.apply(me, { + subject: gettext('Rule'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + if (values.errors) { + var field = me.query('[isFormField][name=modified_marker]')[0]; + field.setValue(1); + Ext.Function.defer(function() { + var form = ipanel.up('form').getForm(); + form.markInvalid(values.errors); + }, 100); + } + } + }); + } else if (me.rec) { + ipanel.setValues(me.rec.data); + } + } +}); + +Ext.define('PVE.FirewallGroupRuleEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + + allow_iface: false, + + initComponent : function() { + + var me = this; + + me.isCreate = (me.rule_pos === undefined); + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.rule_pos.toString(); + me.method = 'PUT'; + } + + var column1 = [ + { + xtype: 'hiddenfield', + name: 'type', + value: 'group' + }, + { + xtype: 'pveSecurityGroupsSelector', + name: 'action', + value: '', + fieldLabel: gettext('Security Group'), + allowBlank: false + } + ]; + + if (me.allow_iface) { + column1.push({ + xtype: 'proxmoxtextfield', + name: 'iface', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('Interface') + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + column1: column1, + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Enable') + } + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + + Ext.apply(me, { + subject: gettext('Rule'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('PVE.FirewallRules', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveFirewallRules', + + onlineHelp: 'chapter_pve_firewall', + + stateful: true, + stateId: 'grid-firewall-rules', + + base_url: undefined, + list_refs_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + groupBtn: undefined, + + tbar_prefix: undefined, + + allow_groups: true, + allow_iface: false, + + setBaseUrl: function(url) { + var me = this; + + me.base_url = url; + + if (url === undefined) { + me.addBtn.setDisabled(true); + if (me.groupBtn) { + me.groupBtn.setDisabled(true); + } + me.store.removeAll(); + } else { + me.addBtn.setDisabled(false); + me.removeBtn.baseurl = url + '/'; + if (me.groupBtn) { + me.groupBtn.setDisabled(false); + } + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json' + url + }); + + me.store.load(); + } + }, + + moveRule: function(from, to) { + var me = this; + + if (!me.base_url) { + return; + } + + Proxmox.Utils.API2Request({ + url: me.base_url + "/" + from, + method: 'PUT', + params: { moveto: to }, + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + } + }); + }, + + updateRule: function(rule) { + var me = this; + + if (!me.base_url) { + return; + } + + rule.enable = rule.enable ? 1 : 0; + + var pos = rule.pos; + delete rule.pos; + delete rule.errors; + + Proxmox.Utils.API2Request({ + url: me.base_url + '/' + pos.toString(), + method: 'PUT', + params: rule, + waitMsgTarget: me, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: function() { + me.store.load(); + } + }); + }, + + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + var store = Ext.create('Ext.data.Store',{ + model: 'pve-fw-rule' + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var type = rec.data.type; + + var editor; + if (type === 'in' || type === 'out') { + editor = 'PVE.FirewallRuleEdit'; + } else if (type === 'group') { + editor = 'PVE.FirewallGroupRuleEdit'; + } else { + return; + } + + var win = Ext.create(editor, { + digest: rec.data.digest, + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url, + rule_pos: rec.data.pos + }); + + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = Ext.create('Proxmox.button.Button',{ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = Ext.create('Ext.Button', { + text: gettext('Add'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.FirewallRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url + }); + win.on('destroy', reload); + win.show(); + } + }); + + var run_copy_editor = function() { + var rec = sm.getSelection()[0]; + + if (!rec) { + return; + } + var type = rec.data.type; + + + if (!(type === 'in' || type === 'out')) { + return; + } + + var win = Ext.create('PVE.FirewallRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url, + list_refs_url: me.list_refs_url, + rec: rec + }); + + win.show(); + win.on('destroy', reload); + }; + + me.copyBtn = Ext.create('Proxmox.button.Button',{ + text: gettext('Copy'), + selModel: sm, + enableFn: function(rec) { + return (rec.data.type === 'in' || rec.data.type === 'out'); + }, + disabled: true, + handler: run_copy_editor + }); + + if (me.allow_groups) { + me.groupBtn = Ext.create('Ext.Button', { + text: gettext('Insert') + ': ' + + gettext('Security Group'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.FirewallGroupRuleEdit', { + allow_iface: me.allow_iface, + base_url: me.base_url + }); + win.on('destroy', reload); + win.show(); + } + }); + } + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton',{ + selModel: sm, + baseurl: me.base_url + '/', + confirmMsg: false, + getRecordName: function(rec) { + var rule = rec.data; + return rule.pos.toString() + + '?digest=' + encodeURIComponent(rule.digest); + }, + callback: function() { + me.store.load(); + } + }); + + var tbar = me.tbar_prefix ? [ me.tbar_prefix ] : []; + tbar.push(me.addBtn, me.copyBtn); + if (me.groupBtn) { + tbar.push(me.groupBtn); + } + tbar.push(me.removeBtn, me.editBtn); + + var render_errors = function(name, value, metaData, record) { + var errors = record.data.errors; + if (errors && errors[name]) { + metaData.tdCls = 'proxmox-invalid-row'; + var html = '

' + Ext.htmlEncode(errors[name]) + '

'; + metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + + html.replace(/\"/g,'"') + '"'; + } + return value; + }; + + var columns = [ + { + // similar to xtype: 'rownumberer', + dataIndex: 'pos', + resizable: false, + width: 23, + sortable: false, + align: 'right', + hideable: false, + menuDisabled: true, + renderer: function(value, metaData, record, rowIdx, colIdx, store) { + metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special'; + if (value >= 0) { + return value; + } + return ''; + } + }, + { + xtype: 'checkcolumn', + header: gettext('Enable'), + dataIndex: 'enable', + listeners: { + checkchange: function(column, recordIndex, checked) { + var record = me.getStore().getData().items[recordIndex]; + record.commit(); + var data = {}; + Ext.Array.forEach(record.getFields(), function(field) { + data[field.name] = record.get(field.name); + }); + if (!me.allow_iface || !data.iface) { + delete data.iface; + } + me.updateRule(data); + } + }, + width: 50 + }, + { + header: gettext('Type'), + dataIndex: 'type', + renderer: function(value, metaData, record) { + return render_errors('type', value, metaData, record); + }, + width: 50 + }, + { + header: gettext('Action'), + dataIndex: 'action', + renderer: function(value, metaData, record) { + return render_errors('action', value, metaData, record); + }, + width: 80 + }, + { + header: gettext('Macro'), + dataIndex: 'macro', + renderer: function(value, metaData, record) { + return render_errors('macro', value, metaData, record); + }, + width: 80 + } + ]; + + if (me.allow_iface) { + columns.push({ + header: gettext('Interface'), + dataIndex: 'iface', + renderer: function(value, metaData, record) { + return render_errors('iface', value, metaData, record); + }, + width: 80 + }); + } + + columns.push( + { + header: gettext('Source'), + dataIndex: 'source', + renderer: function(value, metaData, record) { + return render_errors('source', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Destination'), + dataIndex: 'dest', + renderer: function(value, metaData, record) { + return render_errors('dest', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Protocol'), + dataIndex: 'proto', + renderer: function(value, metaData, record) { + return render_errors('proto', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Dest. port'), + dataIndex: 'dport', + renderer: function(value, metaData, record) { + return render_errors('dport', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Source port'), + dataIndex: 'sport', + renderer: function(value, metaData, record) { + return render_errors('sport', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Log level'), + dataIndex: 'log', + renderer: function(value, metaData, record) { + return render_errors('log', value, metaData, record); + }, + width: 100 + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + flex: 1, + renderer: function(value, metaData, record) { + return render_errors('comment', Ext.util.Format.htmlEncode(value), metaData, record); + } + } + ); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + plugins: [ + { + ptype: 'gridviewdragdrop', + dragGroup: 'FWRuleDDGroup', + dropGroup: 'FWRuleDDGroup' + } + ], + listeners: { + beforedrop: function(node, data, dropRec, dropPosition) { + if (!dropRec) { + return false; // empty view + } + var moveto = dropRec.get('pos'); + if (dropPosition === 'after') { + moveto++; + } + var pos = data.records[0].get('pos'); + me.moveRule(pos, moveto); + return 0; + }, + itemdblclick: run_editor + } + }, + sortableColumns: false, + columns: columns + }); + + me.callParent(); + + if (me.base_url) { + me.setBaseUrl(me.base_url); // load + } + } +}, function() { + + Ext.define('pve-fw-rule', { + extend: 'Ext.data.Model', + fields: [ { name: 'enable', type: 'boolean' }, + 'type', 'action', 'macro', 'source', 'dest', 'proto', 'iface', + 'dport', 'sport', 'comment', 'pos', 'digest', 'errors' ], + idProperty: 'pos' + }); + +}); +Ext.define('PVE.FirewallAliasEdit', { + extend: 'Proxmox.window.Edit', + + base_url: undefined, + + alias_name: undefined, + + initComponent : function() { + + var me = this; + + me.isCreate = (me.alias_name === undefined); + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.alias_name; + me.method = 'PUT'; + } + + var items = [ + { + xtype: 'textfield', + name: me.isCreate ? 'name' : 'rename', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'cidr', + fieldLabel: gettext('IP/CIDR'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + } + ]; + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + items: items + }); + + Ext.apply(me, { + subject: gettext('Alias'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + values.rename = values.name; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('pve-fw-aliases', { + extend: 'Ext.data.Model', + + fields: [ 'name', 'cidr', 'comment', 'digest' ], + idProperty: 'name' +}); + +Ext.define('PVE.FirewallAliases', { + extend: 'Ext.grid.Panel', + alias: ['widget.pveFirewallAliases'], + + onlineHelp: 'pve_firewall_ip_aliases', + + stateful: true, + stateId: 'grid-firewall-aliases', + + base_url: undefined, + + title: gettext('Alias'), + + initComponent : function() { + + var me = this; + + if (!me.base_url) { + throw "missing base_url configuration"; + } + + var store = new Ext.data.Store({ + model: 'pve-fw-aliases', + proxy: { + type: 'proxmox', + url: "/api2/json" + me.base_url + }, + sorters: { + property: 'name', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + var oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('name', oldrec.data.name); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.FirewallAliasEdit', { + base_url: me.base_url, + alias_name: rec.data.name + }); + + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = Ext.create('Ext.Button', { + text: gettext('Add'), + handler: function() { + var win = Ext.create('PVE.FirewallAliasEdit', { + base_url: me.base_url + }); + win.on('destroy', reload); + win.show(); + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload + }); + + + Ext.apply(me, { + store: store, + tbar: [ me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: gettext('Name'), dataIndex: 'name', width: 100 }, + { header: gettext('IP/CIDR'), dataIndex: 'cidr', width: 100 }, + { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 } + ], + listeners: { + itemdblclick: run_editor + } + }); + + me.callParent(); + me.on('activate', reload); + } +}); +Ext.define('PVE.FirewallOptions', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveFirewallOptions'], + + fwtype: undefined, // 'dc', 'node' or 'vm' + + base_url: undefined, + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + if (!me.base_url) { + throw "missing base_url configuration"; + } + + if (me.fwtype === 'dc' || me.fwtype === 'node' || me.fwtype === 'vm') { + if (me.fwtype === 'node') { + me.cwidth1 = 250; + } + } else { + throw "unknown firewall option type"; + } + + me.rows = {}; + + var add_boolean_row = function(name, text, defaultValue) { + me.add_boolean_row(name, text, { defaultValue: defaultValue }); + }; + var add_integer_row = function(name, text, minValue, labelWidth) { + me.add_integer_row(name, text, { + minValue: minValue, + deleteEmpty: true, + labelWidth: labelWidth, + renderer: function(value) { + if (value === undefined) { + return Proxmox.Utils.defaultText; + } + + return value; + } + }); + }; + + var add_log_row = function(name, labelWidth) { + me.rows[name] = { + header: name, + required: true, + defaultValue: 'nolog', + editor: { + xtype: 'proxmoxWindowEdit', + subject: name, + fieldDefaults: { labelWidth: labelWidth || 100 }, + items: { + xtype: 'pveFirewallLogLevels', + name: name, + fieldLabel: name + } + } + }; + }; + + if (me.fwtype === 'node') { + me.rows.enable = { + required: true, + defaultValue: 1, + header: gettext('Firewall'), + renderer: Proxmox.Utils.format_boolean, + editor: { + xtype: 'pveFirewallEnableEdit', + defaultValue: 1 + } + }; + add_boolean_row('nosmurfs', gettext('SMURFS filter'), 1); + add_boolean_row('tcpflags', gettext('TCP flags filter'), 0); + add_boolean_row('ndp', 'NDP', 1); + add_integer_row('nf_conntrack_max', 'nf_conntrack_max', 32768, 120); + add_integer_row('nf_conntrack_tcp_timeout_established', + 'nf_conntrack_tcp_timeout_established', 7875, 250); + add_log_row('log_level_in'); + add_log_row('log_level_out'); + add_log_row('tcp_flags_log_level', 120); + add_log_row('smurf_log_level'); + } else if (me.fwtype === 'vm') { + me.rows.enable = { + required: true, + defaultValue: 0, + header: gettext('Firewall'), + renderer: Proxmox.Utils.format_boolean, + editor: { + xtype: 'pveFirewallEnableEdit', + defaultValue: 0 + } + }; + add_boolean_row('dhcp', 'DHCP', 1); + add_boolean_row('ndp', 'NDP', 1); + add_boolean_row('radv', gettext('Router Advertisement'), 0); + add_boolean_row('macfilter', gettext('MAC filter'), 1); + add_boolean_row('ipfilter', gettext('IP filter'), 0); + add_log_row('log_level_in'); + add_log_row('log_level_out'); + } else if (me.fwtype === 'dc') { + add_boolean_row('enable', gettext('Firewall'), 0); + add_boolean_row('ebtables', 'ebtables', 1); + me.rows.log_ratelimit = { + header: gettext('Log rate limit'), + required: true, + defaultValue: gettext('Default') + ' (enable=1,rate1/second,burst=5)', + editor: { + xtype: 'pveFirewallLograteEdit', + defaultValue: 'enable=1' + } + }; + } + + if (me.fwtype === 'dc' || me.fwtype === 'vm') { + me.rows.policy_in = { + header: gettext('Input Policy'), + required: true, + defaultValue: 'DROP', + editor: { + xtype: 'proxmoxWindowEdit', + subject: gettext('Input Policy'), + items: { + xtype: 'pveFirewallPolicySelector', + name: 'policy_in', + value: 'DROP', + fieldLabel: gettext('Input Policy') + } + } + }; + + me.rows.policy_out = { + header: gettext('Output Policy'), + required: true, + defaultValue: 'ACCEPT', + editor: { + xtype: 'proxmoxWindowEdit', + subject: gettext('Output Policy'), + items: { + xtype: 'pveFirewallPolicySelector', + name: 'policy_out', + value: 'ACCEPT', + fieldLabel: gettext('Output Policy') + } + } + }; + } + + var edit_btn = new Ext.Button({ + text: gettext('Edit'), + disabled: true, + handler: function() { me.run_editor(); } + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + var rowdef = me.rows[rec.data.key]; + edit_btn.setDisabled(!rowdef.editor); + }; + + Ext.apply(me, { + url: "/api2/json" + me.base_url, + tbar: [ edit_btn ], + editorConfig: { + url: '/api2/extjs/' + me.base_url + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + } +}); + + +Ext.define('PVE.FirewallLogLevels', { + extend: 'Proxmox.form.KVComboBox', + alias: ['widget.pveFirewallLogLevels'], + + name: 'log', + fieldLabel: gettext('Log level'), + value: 'nolog', + comboItems: [['nolog', 'nolog'], ['emerg', 'emerg'], ['alert', 'alert'], + ['crit', 'crit'], ['err', 'err'], ['warning', 'warning'], + ['notice', 'notice'], ['info', 'info'], ['debug', 'debug']] +}); +/* + * Left Treepanel, containing all the resources we manage in this datacenter: server nodes, server storages, VMs and Containers + */ +Ext.define('PVE.tree.ResourceTree', { + extend: 'Ext.tree.TreePanel', + alias: ['widget.pveResourceTree'], + + statics: { + typeDefaults: { + node: { + iconCls: 'fa fa-building', + text: gettext('Nodes') + }, + pool: { + iconCls: 'fa fa-tags', + text: gettext('Resource Pool') + }, + storage: { + iconCls: 'fa fa-database', + text: gettext('Storage') + }, + qemu: { + iconCls: 'fa fa-desktop', + text: gettext('Virtual Machine') + }, + lxc: { + //iconCls: 'x-tree-node-lxc', + iconCls: 'fa fa-cube', + text: gettext('LXC Container') + }, + template: { + iconCls: 'fa fa-file-o' + } + } + }, + + useArrows: true, + + // private + nodeSortFn: function(node1, node2) { + var n1 = node1.data; + var n2 = node2.data; + + if ((n1.groupbyid && n2.groupbyid) || + !(n1.groupbyid || n2.groupbyid)) { + + var tcmp; + + var v1 = n1.type; + var v2 = n2.type; + + if ((tcmp = v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)) != 0) { + return tcmp; + } + + // numeric compare for VM IDs + // sort templates after regular VMs + if (v1 === 'qemu' || v1 === 'lxc') { + if (n1.template && !n2.template) { + return 1; + } else if (n2.template && !n1.template) { + return -1; + } + v1 = n1.vmid; + v2 = n2.vmid; + if ((tcmp = v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)) != 0) { + return tcmp; + } + } + + return n1.id > n2.id ? 1 : (n1.id < n2.id ? -1 : 0); + } else if (n1.groupbyid) { + return -1; + } else if (n2.groupbyid) { + return 1; + } + }, + + // private: fast binary search + findInsertIndex: function(node, child, start, end) { + var me = this; + + var diff = end - start; + + var mid = start + (diff>>1); + + if (diff <= 0) { + return start; + } + + var res = me.nodeSortFn(child, node.childNodes[mid]); + if (res <= 0) { + return me.findInsertIndex(node, child, start, mid); + } else { + return me.findInsertIndex(node, child, mid + 1, end); + } + }, + + setIconCls: function(info) { + var me = this; + + var cls = PVE.Utils.get_object_icon_class(info.type, info); + + if (cls !== '') { + info.iconCls = cls; + } + }, + + // add additional elements to text + // at the moment only the usage indicator for storages + setText: function(info) { + var me = this; + + var status = ''; + if (info.type === 'storage') { + var maxdisk = info.maxdisk; + var disk = info.disk; + var usage = disk/maxdisk; + var cls = ''; + if (usage <= 1.0 && usage >= 0.0) { + var height = (usage*100).toFixed(0); + var neg_height = (100-usage*100).toFixed(0); + status = '
'; + status += '
'; + status += '
'; + status += '
'; + } + } + + info.text = status + info.text; + }, + + setToolTip: function(info) { + if (info.type === 'pool' || info.groupbyid !== undefined) { + return; + } + + var qtips = [gettext('Status') + ': ' + (info.qmpstatus || info.status)]; + if (info.lock) { + qtips.push('Config locked (' + info.lock + ')'); + } + if (info.hastate != 'unmanaged') { + qtips.push(gettext('HA State') + ": " + info.hastate); + } + + info.qtip = qtips.join(', '); + }, + + // private + addChildSorted: function(node, info) { + var me = this; + + me.setIconCls(info); + me.setText(info); + me.setToolTip(info); + + var defaults; + if (info.groupbyid) { + info.text = info.groupbyid; + if (info.type === 'type') { + defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid]; + if (defaults && defaults.text) { + info.text = defaults.text; + } + } + } + var child = Ext.create('PVETree', info); + + var cs = node.childNodes; + var pos; + if (cs) { + pos = cs[me.findInsertIndex(node, child, 0, cs.length)]; + } + + node.insertBefore(child, pos); + + return child; + }, + + // private + groupChild: function(node, info, groups, level) { + var me = this; + + var groupby = groups[level]; + var v = info[groupby]; + + if (v) { + var group = node.findChild('groupbyid', v); + if (!group) { + var groupinfo; + if (info.type === groupby) { + groupinfo = info; + } else { + groupinfo = { + type: groupby, + id : groupby + "/" + v + }; + if (groupby !== 'type') { + groupinfo[groupby] = v; + } + } + groupinfo.leaf = false; + groupinfo.groupbyid = v; + group = me.addChildSorted(node, groupinfo); + } + if (info.type === groupby) { + return group; + } + if (group) { + return me.groupChild(group, info, groups, level + 1); + } + } + + return me.addChildSorted(node, info); + }, + + initComponent : function() { + var me = this; + + var rstore = PVE.data.ResourceStore; + var sp = Ext.state.Manager.getProvider(); + + if (!me.viewFilter) { + me.viewFilter = {}; + } + + var pdata = { + dataIndex: {}, + updateCount: 0 + }; + + var store = Ext.create('Ext.data.TreeStore', { + model: 'PVETree', + root: { + expanded: true, + id: 'root', + text: gettext('Datacenter'), + iconCls: 'fa fa-server' + } + }); + + var stateid = 'rid'; + + var updateTree = function() { + var tmp; + + store.suspendEvents(); + + var rootnode = me.store.getRootNode(); + // remember selected node (and all parents) + var sm = me.getSelectionModel(); + + var lastsel = sm.getSelection()[0]; + var reselect = false; + var parents = []; + var p = lastsel; + while (p && !!(p = p.parentNode)) { + parents.push(p); + } + + var index = pdata.dataIndex; + + var groups = me.viewFilter.groups || []; + var filterfn = me.viewFilter.filterfn; + + // remove vanished or moved items + // update in place changed items + var key; + for (key in index) { + if (index.hasOwnProperty(key)) { + var olditem = index[key]; + + // getById() use find(), which is slow (ExtJS4 DP5) + //var item = rstore.getById(olditem.data.id); + var item = rstore.data.get(olditem.data.id); + + var changed = false; + var moved = false; + if (item) { + // test if any grouping attributes changed + // this will also catch migrated nodes + // in server view + var i, len; + for (i = 0, len = groups.length; i < len; i++) { + var attr = groups[i]; + if (item.data[attr] != olditem.data[attr]) { + //console.log("changed " + attr); + moved = true; + break; + } + } + + // explicitly check for node, since + // in some views, node is not a grouping + // attribute + if (!moved && item.data.node !== olditem.data.node) { + moved = true; + } + + // tree item has been updated + var fields = [ + 'text', 'running', 'template', 'status', + 'qmpstatus', 'hastate', 'lock' + ]; + + var field; + for (i = 0; i < fields.length; i++) { + field = fields[i]; + if (item.data[field] !== olditem.data[field]) { + changed = true; + break; + } + } + + // fixme: also test filterfn()? + } + + if (changed) { + olditem.beginEdit(); + //console.log("REM UPDATE UID: " + key + " ITEM " + item.data.running); + var info = olditem.data; + Ext.apply(info, item.data); + me.setIconCls(info); + me.setText(info); + me.setToolTip(info); + olditem.commit(); + } + if ((!item || moved) && olditem.isLeaf()) { + //console.log("REM UID: " + key + " ITEM " + olditem.data.id); + delete index[key]; + var parentNode = olditem.parentNode; + // when the selected item disappears, + // we have to deselect it here, and reselect it + // later + if (lastsel && olditem.data.id === lastsel.data.id) { + reselect = true; + sm.deselect(olditem); + } + // since the store events are suspended, we + // manually remove the item from the store also + store.remove(olditem); + parentNode.removeChild(olditem, true); + } + } + } + + // add new items + rstore.each(function(item) { + var olditem = index[item.data.id]; + if (olditem) { + return; + } + + if (filterfn && !filterfn(item)) { + return; + } + + //console.log("ADD UID: " + item.data.id); + + var info = Ext.apply({ leaf: true }, item.data); + + var child = me.groupChild(rootnode, info, groups, 0); + if (child) { + index[item.data.id] = child; + } + }); + + store.resumeEvents(); + store.fireEvent('refresh', store); + + // select parent node is selection vanished + if (lastsel && !rootnode.findChild('id', lastsel.data.id, true)) { + lastsel = rootnode; + while (!!(p = parents.shift())) { + if (!!(tmp = rootnode.findChild('id', p.data.id, true))) { + lastsel = tmp; + break; + } + } + me.selectById(lastsel.data.id); + } else if (lastsel && reselect) { + me.selectById(lastsel.data.id); + } + + // on first tree load set the selection from the stateful provider + if (!pdata.updateCount) { + rootnode.expand(); + me.applyState(sp.get(stateid)); + } + + pdata.updateCount++; + }; + + var statechange = function(sp, key, value) { + if (key === stateid) { + me.applyState(value); + } + }; + + sp.on('statechange', statechange); + + Ext.apply(me, { + allowSelection: true, + store: store, + viewConfig: { + // note: animate cause problems with applyState + animate: false + }, + //useArrows: true, + //rootVisible: false, + //title: 'Resource Tree', + listeners: { + itemcontextmenu: PVE.Utils.createCmdMenu, + destroy: function() { + rstore.un("load", updateTree); + }, + beforecellmousedown: function (tree, td, cellIndex, record, tr, rowIndex, ev) { + var sm = me.getSelectionModel(); + // disable selection when right clicking + // except the record is already selected + me.allowSelection = (ev.button !== 2) || sm.isSelected(record); + }, + beforeselect: function (tree, record, index, eopts) { + var allow = me.allowSelection; + me.allowSelection = true; + return allow; + }, + itemdblclick: PVE.Utils.openTreeConsole + }, + setViewFilter: function(view) { + me.viewFilter = view; + me.clearTree(); + updateTree(); + }, + setDatacenterText: function(clustername) { + var rootnode = me.store.getRootNode(); + + var rnodeText = gettext('Datacenter'); + if (clustername !== undefined) { + rnodeText += ' (' + clustername + ')'; + } + + rootnode.beginEdit(); + rootnode.data.text = rnodeText; + rootnode.commit(); + }, + clearTree: function() { + pdata.updateCount = 0; + var rootnode = me.store.getRootNode(); + rootnode.collapse(); + rootnode.removeAll(); + pdata.dataIndex = {}; + me.getSelectionModel().deselectAll(); + }, + selectExpand: function(node) { + var sm = me.getSelectionModel(); + if (!sm.isSelected(node)) { + sm.select(node); + var cn = node; + while (!!(cn = cn.parentNode)) { + if (!cn.isExpanded()) { + cn.expand(); + } + } + me.getView().focusRow(node); + } + }, + selectById: function(nodeid) { + var rootnode = me.store.getRootNode(); + var sm = me.getSelectionModel(); + var node; + if (nodeid === 'root') { + node = rootnode; + } else { + node = rootnode.findChild('id', nodeid, true); + } + if (node) { + me.selectExpand(node); + } + return node; + }, + applyState : function(state) { + var sm = me.getSelectionModel(); + if (state && state.value) { + me.selectById(state.value); + } else { + sm.deselectAll(); + } + } + }); + + me.callParent(); + + var sm = me.getSelectionModel(); + sm.on('select', function(sm, n) { + sp.set(stateid, { value: n.data.id}); + }); + + rstore.on("load", updateTree); + rstore.startUpdate(); + //rstore.stopUpdate(); + } + +}); +Ext.define('pve-fw-ipsets', { + extend: 'Ext.data.Model', + fields: [ 'name', 'comment', 'digest' ], + idProperty: 'name' +}); + +Ext.define('PVE.IPSetList', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveIPSetList', + + stateful: true, + stateId: 'grid-firewall-ipsetlist', + + ipset_panel: undefined, + + base_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + initComponent: function() { + + var me = this; + + if (me.ipset_panel == undefined) { + throw "no rule panel specified"; + } + + if (me.base_url == undefined) { + throw "no base_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-fw-ipsets', + proxy: { + type: 'proxmox', + url: "/api2/json" + me.base_url + }, + sorters: { + property: 'name', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + var oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('name', oldrec.data.name); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('Proxmox.window.Edit', { + subject: "IPSet '" + rec.data.name + "'", + url: me.base_url, + method: 'POST', + digest: rec.data.digest, + items: [ + { + xtype: 'hiddenfield', + name: 'rename', + value: rec.data.name + }, + { + xtype: 'textfield', + name: 'name', + value: rec.data.name, + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: rec.data.comment, + fieldLabel: gettext('Comment') + } + ] + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Create'), + handler: function() { + sm.deselectAll(); + var win = Ext.create('Proxmox.window.Edit', { + subject: 'IPSet', + url: me.base_url, + method: 'POST', + items: [ + { + xtype: 'textfield', + name: 'name', + value: '', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + win.show(); + win.on('destroy', reload); + + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload + }); + + Ext.apply(me, { + store: store, + tbar: [ 'IPSet:', me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: 'IPSet', dataIndex: 'name', width: '100' }, + { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 } + ], + listeners: { + itemdblclick: run_editor, + select: function(sm, rec) { + var url = me.base_url + '/' + rec.data.name; + me.ipset_panel.setBaseUrl(url); + }, + deselect: function() { + me.ipset_panel.setBaseUrl(undefined); + }, + show: reload + } + }); + + me.callParent(); + + store.load(); + } +}); + +Ext.define('PVE.IPSetCidrEdit', { + extend: 'Proxmox.window.Edit', + + cidr: undefined, + + initComponent : function() { + + var me = this; + + me.isCreate = (me.cidr === undefined); + + + if (me.isCreate) { + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + } else { + me.url = '/api2/extjs' + me.base_url + '/' + me.cidr; + me.method = 'PUT'; + } + + var column1 = []; + + if (me.isCreate) { + if (!me.list_refs_url) { + throw "no alias_base_url specified"; + } + + column1.push({ + xtype: 'pveIPRefSelector', + name: 'cidr', + ref_type: 'alias', + autoSelect: false, + editable: true, + base_url: me.list_refs_url, + value: '', + fieldLabel: gettext('IP/CIDR') + }); + } else { + column1.push({ + xtype: 'displayfield', + name: 'cidr', + value: '', + fieldLabel: gettext('IP/CIDR') + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + isCreate: me.isCreate, + column1: column1, + column2: [ + { + xtype: 'proxmoxcheckbox', + name: 'nomatch', + checked: false, + uncheckedValue: 0, + fieldLabel: 'nomatch' + } + ], + columnB: [ + { + xtype: 'textfield', + name: 'comment', + value: '', + fieldLabel: gettext('Comment') + } + ] + }); + + Ext.apply(me, { + subject: gettext('IP/CIDR'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + ipanel.setValues(values); + } + }); + } + } +}); + +Ext.define('PVE.IPSetGrid', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveIPSetGrid', + + stateful: true, + stateId: 'grid-firewall-ipsets', + + base_url: undefined, + list_refs_url: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + setBaseUrl: function(url) { + var me = this; + + me.base_url = url; + + if (url === undefined) { + me.addBtn.setDisabled(true); + me.store.removeAll(); + } else { + me.addBtn.setDisabled(false); + me.removeBtn.baseurl = url + '/'; + me.store.setProxy({ + type: 'proxmox', + url: '/api2/json' + url + }); + + me.store.load(); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.list_refs_url) { + throw "no1 list_refs_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-ipset' + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('PVE.IPSetCidrEdit', { + base_url: me.base_url, + cidr: rec.data.cidr + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Add'), + disabled: true, + handler: function() { + if (!me.base_url) { + return; + } + var win = Ext.create('PVE.IPSetCidrEdit', { + base_url: me.base_url, + list_refs_url: me.list_refs_url + }); + win.show(); + win.on('destroy', reload); + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + callback: reload + }); + + var render_errors = function(value, metaData, record) { + var errors = record.data.errors; + if (errors) { + var msg = errors.cidr || errors.nomatch; + if (msg) { + metaData.tdCls = 'proxmox-invalid-row'; + var html = '

' + Ext.htmlEncode(msg) + '

'; + metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + + html.replace(/\"/g,'"') + '"'; + } + } + return value; + }; + + Ext.apply(me, { + tbar: [ 'IP/CIDR:', me.addBtn, me.removeBtn, me.editBtn ], + store: store, + selModel: sm, + listeners: { + itemdblclick: run_editor + }, + columns: [ + { + xtype: 'rownumberer' + }, + { + header: gettext('IP/CIDR'), + dataIndex: 'cidr', + width: 150, + renderer: function(value, metaData, record) { + value = render_errors(value, metaData, record); + if (record.data.nomatch) { + return '! ' + value; + } + return value; + } + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + flex: 1, + renderer: function(value) { + return Ext.util.Format.htmlEncode(value); + } + } + ] + }); + + me.callParent(); + + if (me.base_url) { + me.setBaseUrl(me.base_url); // load + } + } +}, function() { + + Ext.define('pve-ipset', { + extend: 'Ext.data.Model', + fields: [ { name: 'nomatch', type: 'boolean' }, + 'cidr', 'comment', 'errors' ], + idProperty: 'cidr' + }); + +}); + +Ext.define('PVE.IPSet', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveIPSet', + + title: 'IPSet', + + onlineHelp: 'pve_firewall_ip_sets', + + list_refs_url: undefined, + + initComponent: function() { + var me = this; + + if (!me.list_refs_url) { + throw "no list_refs_url specified"; + } + + var ipset_panel = Ext.createWidget('pveIPSetGrid', { + region: 'center', + list_refs_url: me.list_refs_url, + border: false + }); + + var ipset_list = Ext.createWidget('pveIPSetList', { + region: 'west', + ipset_panel: ipset_panel, + base_url: me.base_url, + width: '50%', + border: false, + split: true + }); + + Ext.apply(me, { + layout: 'border', + items: [ ipset_list, ipset_panel ], + listeners: { + show: function() { + ipset_list.fireEvent('show', ipset_list); + } + } + }); + + me.callParent(); + } +}); +/* + * Base class for all the multitab config panels + * + * How to use this: + * + * You create a subclass of this, and then define your wanted tabs + * as items like this: + * + * items: [{ + * title: "myTitle", + * xytpe: "somextype", + * iconCls: 'fa fa-icon', + * groups: ['somegroup'], + * expandedOnInit: true, + * itemId: 'someId' + * }] + * + * this has to be in the declarative syntax, else we + * cannot save them for later + * (so no Ext.create or Ext.apply of an item in the subclass) + * + * the groups array expects the itemids of the items + * which are the parents, which have to come before they + * are used + * + * if you want following the tree: + * + * Option1 + * Option2 + * -> SubOption1 + * -> SubSubOption1 + * + * the suboption1 group array has to look like this: + * groups: ['itemid-of-option2'] + * + * and of subsuboption1: + * groups: ['itemid-of-option2', 'itemid-of-suboption1'] + * + * setting the expandedOnInit determines if the item/group is expanded + * initially (false by default) + */ +Ext.define('PVE.panel.Config', { + extend: 'Ext.panel.Panel', + alias: 'widget.pvePanelConfig', + + showSearch: true, // add a resource grid with a search button as first tab + viewFilter: undefined, // a filter to pass to that resource grid + + tbarSpacing: true, // if true, adds a spacer after the title in tbar + + dockedItems: [{ + // this is needed for the overflow handler + xtype: 'toolbar', + overflowHandler: 'scroller', + dock: 'left', + style: { + backgroundColor: '#f5f5f5', + padding: 0, + margin: 0 + }, + items: { + xtype: 'treelist', + itemId: 'menu', + ui: 'nav', + expanderOnly: true, + expanderFirst: false, + animation: false, + singleExpand: false, + listeners: { + selectionchange: function(treeList, selection) { + var me = this.up('panel'); + me.suspendLayout = true; + me.activateCard(selection.data.id); + me.suspendLayout = false; + me.updateLayout(); + }, + itemclick: function(treelist, info) { + var olditem = treelist.getSelection(); + var newitem = info.node; + + // when clicking on the expand arrow, + // we don't select items, but still want + // the original behaviour + if (info.select === false) { + return; + } + + // if you click on a different item which is open, + // leave it open + // else toggle the clicked item + if (olditem.data.id !== newitem.data.id && + newitem.data.expanded === true) { + info.toggle = false; + } else { + info.toggle = true; + } + } + } + } + }, + { + xtype: 'toolbar', + itemId: 'toolbar', + dock: 'top', + height: 36, + overflowHandler: 'scroller' + }], + + firstItem: '', + layout: 'card', + border: 0, + + // used for automated test + selectById: function(cardid) { + var me = this; + + var root = me.store.getRoot(); + var selection = root.findChild('id', cardid, true); + + if (selection) { + selection.expand(); + var menu = me.down('#menu'); + menu.setSelection(selection); + return cardid; + } + }, + + activateCard: function(cardid) { + var me = this; + if (me.savedItems[cardid]) { + var curcard = me.getLayout().getActiveItem(); + var newcard = me.add(me.savedItems[cardid]); + me.helpButton.setOnlineHelp(newcard.onlineHelp || me.onlineHelp); + if (curcard) { + me.setActiveItem(cardid); + me.remove(curcard, true); + + // trigger state change + + var ncard = cardid; + // Note: '' is alias for first tab. + // First tab can be 'search' or something else + if (cardid === me.firstItem) { + ncard = ''; + } + if (me.hstateid) { + me.sp.set(me.hstateid, { value: ncard }); + } + } + } + }, + + initComponent: function() { + var me = this; + + var stateid = me.hstateid; + + me.sp = Ext.state.Manager.getProvider(); + + var activeTab; // leaving this undefined means items[0] will be the default tab + + if (stateid) { + var state = me.sp.get(stateid); + if (state && state.value) { + // if this tab does not exists, it chooses the first + activeTab = state.value; + } + } + + // get title + var title = me.title || me.pveSelNode.data.text; + me.title = undefined; + + // create toolbar + var tbar = me.tbar || []; + me.tbar = undefined; + + if (!me.onlineHelp) { + switch(me.pveSelNode.data.id) { + case 'type/storage':me.onlineHelp = 'chapter-pvesm.html'; break; + case 'type/qemu':me.onlineHelp = 'chapter-qm.html'; break; + case 'type/lxc':me.onlineHelp = 'chapter-pct.html'; break; + case 'type/pool':me.onlineHelp = 'chapter-pveum.html#_pools'; break; + case 'type/node':me.onlineHelp = 'chapter-sysadmin.html'; break; + } + } + + if (me.tbarSpacing) { + tbar.unshift('->'); + } + tbar.unshift({ + xtype: 'tbtext', + text: title, + baseCls: 'x-panel-header-text' + }); + + me.helpButton = Ext.create('Proxmox.button.Help', { + hidden: false, + listenToGlobalEvent: false, + onlineHelp: me.onlineHelp || undefined + }); + + tbar.push(me.helpButton); + + me.dockedItems[1].items = tbar; + + // include search tab + me.items = me.items || []; + if (me.showSearch) { + me.items.unshift({ + itemId: 'search', + title: gettext('Search'), + iconCls: 'fa fa-search', + xtype: 'pveResourceGrid', + pveSelNode: me.pveSelNode + }); + } + + me.savedItems = {}; + /*jslint confusion:true*/ + if (me.items[0]) { + me.firstItem = me.items[0].itemId; + } + /*jslint confusion:false*/ + + me.store = Ext.create('Ext.data.TreeStore', { + root: { + expanded: true + } + }); + var root = me.store.getRoot(); + me.items.forEach(function(item){ + var treeitem = Ext.create('Ext.data.TreeModel',{ + id: item.itemId, + text: item.title, + iconCls: item.iconCls, + leaf: true, + expanded: item.expandedOnInit + }); + item.header = false; + if (me.savedItems[item.itemId] !== undefined) { + throw "itemId already exists, please use another"; + } + me.savedItems[item.itemId] = item; + + var group; + var curnode = root; + + // get/create the group items + while (Ext.isArray(item.groups) && item.groups.length > 0) { + group = item.groups.shift(); + + var child = curnode.findChild('id', group); + if (child === null) { + // did not find the group item + // so add it where we are + break; + } + curnode = child; + } + + // insert the item + + // lets see if it already exists + var node = curnode.findChild('id', item.itemId); + + if (node === null) { + curnode.appendChild(treeitem); + } else { + // should not happen! + throw "id already exists"; + } + }); + + delete me.items; + me.defaults = me.defaults || {}; + Ext.apply(me.defaults, { + pveSelNode: me.pveSelNode, + viewFilter: me.viewFilter, + workspace: me.workspace, + border: 0 + }); + + me.callParent(); + + var menu = me.down('#menu'); + var selection = root.findChild('id', activeTab, true) || root.firstChild; + var node = selection; + while (node !== root) { + node.expand(); + node = node.parentNode; + } + menu.setStore(me.store); + menu.setSelection(selection); + + // on a state change, + // select the new item + var statechange = function(sp, key, state) { + // it the state change is for this panel + if (stateid && (key === stateid) && state) { + // get active item + var acard = me.getLayout().getActiveItem().itemId; + // get the itemid of the new value + var ncard = state.value || me.firstItem; + if (ncard && (acard != ncard)) { + // select the chosen item + menu.setSelection(root.findChild('id', ncard, true) || root.firstChild); + } + } + }; + + if (stateid) { + me.mon(me.sp, 'statechange', statechange); + } + } +}); +Ext.define('PVE.grid.BackupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveBackupView'], + + onlineHelp: 'chapter_vzdump', + + stateful: true, + stateId: 'grid-guest-backup', + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var vmtype = me.pveSelNode.data.type; + if (!vmtype) { + throw "no VM type specified"; + } + + var vmtypeFilter; + if (vmtype === 'openvz') { + vmtypeFilter = function(item) { + return item.data.volid.match(':backup/vzdump-openvz-'); + }; + } else if (vmtype === 'lxc') { + vmtypeFilter = function(item) { + return item.data.volid.match(':backup/vzdump-lxc-'); + }; + } else if (vmtype === 'qemu') { + vmtypeFilter = function(item) { + return item.data.volid.match(':backup/vzdump-qemu-'); + }; + } else { + throw "unsupported VM type '" + vmtype + "'"; + } + + var searchFilter = { + property: 'volid', + // on initial store display only our vmid backups + // surround with minus sign to prevent the 2016 VMID bug + value: vmtype + '-' + vmid + '-', + anyMatch: true, + caseSensitive: false + }; + + me.store = Ext.create('Ext.data.Store', { + model: 'pve-storage-content', + sorters: { + property: 'volid', + order: 'DESC' + }, + filters: [ + vmtypeFilter, + searchFilter + ] + }); + + var reload = Ext.Function.createBuffered(function() { + if (me.store) { + me.store.load(); + } + }, 100); + + var setStorage = function(storage) { + var url = '/api2/json/nodes/' + nodename + '/storage/' + storage + '/content'; + url += '?content=backup'; + + me.store.setProxy({ + type: 'proxmox', + url: url + }); + + reload(); + }; + + var storagesel = Ext.create('PVE.form.StorageSelector', { + nodename: nodename, + fieldLabel: gettext('Storage'), + labelAlign: 'right', + storageContent: 'backup', + allowBlank: false, + listeners: { + change: function(f, value) { + setStorage(value); + } + } + }); + + var storagefilter = Ext.create('Ext.form.field.Text', { + fieldLabel: gettext('Search'), + labelWidth: 50, + labelAlign: 'right', + enableKeyEvents: true, + value: searchFilter.value, + listeners: { + buffer: 500, + keyup: function(field) { + me.store.clearFilter(true); + searchFilter.value = field.getValue(); + me.store.filter([ + vmtypeFilter, + searchFilter + ]); + } + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var backup_btn = Ext.create('Ext.button.Button', { + text: gettext('Backup now'), + handler: function() { + var win = Ext.create('PVE.window.Backup', { + nodename: nodename, + vmid: vmid, + vmtype: vmtype, + storage: storagesel.getValue(), + listeners : { + close: function() { + reload(); + } + } + }); + win.show(); + } + }); + + var restore_btn = Ext.create('Proxmox.button.Button', { + text: gettext('Restore'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return !!rec; + }, + handler: function(b, e, rec) { + var volid = rec.data.volid; + + var win = Ext.create('PVE.window.Restore', { + nodename: nodename, + vmid: vmid, + volid: rec.data.volid, + volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec), + vmtype: vmtype + }); + win.show(); + win.on('destroy', reload); + } + }); + + var delete_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + dangerous: true, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.volid + "'"); + msg += " " + gettext('This will permanently erase all data.'); + + return msg; + }, + getUrl: function(rec) { + var storage = storagesel.getValue(); + return '/nodes/' + nodename + '/storage/' + storage + '/content/' + rec.data.volid; + }, + callback: function() { + reload(); + } + }); + + var config_btn = Ext.create('Proxmox.button.Button', { + text: gettext('Show Configuration'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return !!rec; + }, + handler: function(b, e, rec) { + var storage = storagesel.getValue(); + if (!storage) { + return; + } + + var win = Ext.create('PVE.window.BackupConfig', { + volume: rec.data.volid, + pveSelNode: me.pveSelNode + }); + + win.show(); + } + }); + + Ext.apply(me, { + selModel: sm, + tbar: [ backup_btn, restore_btn, delete_btn,config_btn, '->', storagesel, storagefilter ], + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + renderer: PVE.Utils.render_storage_content, + dataIndex: 'volid' + }, + { + header: gettext('Format'), + width: 100, + dataIndex: 'format' + }, + { + header: gettext('Size'), + width: 100, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + } + ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.CephCreateService', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCephCreateService', + + showProgress: true, + + setNode: function(nodename) { + var me = this; + + me.nodename = nodename; + me.url = "/nodes/" + nodename + "/ceph/" + me.type + "/" + nodename; + }, + + method: 'POST', + isCreate: true, + + items: [ + { + xtype: 'pveNodeSelector', + submitValue: false, + fieldLabel: gettext('Host'), + selectCurNode: true, + allowBlank: false, + listeners: { + change: function(f, value) { + var me = this.up('pveCephCreateService'); + me.setNode(value); + } + } + } + ], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.type) { + throw "no type specified"; + } + + me.setNode(me.nodename); + + me.callParent(); + } +}); + +Ext.define('PVE.node.CephServiceList', { + extend: 'Ext.grid.GridPanel', + xtype: 'pveNodeCephServiceList', + + onlineHelp: 'chapter_pveceph', + emptyText: gettext('No such service configured.'), + + stateful: true, + + // will be called when the store loads + storeLoadCallback: Ext.emptyFn, + + // if set to true, does shows the ceph install mask if needed + showCephInstallMask: false, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + if (view.pveSelNode) { + view.nodename = view.pveSelNode.data.node; + } + if (!view.nodename) { + throw "no node name specified"; + } + + if (!view.type) { + throw "no type specified"; + } + + view.rstore = Ext.create('Proxmox.data.UpdateStore', { + autoLoad: true, + autoStart: true, + interval: 3000, + storeid: 'ceph-' + view.type + '-list' + view.nodename, + model: 'ceph-service-list', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + view.nodename + "/ceph/" + view.type + } + }); + + view.setStore(Ext.create('Proxmox.data.DiffStore', { + rstore: view.rstore, + sorters: [{ property: 'name' }] + })); + + if (view.storeLoadCallback) { + view.rstore.on('load', view.storeLoadCallback, this); + } + view.on('destroy', view.rstore.stopUpdate); + + if (view.showCephInstallMask) { + var regex = new RegExp("not (installed|initialized)", "i"); + PVE.Utils.handleStoreErrorOrMask(view, view.rstore, regex, function(me, error) { + view.rstore.stopUpdate(); + PVE.Utils.showCephInstallOrMask(view.ownerCt, error.statusText, view.nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + view.rstore.startUpdate(); + }); + } + ); + }); + } + }, + + service_cmd: function(rec, cmd) { + var view = this.getView(); + if (!rec.data.host) { + Ext.Msg.alert(gettext('Error'), "entry has no host"); + return; + } + Proxmox.Utils.API2Request({ + url: "/nodes/" + rec.data.host + "/ceph/" + cmd, + method: 'POST', + params: { service: view.type + '.' + rec.data.name }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + taskDone: function() { + view.rstore.load(); + } + }); + win.show(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + onChangeService: function(btn) { + var me = this; + var view = this.getView(); + var cmd = btn.action; + var rec = view.getSelection()[0]; + me.service_cmd(rec, cmd); + }, + + showSyslog: function() { + var view = this.getView(); + var rec = view.getSelection()[0]; + var servicename = 'ceph-' + view.type + '@' + rec.data.name; + var url = "/api2/extjs/nodes/" + rec.data.host + "/syslog?service=" + encodeURIComponent(servicename); + var win = Ext.create('Ext.window.Window', { + title: gettext('Syslog') + ': ' + servicename, + modal: true, + width: 800, + height: 400, + layout: 'fit', + items: [{ + xtype: 'proxmoxLogView', + url: url, + log_select_timespan: 1 + }] + }); + win.show(); + }, + + onCreate: function() { + var view = this.getView(); + var win = Ext.create('PVE.CephCreateService', { + autoShow: true, + nodename: view.nodename, + subject: view.getTitle(), + type: view.type, + taskDone: function() { + view.rstore.load(); + } + }); + } + }, + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Start'), + iconCls: 'fa fa-play', + action: 'start', + disabled: true, + enableFn: function(rec) { + return rec.data.state === 'stopped' || + rec.data.state === 'unknown'; + }, + handler: 'onChangeService' + }, + { + xtype: 'proxmoxButton', + text: gettext('Stop'), + iconCls: 'fa fa-stop', + action: 'stop', + enableFn: function(rec) { + return rec.data.state !== 'stopped'; + }, + disabled: true, + handler: 'onChangeService' + }, + { + xtype: 'proxmoxButton', + text: gettext('Restart'), + iconCls: 'fa fa-refresh', + action: 'restart', + disabled: true, + enableFn: function(rec) { + return rec.data.state !== 'stopped'; + }, + handler: 'onChangeService' + }, + '-', + { + text: gettext('Create'), + reference: 'createButton', + handler: 'onCreate' + }, + { + text: gettext('Destroy'), + xtype: 'proxmoxStdRemoveButton', + getUrl: function(rec) { + var view = this.up('grid'); + if (!rec.data.host) { + Ext.Msg.alert(gettext('Error'), "entry has no host"); + return; + } + return "/nodes/" + rec.data.host + "/ceph/" + view.type + "/" + rec.data.name; + }, + callback: function(options, success, response) { + var view = this.up('grid'); + if (!success) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + return; + } + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + taskDone: function() { + view.rstore.load(); + } + }); + win.show(); + } + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Syslog'), + disabled: true, + handler: 'showSyslog' + } + ], + + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + renderer: function(v) { + return this.type + '.' + v; + }, + dataIndex: 'name' + }, + { + header: gettext('Host'), + flex: 1, + sortable: true, + renderer: function(v) { + return v || Proxmox.Utils.unknownText; + }, + dataIndex: 'host' + }, + { + header: gettext('Status'), + flex: 1, + sortable: false, + dataIndex: 'state' + }, + { + header: gettext('Address'), + flex: 3, + sortable: true, + renderer: function(v) { + return v || Proxmox.Utils.unknownText; + }, + dataIndex: 'addr' + }, + { + header: gettext('Version'), + flex: 3, + sortable: true, + dataIndex: 'version' + } + ], + + initComponent: function() { + var me = this; + + if (me.additionalColumns) { + me.columns = me.columns.concat(me.additionalColumns); + } + + me.callParent(); + } + +}, function() { + + Ext.define('ceph-service-list', { + extend: 'Ext.data.Model', + fields: [ 'addr', 'name', 'rank', 'host', 'quorum', 'state', + 'ceph_version', 'ceph_version_short', + { type: 'string', name: 'version', calculate: function(data) { + return PVE.Utils.parse_ceph_version(data); + } } + ], + idProperty: 'name' + }); +}); +/*jslint confusion: true */ +Ext.define('PVE.CephCreateFS', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveCephCreateFS', + + showTaskViewer: true, + onlineHelp: 'pveceph_fs_create', + + subject: 'Ceph FS', + isCreate: true, + method: 'POST', + + setFSName: function(fsName) { + var me = this; + + if (fsName === '' || fsName === undefined) { + fsName = 'cephfs'; + } + + me.url = "/nodes/" + me.nodename + "/ceph/fs/" + fsName; + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Name'), + name: 'name', + value: 'cephfs', + listeners: { + change: function(f, value) { + this.up('pveCephCreateFS').setFSName(value); + } + }, + submitValue: false, // already encoded in apicall URL + emptyText: 'cephfs' + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: 'Placement Groups', + name: 'pg_num', + value: 128, + emptyText: 128, + minValue: 8, + maxValue: 32768, + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Add as Storage'), + value: true, + name: 'add-storage', + autoEl: { + tag: 'div', + 'data-qtip': gettext('Add the new CephFS to the cluster storage configuration.'), + }, + } + ], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + me.setFSName(); + + me.callParent(); + } +}); + +Ext.define('PVE.NodeCephFSPanel', { + extend: 'Ext.panel.Panel', + xtype: 'pveNodeCephFSPanel', + mixins: ['Proxmox.Mixin.CBind'], + + title: gettext('CephFS'), + onlineHelp: 'pveceph_fs', + + border: false, + defaults: { + border: false, + cbind: { + nodename: '{nodename}' + } + }, + + viewModel: { + parent: null, + data: { + cephfsConfigured: false, + mdsCount: 0 + }, + formulas: { + canCreateFS: function(get) { + return (!get('cephfsConfigured') && get('mdsCount') > 0); + } + } + }, + + items: [ + { + xtype: 'grid', + emptyText: Ext.String.format(gettext('No {0} configured.'), 'CephFS'), + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.rstore = Ext.create('Proxmox.data.UpdateStore', { + autoLoad: true, + xtype: 'update', + interval: 5 * 1000, + autoStart: true, + storeid: 'pve-ceph-fs', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + view.nodename + '/ceph/fs' + }, + model: 'pve-ceph-fs' + }); + view.setStore(Ext.create('Proxmox.data.DiffStore', { + rstore: view.rstore, + sorters: { + property: 'name', + order: 'DESC' + } + })); + var regex = new RegExp("not (installed|initialized)", "i"); + PVE.Utils.handleStoreErrorOrMask(view, view.rstore, regex, function(me, error){ + me.rstore.stopUpdate(); + PVE.Utils.showCephInstallOrMask(me.ownerCt, error.statusText, view.nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.rstore.startUpdate(); + }); + } + ); + }); + view.rstore.on('load', this.onLoad, this); + view.on('destroy', view.rstore.stopUpdate); + }, + + onCreate: function() { + var view = this.getView(); + view.rstore.stopUpdate(); + var win = Ext.create('PVE.CephCreateFS', { + autoShow: true, + nodename: view.nodename, + listeners: { + destroy: function() { + view.rstore.startUpdate(); + } + } + }); + }, + + onLoad: function(store, records, success) { + var vm = this.getViewModel(); + if (!(success && records && records.length > 0)) { + vm.set('cephfsConfigured', false); + return; + } + vm.set('cephfsConfigured', true); + } + }, + tbar: [ + { + text: gettext('Create CephFS'), + reference: 'createButton', + handler: 'onCreate', + bind: { + // only one CephFS per Ceph cluster makes sense for now + disabled: '{!canCreateFS}' + } + } + ], + columns: [ + { + header: gettext('Name'), + flex: 1, + dataIndex: 'name' + }, + { + header: 'Data Pool', + flex: 1, + dataIndex: 'data_pool' + }, + { + header: 'Metadata Pool', + flex: 1, + dataIndex: 'metadata_pool' + } + ], + cbind: { + nodename: '{nodename}' + } + }, + { + xtype: 'pveNodeCephServiceList', + title: gettext('Metadata Servers'), + stateId: 'grid-ceph-mds', + type: 'mds', + storeLoadCallback: function(store, records, success) { + var vm = this.getViewModel(); + if (!success || !records) { + vm.set('mdsCount', 0); + return; + } + vm.set('mdsCount', records.length); + }, + cbind: { + nodename: '{nodename}' + } + } + ] +}, function() { + Ext.define('pve-ceph-fs', { + extend: 'Ext.data.Model', + fields: [ 'name', 'data_pool', 'metadata_pool' ], + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/localhost/ceph/fs" + }, + idProperty: 'name' + }); +}); +Ext.define('PVE.CephCreatePool', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveCephCreatePool', + + showProgress: true, + onlineHelp: 'pve_ceph_pools', + + subject: 'Ceph Pool', + isCreate: true, + method: 'POST', + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Name'), + name: 'name', + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Size'), + name: 'size', + value: 3, + minValue: 1, + maxValue: 7, + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Min. Size'), + name: 'min_size', + value: 2, + minValue: 1, + maxValue: 7, + allowBlank: false + }, + { + xtype: 'pveCephRuleSelector', + fieldLabel: 'Crush Rule', // do not localize + name: 'crush_rule', + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: 'pg_num', + name: 'pg_num', + value: 128, + minValue: 8, + maxValue: 32768, + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Add as Storage'), + value: true, + name: 'add_storages', + autoEl: { + tag: 'div', + 'data-qtip': gettext('Add the new pool to the cluster storage configuration.'), + }, + } + ], + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: "/nodes/" + me.nodename + "/ceph/pools", + defaults: { + nodename: me.nodename + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.CephPoolList', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveNodeCephPoolList', + + onlineHelp: 'chapter_pveceph', + + stateful: true, + stateId: 'grid-ceph-pools', + bufferedRenderer: false, + + features: [ { ftype: 'summary'} ], + + columns: [ + { + header: gettext('Name'), + width: 120, + sortable: true, + dataIndex: 'pool_name' + }, + { + header: gettext('Size') + '/min', + width: 100, + align: 'right', + renderer: function(v, meta, rec) { + return v + '/' + rec.data.min_size; + }, + dataIndex: 'size' + }, + { + text: '# Placement Groups', // pg_num', + width: 180, + align: 'right', + dataIndex: 'pg_num' + }, + { + text: 'CRUSH Rule', + columns: [ + { + text: 'ID', + align: 'right', + width: 50, + dataIndex: 'crush_rule' + }, + { + text: gettext('Name'), + width: 150, + dataIndex: 'crush_rule_name', + }, + ] + }, + { + text: gettext('Used'), + columns: [ + { + text: '%', + width: 100, + sortable: true, + align: 'right', + renderer: function(val) { + return Ext.util.Format.percent(val, '0.00'); + }, + dataIndex: 'percent_used', + summaryType: 'sum', + summaryRenderer: function(val) { + return Ext.util.Format.percent(val, '0.00'); + }, + }, + { + text: gettext('Total'), + width: 100, + sortable: true, + renderer: PVE.Utils.render_size, + align: 'right', + dataIndex: 'bytes_used', + summaryType: 'sum', + summaryRenderer: PVE.Utils.render_size + } + ] + } + ], + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var rstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: 'ceph-pool-list' + nodename, + model: 'ceph-pool-list', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/ceph/pools" + } + }); + + var store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore }); + + var regex = new RegExp("not (installed|initialized)", "i"); + PVE.Utils.handleStoreErrorOrMask(me, rstore, regex, function(me, error){ + me.store.rstore.stopUpdate(); + PVE.Utils.showCephInstallOrMask(me, error.statusText, nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.store.rstore.startUpdate(); + }); + } + ); + }); + + var create_btn = new Ext.Button({ + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.CephCreatePool', { + nodename: nodename + }); + win.show(); + win.on('destroy', function() { + rstore.load(); + }); + } + }); + + var destroy_btn = Ext.create('Proxmox.button.Button', { + text: gettext('Destroy'), + selModel: sm, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + + if (!rec.data.pool_name) { + return; + } + var base_url = '/nodes/' + nodename + '/ceph/pools/' + + rec.data.pool_name; + + var win = Ext.create('PVE.window.SafeDestroy', { + showProgress: true, + url: base_url, + params: { + remove_storages: 1 + }, + item: { type: 'CephPool', id: rec.data.pool_name } + }).show(); + win.on('destroy', function() { + rstore.load(); + }); + } + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ create_btn, destroy_btn ], + listeners: { + activate: rstore.startUpdate, + destroy: rstore.stopUpdate + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('ceph-pool-list', { + extend: 'Ext.data.Model', + fields: [ 'pool_name', + { name: 'pool', type: 'integer'}, + { name: 'size', type: 'integer'}, + { name: 'min_size', type: 'integer'}, + { name: 'pg_num', type: 'integer'}, + { name: 'bytes_used', type: 'integer'}, + { name: 'percent_used', type: 'number'}, + { name: 'crush_rule', type: 'integer'}, + { name: 'crush_rule_name', type: 'string'} + ], + idProperty: 'pool_name' + }); +}); + +Ext.define('PVE.form.CephRuleSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCephRuleSelector', + + allowBlank: false, + valueField: 'name', + displayField: 'name', + editable: false, + queryMode: 'local', + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['name'], + sorters: 'name', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/ceph/rules' + } + }); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + + store.load({ + callback: function(rec, op, success){ + if (success && rec.length > 0) { + me.select(rec[0]); + } + } + }); + } + +}); +Ext.define('PVE.CephCreateOsd', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCephCreateOsd', + + subject: 'Ceph OSD', + + showProgress: true, + + onlineHelp: 'pve_ceph_osds', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/ceph/osd", + method: 'POST', + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + Object.keys(values || {}).forEach(function(name) { + if (values[name] === '') { + delete values[name]; + } + }); + + return values; + }, + column1: [ + { + xtype: 'pveDiskSelector', + name: 'dev', + nodename: me.nodename, + diskType: 'unused', + fieldLabel: gettext('Disk'), + allowBlank: false + } + ], + column2: [ + { + xtype: 'pveDiskSelector', + name: 'db_dev', + nodename: me.nodename, + diskType: 'journal_disks', + fieldLabel: gettext('DB Disk'), + value: '', + autoSelect: false, + allowBlank: true, + emptyText: 'use OSD disk', + listeners: { + change: function(field, val) { + me.down('field[name=db_size]').setDisabled(!val); + } + } + }, + { + xtype: 'numberfield', + name: 'db_size', + fieldLabel: gettext('DB size') + ' (GiB)', + minValue: 1, + maxValue: 128*1024, + decimalPrecision: 2, + allowBlank: true, + disabled: true, + emptyText: gettext('Automatic') + } + ], + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + name: 'encrypted', + fieldLabel: gettext('Encrypt OSD') + }, + ], + advancedColumn2: [ + { + xtype: 'pveDiskSelector', + name: 'wal_dev', + nodename: me.nodename, + diskType: 'journal_disks', + fieldLabel: gettext('WAL Disk'), + value: '', + autoSelect: false, + allowBlank: true, + emptyText: 'use OSD/DB disk', + listeners: { + change: function(field, val) { + me.down('field[name=wal_size]').setDisabled(!val); + } + } + }, + { + xtype: 'numberfield', + name: 'wal_size', + fieldLabel: gettext('WAL size') + ' (GiB)', + minValue: 0.5, + maxValue: 128*1024, + decimalPrecision: 2, + allowBlank: true, + disabled: true, + emptyText: gettext('Automatic') + } + ] + }, + { + xtype: 'displayfield', + padding: '5 0 0 0', + userCls: 'pve-hint', + value: 'Note: Ceph is not compatible with disks backed by a hardware ' + + 'RAID controller. For details see ' + + 'the reference documentation.', + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.CephRemoveOsd', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveCephRemoveOsd'], + + isRemove: true, + + showProgress: true, + method: 'DELETE', + items: [ + { + xtype: 'proxmoxcheckbox', + name: 'cleanup', + checked: true, + labelWidth: 130, + fieldLabel: gettext('Cleanup Disks') + } + ], + initComponent : function() { + + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + if (me.osdid === undefined || me.osdid < 0) { + throw "no osdid specified"; + } + + me.isCreate = true; + + me.title = gettext('Destroy') + ': Ceph OSD osd.' + me.osdid.toString(); + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/ceph/osd/" + me.osdid.toString() + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.CephOsdTree', { + extend: 'Ext.tree.Panel', + alias: ['widget.pveNodeCephOsdTree'], + onlineHelp: 'chapter_pveceph', + + viewModel: { + data: { + nodename: '', + flags: [], + maxversion: '0', + versions: {}, + isOsd: false, + downOsd: false, + upOsd: false, + inOsd: false, + outOsd: false, + osdid: '', + osdhost: '', + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + + reload: function() { + var me = this.getView(); + var vm = this.getViewModel(); + var nodename = vm.get('nodename'); + var sm = me.getSelectionModel(); + Proxmox.Utils.API2Request({ + url: "/nodes/" + nodename + "/ceph/osd", + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + var msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask(me, msg, nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', this.reload); + } + ); + }, + success: function(response, opts) { + var data = response.result.data; + var selected = me.getSelection(); + var name; + if (selected.length) { + name = selected[0].data.name; + } + vm.set('versions', data.versions); + // extract max version + var maxversion = vm.get('maxversion'); + Object.values(data.versions || {}).forEach(function(version) { + if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) { + maxversion = version; + } + }); + vm.set('maxversion', maxversion); + sm.deselectAll(); + me.setRootNode(data.root); + me.expandAll(); + if (name) { + var node = me.getRootNode().findChild('name', name, true); + if (node) { + me.setSelection([node]); + } + } + + var flags = data.flags.split(','); + vm.set('flags', flags); + var noout = flags.includes('noout'); + me.down('#nooutBtn').setText(noout ? gettext("Unset noout") : gettext("Set noout")); + } + }); + }, + + osd_cmd: function(comp) { + var me = this; + var vm = this.getViewModel(); + var cmd = comp.cmd; + var params = comp.params || {}; + var osdid = vm.get('osdid'); + + var doRequest = function() { + Proxmox.Utils.API2Request({ + url: "/nodes/" + vm.get('osdhost') + "/ceph/osd/" + osdid + '/' + cmd, + waitMsgTarget: me.getView(), + method: 'POST', + params: params, + success: () => { me.reload(); }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + if (cmd === 'scrub') { + Ext.MessageBox.defaultButton = params.deep === 1 ? 2 : 1; + Ext.Msg.show({ + title: gettext('Confirm'), + icon: params.deep === 1 ? Ext.Msg.WARNING : Ext.Msg.QUESTION, + msg: params.deep !== 1 ? + Ext.String.format(gettext("Scrub OSD.{0}"), osdid) : + Ext.String.format(gettext("Deep Scrub OSD.{0}"), osdid) + + "
Caution: This can reduce performance while it is running.", + buttons: Ext.Msg.YESNO, + callback: function(btn) { + if (btn !== 'yes') { + return; + } + doRequest(); + } + }); + } else { + doRequest(); + } + }, + + create_osd: function() { + var me = this; + var vm = this.getViewModel(); + Ext.create('PVE.CephCreateOsd', { + nodename: vm.get('nodename'), + taskDone: () => { me.reload(); } + }).show(); + }, + + destroy_osd: function() { + var me = this; + var vm = this.getViewModel(); + Ext.create('PVE.CephRemoveOsd', { + nodename: vm.get('osdhost'), + osdid: vm.get('osdid'), + taskDone: () => { me.reload(); } + }).show(); + }, + + set_flag: function() { + var me = this; + var vm = this.getViewModel(); + var flags = vm.get('flags'); + Proxmox.Utils.API2Request({ + url: "/nodes/" + vm.get('nodename') + "/ceph/flags/noout", + waitMsgTarget: me.getView(), + method: flags.includes('noout') ? 'DELETE' : 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: () => { me.reload(); } + }); + }, + + service_cmd: function(comp) { + var me = this; + var vm = this.getViewModel(); + var cmd = comp.cmd || comp; + Proxmox.Utils.API2Request({ + url: "/nodes/" + vm.get('osdhost') + "/ceph/" + cmd, + params: { service: "osd." + vm.get('osdid') }, + waitMsgTarget: me.getView(), + method: 'POST', + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + taskDone: () => { me.reload(); } + }); + win.show(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + set_selection_status: function(tp, selection) { + if (selection.length < 1) { + return; + } + var rec = selection[0]; + var vm = this.getViewModel(); + + var isOsd = (rec.data.host && (rec.data.type === 'osd') && (rec.data.id >= 0)); + + vm.set('isOsd', isOsd); + vm.set('downOsd', isOsd && rec.data.status === 'down'); + vm.set('upOsd', isOsd && rec.data.status !== 'down'); + vm.set('inOsd', isOsd && rec.data.in); + vm.set('outOsd', isOsd && !rec.data.in); + vm.set('osdid', isOsd ? rec.data.id : undefined); + vm.set('osdhost', isOsd ? rec.data.host : undefined); + }, + + render_status: function(value, metaData, rec) { + if (!value) { + return value; + } + var inout = rec.data['in'] ? 'in' : 'out'; + var updownicon = value === 'up' ? 'good fa-arrow-circle-up' : + 'critical fa-arrow-circle-down'; + + var inouticon = rec.data['in'] ? 'good fa-circle' : + 'warning fa-circle-o'; + + var text = value + ' / ' + + inout + ' '; + + return text; + }, + + render_wal: function(value, metaData, rec) { + if (!value && + rec.data.osdtype === 'bluestore' && + rec.data.type === 'osd') { + return 'N/A'; + } + return value; + }, + + render_version: function(value, metadata, rec) { + var vm = this.getViewModel(); + var versions = vm.get('versions'); + var icon = ""; + var version = value || ""; + if (value && value != vm.get('maxversion')) { + icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD'); + } + + if (!value && rec.data.type == 'host') { + version = versions[rec.data.name] || Proxmox.Utils.unknownText; + } + + return icon + version; + }, + + render_osd_val: function(value, metaData, rec) { + return (rec.data.type === 'osd') ? value : ''; + }, + render_osd_weight: function(value, metaData, rec) { + if (rec.data.type !== 'osd') { + return ''; + } + return Ext.util.Format.number(value, '0.00###'); + }, + + render_osd_latency: function(value, metaData, rec) { + if (rec.data.type !== 'osd') { + return ''; + } + let commit_ms = rec.data.commit_latency_ms, + apply_ms = rec.data.apply_latency_ms; + return apply_ms + ' / ' + commit_ms; + }, + + render_osd_size: function(value, metaData, rec) { + return this.render_osd_val(PVE.Utils.render_size(value), metaData, rec); + }, + + control: { + '#': { + selectionchange: 'set_selection_status' + } + }, + + init: function(view) { + var me = this; + var vm = this.getViewModel(); + + if (!view.pveSelNode.data.node) { + throw "no node name specified"; + } + + vm.set('nodename', view.pveSelNode.data.node); + + me.callParent(); + me.reload(); + } + }, + + stateful: true, + stateId: 'grid-ceph-osd', + rootVisible: false, + useArrows: true, + + columns: [ + { + xtype: 'treecolumn', + text: 'Name', + dataIndex: 'name', + width: 150 + }, + { + text: 'Type', + dataIndex: 'type', + hidden: true, + align: 'right', + width: 75 + }, + { + text: gettext("Class"), + dataIndex: 'device_class', + align: 'right', + width: 75 + }, + { + text: "OSD Type", + dataIndex: 'osdtype', + align: 'right', + width: 100 + }, + { + text: "Bluestore Device", + dataIndex: 'blfsdev', + align: 'right', + width: 75, + hidden: true + }, + { + text: "DB Device", + dataIndex: 'dbdev', + align: 'right', + width: 75, + hidden: true + }, + { + text: "WAL Device", + dataIndex: 'waldev', + align: 'right', + renderer: 'render_wal', + width: 75, + hidden: true + }, + { + text: 'Status', + dataIndex: 'status', + align: 'right', + renderer: 'render_status', + width: 120 + }, + { + text: gettext('Version'), + dataIndex: 'version', + align: 'right', + renderer: 'render_version' + }, + { + text: 'weight', + dataIndex: 'crush_weight', + align: 'right', + renderer: 'render_osd_weight', + width: 90 + }, + { + text: 'reweight', + dataIndex: 'reweight', + align: 'right', + renderer: 'render_osd_weight', + width: 90 + }, + { + text: gettext('Used') + ' (%)', + dataIndex: 'percent_used', + align: 'right', + renderer: function(value, metaData, rec) { + if (rec.data.type !== 'osd') { + return ''; + } + return Ext.util.Format.number(value, '0.00'); + }, + width: 100 + }, + { + text: gettext('Total'), + dataIndex: 'total_space', + align: 'right', + renderer: 'render_osd_size', + width: 100 + }, + { + text: 'Apply/Commit
Latency (ms)', + dataIndex: 'apply_latency_ms', + align: 'right', + renderer: 'render_osd_latency', + width: 120 + } + ], + + + tbar: { + items: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: 'reload' + }, + '-', + { + text: gettext('Create') + ': OSD', + handler: 'create_osd', + }, + { + text: gettext('Set noout'), + itemId: 'nooutBtn', + handler: 'set_flag', + }, + '->', + { + xtype: 'tbtext', + data: { + osd: undefined + }, + bind: { + data: { + osd: "{osdid}" + } + }, + tpl: [ + '', + 'osd.{osd}:', + '', + gettext('No OSD selected'), + '' + ] + }, + { + text: gettext('Start'), + iconCls: 'fa fa-play', + disabled: true, + bind: { + disabled: '{!downOsd}' + }, + cmd: 'start', + handler: 'service_cmd' + }, + { + text: gettext('Stop'), + iconCls: 'fa fa-stop', + disabled: true, + bind: { + disabled: '{!upOsd}' + }, + cmd: 'stop', + handler: 'service_cmd' + }, + { + text: gettext('Restart'), + iconCls: 'fa fa-refresh', + disabled: true, + bind: { + disabled: '{!upOsd}' + }, + cmd: 'restart', + handler: 'service_cmd' + }, + '-', + { + text: 'Out', + iconCls: 'fa fa-circle-o', + disabled: true, + bind: { + disabled: '{!inOsd}' + }, + cmd: 'out', + handler: 'osd_cmd' + }, + { + text: 'In', + iconCls: 'fa fa-circle', + disabled: true, + bind: { + disabled: '{!outOsd}' + }, + cmd: 'in', + handler: 'osd_cmd' + }, + '-', + { + text: gettext('More'), + iconCls: 'fa fa-bars', + disabled: true, + bind: { + disabled: '{!isOsd}' + }, + menu: [ + { + text: gettext('Scrub'), + iconCls: 'fa fa-shower', + cmd: 'scrub', + handler: 'osd_cmd' + }, + { + text: gettext('Deep Scrub'), + iconCls: 'fa fa-bath', + cmd: 'scrub', + params: { + deep: 1, + }, + handler: 'osd_cmd' + }, + { + text: gettext('Destroy'), + itemId: 'remove', + iconCls: 'fa fa-fw fa-trash-o', + bind: { + disabled: '{!downOsd}' + }, + handler: 'destroy_osd' + } + ], + } + ] + }, + + fields: [ + 'name', 'type', 'status', 'host', 'in', 'id' , + { type: 'number', name: 'reweight' }, + { type: 'number', name: 'percent_used' }, + { type: 'integer', name: 'bytes_used' }, + { type: 'integer', name: 'total_space' }, + { type: 'integer', name: 'apply_latency_ms' }, + { type: 'integer', name: 'commit_latency_ms' }, + { type: 'string', name: 'device_class' }, + { type: 'string', name: 'osdtype' }, + { type: 'string', name: 'blfsdev' }, + { type: 'string', name: 'dbdev' }, + { type: 'string', name: 'waldev' }, + { type: 'string', name: 'version', calculate: function(data) { + return PVE.Utils.parse_ceph_version(data); + } }, + { type: 'string', name: 'iconCls', calculate: function(data) { + var iconMap = { + host: 'fa-building', + osd: 'fa-hdd-o', + root: 'fa-server', + }; + return 'fa x-fa-tree ' + iconMap[data.type]; + } }, + { type: 'number', name: 'crush_weight' } + ], +}); +Ext.define('PVE.node.CephMonMgrList', { + extend: 'Ext.container.Container', + xtype: 'pveNodeCephMonMgr', + + mixins: ['Proxmox.Mixin.CBind' ], + + onlineHelp: 'chapter_pveceph', + + defaults: { + border: false, + onlineHelp: 'chapter_pveceph', + flex: 1 + }, + + layout: { + type: 'vbox', + align: 'stretch' + }, + + items: [ + { + xtype: 'pveNodeCephServiceList', + cbind: { pveSelNode: '{pveSelNode}' }, + type: 'mon', + additionalColumns: [ + { + header: gettext('Quorum'), + width: 70, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'quorum' + } + ], + stateId: 'grid-ceph-monitor', + showCephInstallMask: true, + title: gettext('Monitor') + }, + { + xtype: 'pveNodeCephServiceList', + type: 'mgr', + stateId: 'grid-ceph-manager', + cbind: { pveSelNode: '{pveSelNode}' }, + title: gettext('Manager') + } + ] +}); +Ext.define('PVE.node.CephCrushMap', { + extend: 'Ext.panel.Panel', + alias: ['widget.pveNodeCephCrushMap'], + bodyStyle: 'white-space:pre', + bodyPadding: 5, + border: false, + stateful: true, + stateId: 'layout-ceph-crush', + scrollable: true, + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + var msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.load(); + }); + } + ); + }, + success: function(response, opts) { + var data = response.result.data; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: '/nodes/' + nodename + '/ceph/crush', + + listeners: { + activate: function() { + me.load(); + } + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('PVE.node.CephStatus', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephStatus', + + onlineHelp: 'chapter_pveceph', + + scrollable: true, + + bodyPadding: 5, + + layout: { + type: 'column' + }, + + defaults: { + padding: 5 + }, + + items: [ + { + xtype: 'panel', + title: gettext('Health'), + bodyPadding: 10, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + minHeight: 230, + columnWidth: 1 + }, + 'width >= 1900': { + minHeight: 500, + columnWidth: 0.5 + } + }, + layout: { + type: 'hbox', + align: 'stretch' + }, + items: [ + { + flex: 1, + itemId: 'overallhealth', + xtype: 'pveHealthWidget', + title: gettext('Status') + }, + { + flex: 2, + itemId: 'warnings', + stateful: true, + stateId: 'ceph-status-warnings', + xtype: 'grid', + // since we load the store manually, + // to show the emptytext, we have to + // specify an empty store + store: { data:[] }, + emptyText: gettext('No Warnings/Errors'), + columns: [ + { + dataIndex: 'severity', + header: gettext('Severity'), + align: 'center', + width: 70, + renderer: function(value) { + var health = PVE.Utils.map_ceph_health[value]; + var classes = PVE.Utils.get_health_icon(health); + + return ''; + }, + sorter: { + sorterFn: function(a,b) { + var healthArr = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK']; + return healthArr.indexOf(b.data.severity) - healthArr.indexOf(a.data.severity); + } + } + }, + { + dataIndex: 'summary', + header: gettext('Summary'), + flex: 1 + }, + { + xtype: 'actioncolumn', + width: 40, + align: 'center', + tooltip: gettext('Detail'), + items: [ + { + iconCls: 'x-fa fa-info-circle', + handler: function(grid, rowindex, colindex, item, e, record) { + var win = Ext.create('Ext.window.Window', { + title: gettext('Detail'), + resizable: true, + modal: true, + width: 650, + height: 400, + layout: { + type: 'fit' + }, + items: [{ + scrollable: true, + padding: 10, + xtype: 'box', + html: [ + '' + Ext.htmlEncode(record.data.summary) + '', + '
' + Ext.htmlEncode(record.data.detail) + '
' + ] + }] + }); + win.show(); + } + } + ] + } + ] + } + ] + }, + { + xtype: 'pveCephStatusDetail', + itemId: 'statusdetail', + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1, + minHeight: 250 + }, + 'width >= 1900': { + columnWidth: 0.5, + minHeight: 300 + } + }, + title: gettext('Status') + }, + { + title: gettext('Services'), + xtype: 'pveCephServices', + itemId: 'services', + plugins: 'responsive', + layout: { + type: 'hbox', + align: 'stretch' + }, + responsiveConfig: { + 'width < 1900': { + columnWidth: 1, + minHeight: 200 + }, + 'width >= 1900': { + columnWidth: 0.5, + minHeight: 200 + } + } + }, + { + xtype: 'panel', + title: gettext('Performance'), + columnWidth: 1, + bodyPadding: 5, + layout: { + type: 'hbox', + align: 'center' + }, + items: [ + { + flex: 1, + xtype: 'proxmoxGauge', + itemId: 'space', + title: gettext('Usage') + }, + { + flex: 2, + xtype: 'container', + defaults: { + padding: 0, + height: 100 + }, + items: [ + { + itemId: 'reads', + xtype: 'pveRunningChart', + title: gettext('Reads'), + renderer: PVE.Utils.render_bandwidth + }, + { + itemId: 'writes', + xtype: 'pveRunningChart', + title: gettext('Writes'), + renderer: PVE.Utils.render_bandwidth + }, + { + itemId: 'iops', + xtype: 'pveRunningChart', + hidden: true, + title: 'IOPS', // do not localize + renderer: Ext.util.Format.numberRenderer('0,000') + }, + { + itemId: 'readiops', + xtype: 'pveRunningChart', + hidden: true, + title: 'IOPS: ' + gettext('Reads'), + renderer: Ext.util.Format.numberRenderer('0,000') + }, + { + itemId: 'writeiops', + xtype: 'pveRunningChart', + hidden: true, + title: 'IOPS: ' + gettext('Writes'), + renderer: Ext.util.Format.numberRenderer('0,000') + } + ] + } + ] + } + ], + + generateCheckData: function(health) { + var result = []; + var checks = health.checks || {}; + var keys = Ext.Object.getKeys(checks).sort(); + + Ext.Array.forEach(keys, function(key) { + var details = checks[key].detail || []; + result.push({ + id: key, + summary: checks[key].summary.message, + detail: Ext.Array.reduce( + checks[key].detail, + function(first, second) { + return first + '\n' + second.message; + }, + '' + ), + severity: checks[key].severity + }); + }); + + return result; + }, + + updateAll: function(store, records, success) { + if (!success || records.length === 0) { + return; + } + + var me = this; + var rec = records[0]; + me.status = rec.data; + + // add health panel + me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {})); + // add errors to gridstore + me.down('#warnings').getStore().loadRawData(me.generateCheckData(rec.data.health || {}), false); + + // update services + me.getComponent('services').updateAll(me.metadata || {}, rec.data); + + // update detailstatus panel + me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data); + + // add performance data + var used = rec.data.pgmap.bytes_used; + var total = rec.data.pgmap.bytes_total; + + var text = Ext.String.format(gettext('{0} of {1}'), + PVE.Utils.render_size(used), + PVE.Utils.render_size(total) + ); + + // update the usage widget + me.down('#space').updateValue(used/total, text); + + // TODO: logic for jewel (iops split in read/write) + + var iops = rec.data.pgmap.op_per_sec; + var readiops = rec.data.pgmap.read_op_per_sec; + var writeiops = rec.data.pgmap.write_op_per_sec; + var reads = rec.data.pgmap.read_bytes_sec || 0; + var writes = rec.data.pgmap.write_bytes_sec || 0; + + if (iops !== undefined && me.version !== 'hammer') { + me.change_version('hammer'); + } else if((readiops !== undefined || writeiops !== undefined) && me.version !== 'jewel') { + me.change_version('jewel'); + } + // update the graphs + me.reads.addDataPoint(reads); + me.writes.addDataPoint(writes); + me.iops.addDataPoint(iops); + me.readiops.addDataPoint(readiops); + me.writeiops.addDataPoint(writeiops); + }, + + change_version: function(version) { + var me = this; + me.version = version; + me.sp.set('ceph-version', version); + me.iops.setVisible(version === 'hammer'); + me.readiops.setVisible(version === 'jewel'); + me.writeiops.setVisible(version === 'jewel'); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + + me.callParent(); + var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph'; + me.store = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'ceph-status-' + (nodename || 'cluster'), + interval: 5000, + proxy: { + type: 'proxmox', + url: baseurl + '/status' + } + }); + + me.metadatastore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'ceph-metadata-' + (nodename || 'cluster'), + interval: 15*1000, + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/ceph/metadata' + } + }); + + // save references for the updatefunction + me.iops = me.down('#iops'); + me.readiops = me.down('#readiops'); + me.writeiops = me.down('#writeiops'); + me.reads = me.down('#reads'); + me.writes = me.down('#writes'); + + // get ceph version + me.sp = Ext.state.Manager.getProvider(); + me.version = me.sp.get('ceph-version'); + me.change_version(me.version); + + var regex = new RegExp("not (installed|initialized)", "i"); + PVE.Utils.handleStoreErrorOrMask(me, me.store, regex, function(me, error){ + me.store.stopUpdate(); + PVE.Utils.showCephInstallOrMask(me, error.statusText, (nodename || 'localhost'), + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.store.startUpdate(); + }); + } + ); + }); + + me.mon(me.store, 'load', me.updateAll, me); + me.mon(me.metadatastore, 'load', function(store, records, success) { + if (!success || records.length < 1) { + return; + } + var rec = records[0]; + me.metadata = rec.data; + + // update services + me.getComponent('services').updateAll(rec.data, me.status || {}); + + // update detailstatus panel + me.getComponent('statusdetail').updateAll(rec.data, me.status || {}); + + }, me); + + me.on('destroy', me.store.stopUpdate); + me.on('destroy', me.metadatastore.stopUpdate); + me.store.startUpdate(); + me.metadatastore.startUpdate(); + } + +}); +Ext.define('PVE.ceph.StatusDetail', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveCephStatusDetail', + + layout: { + type: 'hbox', + align: 'stretch' + }, + + bodyPadding: '0 5', + defaults: { + xtype: 'box', + style: { + 'text-align':'center' + } + }, + + items: [{ + flex: 1, + itemId: 'osds', + maxHeight: 250, + scrollable: true, + padding: '0 10 5 10', + data: { + total: 0, + upin: 0, + upout: 0, + downin: 0, + downout: 0, + oldosds: [] + }, + tpl: [ + '

' + 'OSDs' + '

', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
', + gettext('In'), + '', + gettext('Out'), + '
', + gettext('Up'), + '{upin}{upout}
', + gettext('Down'), + '{downin}{downout}
', + '
', + gettext('Total'), + ': {total}', + '

', + '', + ' ' + gettext('Outdated OSDs') + "
", + '
', + '', + '
osd.{id}:
', + '
{version}

', + '
', + '
', + '
', + '
' + ] + }, + { + flex: 1, + border: false, + itemId: 'pgchart', + xtype: 'polar', + height: 184, + innerPadding: 5, + insetPadding: 5, + colors: [ + '#CFCFCF', + '#21BF4B', + '#FFCC00', + '#FF6C59' + ], + store: { }, + series: [ + { + type: 'pie', + donut: 60, + angleField: 'count', + tooltip: { + trackMouse: true, + renderer: function(tooltip, record, ctx) { + var html = record.get('text'); + html += '
'; + record.get('states').forEach(function(state) { + html += '
' + + state.state_name + ': ' + state.count.toString(); + }); + tooltip.setHtml(html); + } + }, + subStyle: { + strokeStyle: false + } + } + ] + }, + { + flex: 1.6, + itemId: 'pgs', + padding: '0 10', + maxHeight: 250, + scrollable: true, + data: { + states: [] + }, + tpl: [ + '

' + 'PGs' + '

', + '', + '
{state_name}:
', + '
{count}

', + '
', + '
' + ] + }], + + // similar to mgr dashboard + pgstates: { + // clean + clean: 1, + active: 1, + + // working + activating: 2, + backfill_wait: 2, + backfilling: 2, + creating: 2, + deep: 2, + degraded: 2, + forced_backfill: 2, + forced_recovery: 2, + peered: 2, + peering: 2, + recovering: 2, + recovery_wait: 2, + repair: 2, + scrubbing: 2, + snaptrim: 2, + snaptrim_wait: 2, + + // error + backfill_toofull: 3, + backfill_unfound: 3, + down: 3, + incomplete: 3, + inconsistent: 3, + recovery_toofull: 3, + recovery_unfound: 3, + remapped: 3, + snaptrim_error: 3, + stale: 3, + undersized: 3 + }, + + statecategories: [ + { + text: gettext('Unknown'), + count: 0, + states: [], + cls: 'faded' + }, + { + text: gettext('Clean'), + cls: 'good' + }, + { + text: gettext('Working'), + cls: 'warning' + }, + { + text: gettext('Error'), + cls: 'critical' + } + ], + + updateAll: function(metadata, status) { + var me = this; + me.suspendLayout = true; + + var maxversion = "0"; + Object.values(metadata.version || {}).forEach(function(version) { + if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) { + maxversion = version; + } + }); + + var oldosds = []; + + if (metadata.osd) { + metadata.osd.forEach(function(osd) { + var version = PVE.Utils.parse_ceph_version(osd); + if (version != maxversion) { + oldosds.push({ + id: osd.id, + version: version + }); + } + }); + } + + var pgmap = status.pgmap || {}; + var health = status.health || {}; + var osdmap = status.osdmap || { osdmap: {} }; + + + // update pgs sorted + var pgs_by_state = pgmap.pgs_by_state || []; + pgs_by_state.sort(function(a,b){ + return (a.state_name < b.state_name)?-1:(a.state_name === b.state_name)?0:1; + }); + + me.statecategories.forEach(function(cat) { + cat.count = 0; + cat.states = []; + }); + + pgs_by_state.forEach(function(state) { + var i; + var states = state.state_name.split(/[^a-z]+/); + var result = 0; + for (i = 0; i < states.length; i++) { + if (me.pgstates[states[i]] > result) { + result = me.pgstates[states[i]]; + } + } + // for the list + state.cls = me.statecategories[result].cls; + + me.statecategories[result].count += state.count; + me.statecategories[result].states.push(state); + }); + + me.getComponent('pgchart').getStore().setData(me.statecategories); + me.getComponent('pgs').update({states: pgs_by_state}); + + var downinregex = /(\d+) osds down/; + var downin_osds = 0; + + // we collect monitor/osd information from the checks + Ext.Object.each(health.checks, function(key, value, obj) { + var found = null; + if (key === 'OSD_DOWN') { + found = value.summary.message.match(downinregex); + if (found !== null) { + downin_osds = parseInt(found[1],10); + } + } + }); + + // update osds counts + + var total_osds = osdmap.osdmap.num_osds || 0; + var in_osds = osdmap.osdmap.num_in_osds || 0; + var up_osds = osdmap.osdmap.num_up_osds || 0; + var out_osds = total_osds - in_osds; + var down_osds = total_osds - up_osds; + + var downout_osds = down_osds - downin_osds; + var upin_osds = in_osds - downin_osds; + var upout_osds = up_osds - upin_osds; + var osds = { + total: total_osds, + upin: upin_osds, + upout: upout_osds, + downin: downin_osds, + downout: downout_osds, + oldosds: oldosds + }; + var osdcomponent = me.getComponent('osds'); + osdcomponent.update(Ext.apply(osdcomponent.data, osds)); + + me.suspendLayout = false; + me.updateLayout(); + } +}); + +Ext.define('PVE.ceph.Services', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveCephServices', + + layout: { + type: 'hbox', + align: 'stretch' + }, + + bodyPadding: '0 5 20', + defaults: { + xtype: 'box', + style: { + 'text-align':'center' + } + }, + + items: [ + { + flex: 1, + xtype: 'pveCephServiceList', + itemId: 'mons', + title: gettext('Monitors') + }, + { + flex: 1, + xtype: 'pveCephServiceList', + itemId: 'mgrs', + title: gettext('Managers') + }, + { + flex: 1, + xtype: 'pveCephServiceList', + itemId: 'mdss', + title: gettext('Meta Data Servers') + } + ], + + updateAll: function(metadata, status) { + var me = this; + + var healthstates = { + 'HEALTH_UNKNOWN': 0, + 'HEALTH_ERR': 1, + 'HEALTH_WARN': 2, + 'HEALTH_OLD': 3, + 'HEALTH_OK': 4 + }; + var healthmap = [ + 'HEALTH_UNKNOWN', + 'HEALTH_ERR', + 'HEALTH_WARN', + 'HEALTH_OLD', + 'HEALTH_OK' + ]; + var reduceFn = function(first, second) { + return first + '\n' + second.message; + }; + var services = ['mon','mgr','mds']; + var maxversion = "00.0.00"; + Object.values(metadata.version || {}).forEach(function(version) { + if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) { + maxversion = version; + } + }); + var i; + var quorummap = (status && status.quorum_names) ? status.quorum_names : []; + var monmessages = {}; + var mgrmessages = {}; + var mdsmessages = {}; + if (status) { + if (status.health) { + Ext.Object.each(status.health.checks, function(key, value, obj) { + if (!Ext.String.startsWith(key, "MON_")) { + return; + } + + var i; + for (i = 0; i < value.detail.length; i++) { + var match = value.detail[i].message.match(/mon.([a-zA-Z0-9\-\.]+)/); + if (!match) { + continue; + } + var monid = match[1]; + + if (!monmessages[monid]) { + monmessages[monid] = { + worstSeverity: healthstates.HEALTH_OK, + messages: [] + }; + } + + + monmessages[monid].messages.push( + PVE.Utils.get_ceph_icon_html(value.severity, true) + + Ext.Array.reduce(value.detail, reduceFn, '') + ); + if (healthstates[value.severity] < monmessages[monid].worstSeverity) { + monmessages[monid].worstSeverity = healthstates[value.severity]; + } + } + }); + } + + if (status.mgrmap) { + mgrmessages[status.mgrmap.active_name] = "active"; + status.mgrmap.standbys.forEach(function(mgr) { + mgrmessages[mgr.name] = "standby"; + }); + } + + if (status.fsmap) { + status.fsmap.by_rank.forEach(function(mds) { + mdsmessages[mds.name] = 'rank: ' + mds.rank + "; " + mds.status; + }); + } + } + + var checks = { + mon: function(mon) { + if (quorummap.indexOf(mon.name) !== -1) { + mon.health = healthstates.HEALTH_OK; + } else { + mon.health = healthstates.HEALTH_ERR; + } + if (monmessages[mon.name]) { + if (monmessages[mon.name].worstSeverity < mon.health) { + mon.health = monmessages[mon.name].worstSeverity; + } + Array.prototype.push.apply(mon.messages, monmessages[mon.name].messages); + } + return mon; + }, + mgr: function(mgr) { + if (mgrmessages[mgr.name] === 'active') { + mgr.title = '' + mgr.title + ''; + mgr.statuses.push(gettext('Status') + ': active'); + } else if (mgrmessages[mgr.name] === 'standby') { + mgr.statuses.push(gettext('Status') + ': standby'); + } else if (mgr.health > healthstates.HEALTH_WARN) { + mgr.health = healthstates.HEALTH_WARN; + } + + return mgr; + }, + mds: function(mds) { + if (mdsmessages[mds.name]) { + mds.title = '' + mds.title + ''; + mds.statuses.push(gettext('Status') + ': ' + mdsmessages[mds.name]+""); + } else if (mds.addr !== Proxmox.Utils.unknownText) { + mds.statuses.push(gettext('Status') + ': standby'); + } + + return mds; + } + }; + + for (i = 0; i < services.length; i++) { + var type = services[i]; + var ids = Object.keys(metadata[type] || {}); + me[type] = {}; + + var j; + for (j = 0; j < ids.length; j++) { + var id = ids[j]; + var tmp = id.split('@'); + var name = tmp[0]; + var host = tmp[1]; + var result = { + id: id, + health: healthstates.HEALTH_OK, + statuses: [], + messages: [], + name: name, + title: metadata[type][id].name || name, + host: host, + version: PVE.Utils.parse_ceph_version(metadata[type][id]), + service: metadata[type][id].service, + addr: metadata[type][id].addr || metadata[type][id].addrs || Proxmox.Utils.unknownText + }; + + result.statuses = [ + gettext('Host') + ": " + result.host, + gettext('Address') + ": " + result.addr + ]; + + if (checks[type]) { + result = checks[type](result); + } + + if (result.service && !result.version) { + result.messages.push( + PVE.Utils.get_ceph_icon_html('HEALTH_UNKNOWN', true) + + gettext('Stopped') + ); + result.health = healthstates.HEALTH_UNKNOWN; + } + + if (!result.version && result.addr === Proxmox.Utils.unknownText) { + result.health = healthstates.HEALTH_UNKNOWN; + } + + if (result.version) { + result.statuses.push(gettext('Version') + ": " + result.version); + + if (result.version != maxversion) { + if (result.health > healthstates.HEALTH_OLD) { + result.health = healthstates.HEALTH_OLD; + } + result.messages.push( + PVE.Utils.get_ceph_icon_html('HEALTH_OLD', true) + + gettext('Not Current Version, please upgrade') + ); + } + } + + result.statuses.push(''); // empty line + result.text = result.statuses.concat(result.messages).join('
'); + + result.health = healthmap[result.health]; + + me[type][id] = result; + } + } + + me.getComponent('mons').updateAll(Object.values(me.mon)); + me.getComponent('mgrs').updateAll(Object.values(me.mgr)); + me.getComponent('mdss').updateAll(Object.values(me.mds)); + } +}); + +Ext.define('PVE.ceph.ServiceList', { + extend: 'Ext.container.Container', + xtype: 'pveCephServiceList', + + style: { + 'text-align':'center' + }, + defaults: { + xtype: 'box', + style: { + 'text-align':'center' + } + }, + + items: [ + { + itemId: 'title', + data: { + title: '' + }, + tpl: '

{title}

' + } + ], + + updateAll: function(list) { + var me = this; + me.suspendLayout = true; + + var i; + list.sort(function(a,b) { + return a.id > b.id ? 1 : a.id < b.id ? -1 : 0; + }); + var ids = {}; + if (me.ids) { + me.ids.forEach(function(id) { + ids[id] = true; + }); + } + for (i = 0; i < list.length; i++) { + var service = me.getComponent(list[i].id); + if (!service) { + // since services are already sorted, and + // we always have a sorted list + // we can add it at the service+1 position (because of the title) + service = me.insert(i+1, { + xtype: 'pveCephServiceWidget', + itemId: list[i].id + }); + if (!me.ids) { + me.ids = []; + } + me.ids.push(list[i].id); + } else { + delete ids[list[i].id]; + } + service.updateService(list[i].title, list[i].text, list[i].health); + } + + Object.keys(ids).forEach(function(id) { + me.remove(id); + }); + me.suspendLayout = false; + me.updateLayout(); + }, + + initComponent: function() { + var me = this; + me.callParent(); + me.getComponent('title').update({ + title: me.title + }); + } +}); + +/*jslint confusion: true*/ +Ext.define('PVE.ceph.ServiceWidget', { + extend: 'Ext.Component', + alias: 'widget.pveCephServiceWidget', + + userCls: 'monitor inline-block', + data: { + title: '0', + health: 'HEALTH_ERR', + text: '', + iconCls: PVE.Utils.get_health_icon() + }, + + tpl: [ + '{title}: ', + '' + ], + + updateService: function(title, text, health) { + var me = this; + + me.update(Ext.apply(me.data, { + health: health, + text: text, + title: title, + iconCls: PVE.Utils.get_health_icon(PVE.Utils.map_ceph_health[health]) + })); + + if (me.tooltip) { + me.tooltip.setHtml(text); + } + }, + + listeners: { + destroy: function() { + var me = this; + if (me.tooltip) { + me.tooltip.destroy(); + delete me.tooltip; + } + }, + mouseenter: { + element: 'el', + fn: function(events, element) { + var me = this.component; + if (!me) { + return; + } + if (!me.tooltip) { + me.tooltip = Ext.create('Ext.tip.ToolTip', { + target: me.el, + trackMouse: true, + dismissDelay: 0, + renderTo: Ext.getBody(), + html: me.data.text + }); + } + me.tooltip.show(); + } + }, + mouseleave: { + element: 'el', + fn: function(events, element) { + var me = this.component; + if (me.tooltip) { + me.tooltip.destroy(); + delete me.tooltip; + } + } + } + } +}); +Ext.define('PVE.node.CephConfigDb', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveNodeCephConfigDb', + + border: false, + store: { + proxy: { + type: 'proxmox' + } + }, + + columns: [ + { + dataIndex: 'section', + text: 'WHO', + width: 100, + }, + { + dataIndex: 'mask', + text: 'MASK', + hidden: true, + width: 80, + }, + { + dataIndex: 'level', + hidden: true, + text: 'LEVEL', + }, + { + dataIndex: 'name', + flex: 1, + text: 'OPTION', + }, + { + dataIndex: 'value', + flex: 1, + text: 'VALUE', + }, + { + dataIndex: 'can_update_at_runtime', + text: 'Runtime Updatable', + hidden: true, + width: 80, + renderer: Proxmox.Utils.format_boolean + }, + ], + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.store.proxy.url = '/api2/json/nodes/' + nodename + '/ceph/configdb'; + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore()); + me.getStore().load(); + } +}); +Ext.define('PVE.node.CephConfig', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephConfig', + + bodyStyle: 'white-space:pre', + bodyPadding: 5, + border: false, + scrollable: true, + load: function() { + var me = this; + + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + failure: function(response, opts) { + me.update(gettext('Error') + " " + response.htmlStatus); + var msg = response.htmlStatus; + PVE.Utils.showCephInstallOrMask(me.ownerCt, msg, me.pveSelNode.data.node, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.load(); + }); + } + ); + + }, + success: function(response, opts) { + var data = response.result.data; + me.update(Ext.htmlEncode(data)); + } + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + url: '/nodes/' + nodename + '/ceph/config', + listeners: { + activate: function() { + me.load(); + } + } + }); + + me.callParent(); + + me.load(); + } +}); + +Ext.define('PVE.node.CephConfigCrush', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeCephConfigCrush', + + onlineHelp: 'chapter_pveceph', + + layout: 'border', + items: [{ + title: gettext('Configuration'), + xtype: 'pveNodeCephConfig', + region: 'center' + }, + { + title: 'Crush Map', // do not localize + xtype: 'pveNodeCephCrushMap', + region: 'east', + split: true, + width: '50%' + }, + { + title: gettext('Configuration Database'), + xtype: 'pveNodeCephConfigDb', + region: 'south', + split: true, + weight: -30, + height: '50%' + }], + + initComponent: function() { + var me = this; + me.defaults = { + pveSelNode: me.pveSelNode + }; + me.callParent(); + } +}); +Ext.define('PVE.ceph.Log', { + extend: 'Proxmox.panel.LogView', + xtype: 'cephLogView', + nodename: undefined, + failCallback: function(response) { + var me = this; + var msg = response.htmlStatus; + var windowShow = PVE.Utils.showCephInstallOrMask(me, msg, me.nodename, + function(win){ + me.mon(win, 'cephInstallWindowClosed', function(){ + me.loadTask.delay(200); + }); + } + ); + if (!windowShow) { + Proxmox.Utils.setErrorMask(me, msg); + } + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.ceph.CephInstallWizard', { + extend: 'PVE.window.Wizard', + alias: 'widget.pveCephInstallWizard', + mixins: ['Proxmox.Mixin.CBind'], + resizable: false, + nodename: undefined, + viewModel: { + data: { + nodename: '', + configuration: true, + isInstalled: false + } + }, + cbindData: { + nodename: undefined + }, + title: gettext('Setup'), + navigateNext: function() { + var tp = this.down('#wizcontent'); + var atab = tp.getActiveTab(); + + var next = tp.items.indexOf(atab) + 1; + var ntab = tp.items.getAt(next); + if (ntab) { + ntab.enable(); + tp.setActiveTab(ntab); + } + }, + setInitialTab: function (index) { + var tp = this.down('#wizcontent'); + var initialTab = tp.items.getAt(index); + initialTab.enable(); + tp.setActiveTab(initialTab); + }, + onShow: function() { + this.callParent(arguments); + var isInstalled = this.getViewModel().get('isInstalled'); + if (isInstalled) { + this.getViewModel().set('configuration', false); + this.setInitialTab(2); + } + }, + items: [ + { + title: gettext('Info'), + xtype: 'panel', + border: false, + bodyBorder: false, + onlineHelp: 'chapter_pveceph', + html: '

Ceph?

'+ + '

"Ceph is a unified, distributed storage system designed for excellent performance, reliability and scalability."

'+ + '

Ceph is currently not installed on this node, click on the next button below to start the installation.'+ + ' This wizard will guide you through the necessary steps, after the initial installation you will be offered to create an initial configuration.'+ + ' The configuration step is only needed once per cluster and will be skipped if a config is already present.

'+ + '

Please take a look at our documentation, by clicking the help button below, before starting the installation, '+ + 'if you want to gain deeper knowledge about Ceph visit ceph.com.

', + listeners: { + activate: function() { + // notify owning container that it should display a help button + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp); + } + this.up('pveCephInstallWizard').down('#back').hide(true); + this.up('pveCephInstallWizard').down('#next').setText(gettext('Start installation')); + }, + deactivate: function() { + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp); + } + this.up('pveCephInstallWizard').down('#next').setText(gettext('Next')); + } + } + }, + { + title: gettext('Installation'), + xtype: 'panel', + layout: 'fit', + cbind:{ + nodename: '{nodename}' + }, + viewModel: {}, // needed to inherit parent viewModel data + listeners: { + afterrender: function() { + var me = this; + if (this.getViewModel().get('isInstalled')) { + this.mask("Ceph is already installed, click next to create your configuration.",['pve-static-mask']); + } else { + me.down('pveNoVncConsole').fireEvent('activate'); + } + }, + activate: function() { + var me = this; + var nodename = me.nodename; + me.updateStore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'ceph-status-' + nodename, + interval: 1000, + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + nodename + '/ceph/status' + }, + listeners: { + load: function(rec, response, success, operation) { + + if (success) { + me.updateStore.stopUpdate(); + me.down('textfield').setValue('success'); + } else if (operation.error.statusText.match("not initialized", "i")) { + me.updateStore.stopUpdate(); + me.up('pveCephInstallWizard').getViewModel().set('configuration',false); + me.down('textfield').setValue('success'); + } else if (operation.error.statusText.match("rados_connect failed", "i")) { + me.updateStore.stopUpdate(); + me.up('pveCephInstallWizard').getViewModel().set('configuration',true); + me.down('textfield').setValue('success'); + } else if (!operation.error.statusText.match("not installed", "i")) { + Proxmox.Utils.setErrorMask(me, operation.error.statusText); + } + } + } + }); + me.updateStore.startUpdate(); + }, + destroy: function() { + var me = this; + if (me.updateStore) { + me.updateStore.stopUpdate(); + } + } + }, + items: [ + { + itemId: 'jsconsole', + consoleType: 'cmd', + xtermjs: true, + xtype: 'pveNoVncConsole', + cbind:{ + nodename: '{nodename}' + }, + cmd: 'ceph_install' + }, + { + xtype: 'textfield', + name: 'installSuccess', + value: '', + allowBlank: false, + submitValue: false, + hidden: true + } + ] + }, + { + xtype: 'inputpanel', + title: gettext('Configuration'), + onlineHelp: 'chapter_pveceph', + cbind: { + nodename: '{nodename}' + }, + viewModel: { + data: { + replicas: undefined, + minreplicas: undefined + } + }, + listeners: { + activate: function() { + this.up('pveCephInstallWizard').down('#submit').setText(gettext('Next')); + }, + beforeshow: function() { + if (this.up('pveCephInstallWizard').getViewModel().get('configuration')) { + this.mask("Coniguration already initialized",['pve-static-mask']); + } else { + this.unmask(); + } + }, + deactivate: function() { + this.up('pveCephInstallWizard').down('#submit').setText(gettext('Finish')); + } + }, + column1: [ + { + xtype: 'displayfield', + value: gettext('Ceph cluster configuration') + ':' + }, + { + xtype: 'proxmoxNetworkSelector', + name: 'network', + value: '', + fieldLabel: 'Public Network IP/CIDR', + bind: { + allowBlank: '{configuration}' + } + }, + { + xtype: 'proxmoxNetworkSelector', + name: 'cluster-network', + fieldLabel: 'Cluster Network IP/CIDR', + allowBlank: true, + autoSelect: false, + emptyText: gettext('Same as Public Network') + } + // FIXME: add hint about cluster network and/or reference user to docs?? + ], + column2: [ + { + xtype: 'displayfield', + value: gettext('First Ceph monitor') + ':' + }, + { + xtype: 'pveNodeSelector', + fieldLabel: gettext('Monitor node'), + name: 'mon-node', + selectCurNode: true, + allowBlank: false + }, + { + xtype: 'displayfield', + value: gettext('Additional monitors are recommended. They can be created at any time in the Monitor tab.'), + userCls: 'pve-hint' + } + ], + advancedColumn1: [ + { + xtype: 'numberfield', + name: 'size', + fieldLabel: 'Number of replicas', + bind: { + value: '{replicas}' + }, + maxValue: 7, + minValue: 2, + emptyText: '3' + }, + { + xtype: 'numberfield', + name: 'min_size', + fieldLabel: 'Minimum replicas', + bind: { + maxValue: '{replicas}', + value: '{minreplicas}' + }, + minValue: 2, + maxValue: 3, + setMaxValue: function(value) { + this.maxValue = Ext.Number.from(value, 2); + // allow enough to avoid split brains with max 'size', but more makes simply no sense + if (this.maxValue > 4) { + this.maxValue = 4; + } + this.toggleSpinners(); + this.validate(); + }, + emptyText: '2' + } + ], + onGetValues: function(values) { + ['cluster-network', 'size', 'min_size'].forEach(function(field) { + if (!values[field]) { + delete values[field]; + } + }); + return values; + }, + onSubmit: function() { + var me = this; + if (!this.up('pveCephInstallWizard').getViewModel().get('configuration')) { + var wizard = me.up('window'); + var kv = wizard.getValues(); + delete kv['delete']; + var monNode = kv['mon-node']; + delete kv['mon-node']; + var nodename = me.nodename; + delete kv.nodename; + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/ceph/init', + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function() { + Proxmox.Utils.API2Request({ + url: '/nodes/' + monNode + '/ceph/mon/' + monNode, + waitMsgTarget: wizard, + method: 'POST', + success: function() { + me.up('pveCephInstallWizard').navigateNext(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + + } else { + me.up('pveCephInstallWizard').navigateNext(); + } + } + }, + { + title: gettext('Success'), + xtype: 'panel', + border: false, + bodyBorder: false, + onlineHelp: 'pve_ceph_install', + html: '

Installation successful!

'+ + '

The basic installation and configuration is completed, depending on your setup some of the following steps are required to start using Ceph:

'+ + '
  1. Install Ceph on other nodes
  2. '+ + '
  3. Create additional Ceph Monitors
  4. '+ + '
  5. Create Ceph OSDs
  6. '+ + '
  7. Create Ceph Pools
'+ + '

To learn more click on the help button below.

', + listeners: { + activate: function() { + // notify owning container that it should display a help button + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp); + } + + var tp = this.up('#wizcontent'); + var idx = tp.items.indexOf(this)-1; + for(;idx >= 0;idx--) { + var nc = tp.items.getAt(idx); + if (nc) { + nc.disable(); + } + } + }, + deactivate: function() { + if (this.onlineHelp) { + Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp); + } + } + }, + onSubmit: function() { + var wizard = this.up('pveCephInstallWizard'); + wizard.close(); + } + } + ] + }); +Ext.define('PVE.node.DiskList', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveNodeDiskList', + + emptyText: gettext('No Disks found'), + + stateful: true, + stateId: 'grid-node-disks', + + columns: [ + { + header: gettext('Device'), + width: 150, + sortable: true, + dataIndex: 'devpath' + }, + { + header: gettext('Type'), + width: 80, + sortable: true, + dataIndex: 'type', + renderer: function(v) { + if (v === 'ssd') { + return 'SSD'; + } else if (v === 'hdd') { + return 'Hard Disk'; + } else if (v === 'usb'){ + return 'USB'; + } else { + return gettext('Unknown'); + } + } + }, + { + header: gettext('Usage'), + width: 150, + sortable: false, + renderer: function(v, metaData, rec) { + if (rec) { + if (rec.data.osdid >= 0) { + var bluestore = ''; + if (rec.data.bluestore === 1) { + bluestore = ' (Bluestore)'; + } + return "Ceph osd." + rec.data.osdid.toString() + bluestore; + } + + var types = []; + if (rec.data.journals > 0) { + types.push('Journal'); + } + + if (rec.data.db > 0) { + types.push('DB'); + } + + if (rec.data.wal > 0) { + types.push('WAL'); + } + + if (types.length > 0) { + return 'Ceph (' + types.join(', ') + ')'; + } + } + + return v || Proxmox.Utils.noText; + }, + dataIndex: 'used' + }, + { + header: gettext('Size'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + }, + { + header: 'GPT', + width: 60, + align: 'right', + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'gpt' + }, + { + header: gettext('Vendor'), + width: 100, + sortable: true, + hidden: true, + renderer: Ext.String.htmlEncode, + dataIndex: 'vendor' + }, + { + header: gettext('Model'), + width: 200, + sortable: true, + renderer: Ext.String.htmlEncode, + dataIndex: 'model' + }, + { + header: gettext('Serial'), + width: 200, + sortable: true, + renderer: Ext.String.htmlEncode, + dataIndex: 'serial' + }, + { + header: 'S.M.A.R.T.', + width: 100, + sortable: true, + renderer: Ext.String.htmlEncode, + dataIndex: 'health' + }, + { + header: 'Wearout', + width: 90, + sortable: true, + align: 'right', + dataIndex: 'wearout', + renderer: function(value) { + if (Ext.isNumeric(value)) { + return (100 - value).toString() + '%'; + } + return 'N/A'; + } + } + ], + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var store = Ext.create('Ext.data.Store', { + storeid: 'node-disk-list' + nodename, + model: 'node-disk-list', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/disks/list" + }, + sorters: [ + { + property : 'dev', + direction: 'ASC' + } + ] + }); + + var reloadButton = Ext.create('Proxmox.button.Button', { + text: gettext('Reload'), + handler: function() { + me.store.load(); + } + }); + + var smartButton = Ext.create('Proxmox.button.Button', { + text: gettext('Show S.M.A.R.T. values'), + selModel: sm, + enableFn: function() { + return !!sm.getSelection().length; + }, + disabled: true, + handler: function() { + var rec = sm.getSelection()[0]; + + var win = Ext.create('PVE.DiskSmartWindow', { + nodename: nodename, + dev: rec.data.devpath + }); + win.show(); + } + }); + + var initButton = Ext.create('Proxmox.button.Button', { + text: gettext('Initialize Disk with GPT'), + selModel: sm, + enableFn: function() { + var selection = sm.getSelection(); + + if (!selection.length || selection[0].data.used) { + return false; + } else { + return true; + } + }, + disabled: true, + + handler: function() { + var rec = sm.getSelection()[0]; + Proxmox.Utils.API2Request({ + url: '/api2/extjs/nodes/' + nodename + '/disks/initgpt', + waitMsgTarget: me, + method: 'POST', + params: { disk: rec.data.devpath}, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid + }); + win.show(); + } + }); + } + }); + + me.loadCount = 1; // avoid duplicate loadmask + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ reloadButton, smartButton, initButton ], + listeners: { + itemdblclick: function() { + var rec = sm.getSelection()[0]; + + var win = Ext.create('PVE.DiskSmartWindow', { + nodename: nodename, + dev: rec.data.devpath + }); + win.show(); + } + } + }); + + + me.callParent(); + me.store.load(); + } +}, function() { + + Ext.define('node-disk-list', { + extend: 'Ext.data.Model', + fields: [ 'devpath', 'used', { name: 'size', type: 'number'}, + {name: 'osdid', type: 'number'}, + 'vendor', 'model', 'serial', 'rpm', 'type', 'health', 'wearout' ], + idProperty: 'devpath' + }); +}); + +Ext.define('PVE.DiskSmartWindow', { + extend: 'Ext.window.Window', + alias: 'widget.pveSmartWindow', + + modal: true, + + items: [ + { + xtype: 'gridpanel', + layout: { + type: 'fit' + }, + emptyText: gettext('No S.M.A.R.T. Values'), + scrollable: true, + flex: 1, + itemId: 'smarts', + reserveScrollbar: true, + columns: [ + { text: 'ID', dataIndex: 'id', width: 50 }, + { text: gettext('Attribute'), flex: 1, dataIndex: 'name', renderer: Ext.String.htmlEncode }, + { text: gettext('Value'), dataIndex: 'raw', renderer: Ext.String.htmlEncode }, + { text: gettext('Normalized'), dataIndex: 'value', width: 60}, + { text: gettext('Threshold'), dataIndex: 'threshold', width: 60}, + { text: gettext('Worst'), dataIndex: 'worst', width: 60}, + { text: gettext('Flags'), dataIndex: 'flags'}, + { text: gettext('Failing'), dataIndex: 'fail', renderer: Ext.String.htmlEncode } + ] + }, + { + xtype: 'component', + itemId: 'text', + layout: { + type: 'fit' + }, + hidden: true, + style: { + 'background-color': 'white', + 'white-space': 'pre', + 'font-family': 'monospace' + } + } + ], + + buttons: [ + { + text: gettext('Reload'), + name: 'reload', + handler: function() { + var me = this; + me.up('window').store.reload(); + } + }, + { + text: gettext('Close'), + name: 'close', + handler: function() { + var me = this; + me.up('window').close(); + } + } + ], + + layout: { + type: 'vbox', + align: 'stretch' + }, + width: 800, + height: 500, + minWidth: 600, + minHeight: 400, + bodyPadding: 5, + title: gettext('S.M.A.R.T. Values'), + + initComponent: function() { + var me = this; + + var nodename = me.nodename; + if (!nodename) { + throw "no node name specified"; + } + + var dev = me.dev; + if (!dev) { + throw "no device specified"; + } + + me.store = Ext.create('Ext.data.Store', { + model: 'disk-smart', + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + nodename + "/disks/smart?disk=" + dev + } + }); + + me.callParent(); + var grid = me.down('#smarts'); + var text = me.down('#text'); + + Proxmox.Utils.monStoreErrors(grid, me.store); + me.mon(me.store, 'load', function(s, records, success) { + if (success && records.length > 0) { + var rec = records[0]; + switch (rec.data.type) { + case 'text': + grid.setVisible(false); + text.setVisible(true); + text.setHtml(Ext.String.htmlEncode(rec.data.text)); + break; + default: + // includes 'ata' + // cannot use empty case because + // of jslint + grid.setVisible(true); + text.setVisible(false); + grid.setStore(rec.attributes()); + break; + } + } + }); + + me.store.load(); + } +}, function() { + + Ext.define('disk-smart', { + extend: 'Ext.data.Model', + fields: [ + { name:'health'}, + { name:'type'}, + { name:'text'} + ], + hasMany: {model: 'smart-attribute', name: 'attributes'} + }); + Ext.define('smart-attribute', { + extend: 'Ext.data.Model', + fields: [ + { name:'id', type:'number' }, 'name', 'value', 'worst', 'threshold', 'flags', 'fail', 'raw' + ] + }); +}); +Ext.define('PVE.node.CreateLVM', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateLVM', + + subject: 'LVM Volume Group', + + showProgress: true, + + onlineHelp: 'chapter_lvm', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/disks/lvm", + method: 'POST', + items: [ + { + xtype: 'pveDiskSelector', + name: 'device', + nodename: me.nodename, + diskType: 'unused', + fieldLabel: gettext('Disk'), + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1' + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.LVMList', { + extend: 'Ext.tree.Panel', + xtype: 'pveLVMList', + emptyText: gettext('No Volume Groups found'), + stateful: true, + stateId: 'grid-node-lvm', + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + text: gettext('Number of LVs'), + dataIndex: 'lvcount', + width: 150, + align: 'right' + }, + { + header: gettext('Usage'), + width: 110, + dataIndex: 'usage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Size'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + }, + { + header: gettext('Free'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'free' + } + ], + + rootVisible: false, + useArrows: true, + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + var me = this.up('panel'); + me.reload(); + } + }, + { + text: gettext('Create') + ': Volume Group', + handler: function() { + var me = this.up('panel'); + var win = Ext.create('PVE.node.CreateLVM', { + nodename: me.nodename, + taskDone: function() { + me.reload(); + } + }).show(); + } + } + ], + + reload: function() { + var me = this; + var sm = me.getSelectionModel(); + Proxmox.Utils.API2Request({ + url: "/nodes/" + me.nodename + "/disks/lvm", + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + }, + success: function(response, opts) { + sm.deselectAll(); + me.setRootNode(response.result.data); + me.expandAll(); + } + }); + }, + + listeners: { + activate: function() { + var me = this; + me.reload(); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + var sm = Ext.create('Ext.selection.TreeModel', {}); + + Ext.apply(me, { + selModel: sm, + fields: ['name', 'size', 'free', + { + type: 'string', + name: 'iconCls', + calculate: function(data) { + var txt = 'fa x-fa-tree fa-'; + txt += (data.leaf) ? 'hdd-o' : 'object-group'; + return txt; + } + }, + { + type: 'number', + name: 'usage', + calculate: function(data) { + return ((data.size-data.free)/data.size); + } + } + ], + sorters: 'name' + }); + + me.callParent(); + + me.reload(); + } +}); + +Ext.define('PVE.node.CreateLVMThin', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateLVMThin', + + subject: 'LVM Thinpool', + + showProgress: true, + + onlineHelp: 'chapter_lvm', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/disks/lvmthin", + method: 'POST', + items: [ + { + xtype: 'pveDiskSelector', + name: 'device', + nodename: me.nodename, + diskType: 'unused', + fieldLabel: gettext('Disk'), + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1' + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.LVMThinList', { + extend: 'Ext.grid.Panel', + xtype: 'pveLVMThinList', + + emptyText: gettext('No thinpools found'), + stateful: true, + stateId: 'grid-node-lvmthin', + columns: [ + { + text: gettext('Name'), + dataIndex: 'lv', + flex: 1 + }, + { + header: gettext('Usage'), + width: 110, + dataIndex: 'usage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Size'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'lv_size' + }, + { + header: gettext('Used'), + width: 100, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'used' + }, + { + header: gettext('Metadata Usage'), + width: 120, + dataIndex: 'metadata_usage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Metadata Size'), + width: 120, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'metadata_size' + }, + { + header: gettext('Metadata Used'), + width: 125, + align: 'right', + sortable: true, + renderer: Proxmox.Utils.format_size, + dataIndex: 'metadata_used' + } + ], + + rootVisible: false, + useArrows: true, + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + var me = this.up('panel'); + me.reload(); + } + }, + { + text: gettext('Create') + ': Thinpool', + handler: function() { + var me = this.up('panel'); + var win = Ext.create('PVE.node.CreateLVMThin', { + nodename: me.nodename, + taskDone: function() { + me.reload(); + } + }).show(); + } + } + ], + + reload: function() { + var me = this; + me.store.load(); + me.store.sort(); + }, + + listeners: { + activate: function() { + var me = this; + me.reload(); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + store: { + fields: ['lv', 'lv_size', 'used', 'metadata_size', 'metadata_used', + { + type: 'number', + name: 'usage', + calculate: function(data) { + return data.used/data.lv_size; + } + }, + { + type: 'number', + name: 'metadata_usage', + calculate: function(data) { + return data.metadata_used/data.metadata_size; + } + } + ], + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + '/disks/lvmthin' + }, + sorters: 'lv' + } + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore(), true); + me.reload(); + } +}); + +Ext.define('PVE.node.CreateDirectory', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateDirectory', + + subject: Proxmox.Utils.directoryText, + + showProgress: true, + + onlineHelp: 'chapter_storage', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + Ext.applyIf(me, { + url: "/nodes/" + me.nodename + "/disks/directory", + method: 'POST', + items: [ + { + xtype: 'pveDiskSelector', + name: 'device', + nodename: me.nodename, + diskType: 'unused', + fieldLabel: gettext('Disk'), + allowBlank: false + }, + { + xtype: 'proxmoxKVComboBox', + comboItems: [ + ['ext4', 'ext4'], + ['xfs', 'xfs'] + ], + fieldLabel: gettext('Filesystem'), + name: 'filesystem', + value: '', + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1' + } + ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.node.Directorylist', { + extend: 'Ext.grid.Panel', + xtype: 'pveDirectoryList', + + stateful: true, + stateId: 'grid-node-directory', + columns: [ + { + text: gettext('Path'), + dataIndex: 'path', + flex: 1 + }, + { + header: gettext('Device'), + flex: 1, + dataIndex: 'device' + }, + { + header: gettext('Type'), + width: 100, + dataIndex: 'type' + }, + { + header: gettext('Options'), + width: 100, + dataIndex: 'options' + }, + { + header: gettext('Unit File'), + hidden: true, + dataIndex: 'unitfile' + } + ], + + rootVisible: false, + useArrows: true, + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + var me = this.up('panel'); + me.reload(); + } + }, + { + text: gettext('Create') + ': Directory', + handler: function() { + var me = this.up('panel'); + var win = Ext.create('PVE.node.CreateDirectory', { + nodename: me.nodename + }).show(); + win.on('destroy', function() { me.reload(); }); + } + } + ], + + reload: function() { + var me = this; + me.store.load(); + me.store.sort(); + }, + + listeners: { + activate: function() { + var me = this; + me.reload(); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + store: { + fields: ['path', 'device', 'type', 'options', 'unitfile' ], + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + '/disks/directory' + }, + sorters: 'path' + } + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore(), true); + me.reload(); + } +}); + +/*jslint confusion: true*/ +Ext.define('PVE.node.CreateZFS', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCreateZFS', + + subject: 'ZFS', + + showProgress: true, + + onlineHelp: 'chapter_zfs', + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + me.isCreate = true; + + var update_disklist = function() { + var grid = me.down('#disklist'); + var disks = grid.getSelection(); + + var val = []; + disks.sort(function(a,b) { + var aorder = a.get('order') || 0; + var border = b.get('order') || 0; + return (aorder - border); + }); + + disks.forEach(function(disk) { + val.push(disk.get('devpath')); + }); + + me.down('field[name=devices]').setValue(val.join(',')); + }; + + Ext.apply(me, { + url: '/nodes/' + me.nodename + '/disks/zfs', + method: 'POST', + items: [ + { + xtype: 'inputpanel', + onGetValues: function(values) { + return values; + }, + column1: [ + { + xtype: 'textfield', + hidden: true, + name: 'devices', + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'add_storage', + fieldLabel: gettext('Add Storage'), + value: '1' + } + ], + column2: [ + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('RAID Level'), + name: 'raidlevel', + value: 'single', + comboItems: [ + ['single', gettext('Single Disk')], + ['mirror', 'Mirror'], + ['raid10', 'RAID10'], + ['raidz', 'RAIDZ'], + ['raidz2', 'RAIDZ2'], + ['raidz3', 'RAIDZ3'] + ] + }, + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Compression'), + name: 'compression', + value: 'on', + comboItems: [ + ['on', 'on'], + ['off', 'off'], + ['gzip', 'gzip'], + ['lz4', 'lz4'], + ['lzjb', 'lzjb'], + ['zle', 'zle'] + ] + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('ashift'), + minValue: 9, + maxValue: 16, + value: '12', + name: 'ashift' + } + ], + columnB: [ + { + xtype: 'grid', + height: 200, + emptyText: gettext('No Disks unused'), + itemId: 'disklist', + selModel: 'checkboxmodel', + listeners: { + selectionchange: update_disklist + }, + store: { + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/disks/list?type=unused' + } + }, + columns: [ + { + text: gettext('Device'), + dataIndex: 'devpath', + flex: 1 + }, + { + text: gettext('Serial'), + dataIndex: 'serial' + }, + { + text: gettext('Size'), + dataIndex: 'size', + renderer: PVE.Utils.render_size + }, + { + header: gettext('Order'), + xtype: 'widgetcolumn', + dataIndex: 'order', + sortable: true, + widget: { + xtype: 'proxmoxintegerfield', + minValue: 1, + isFormField: false, + listeners: { + change: function(numberfield, value, old_value) { + var record = numberfield.getWidgetRecord(); + record.set('order', value); + update_disklist(record); + } + } + } + } + ] + } + ] + }, + { + xtype: 'displayfield', + padding: '5 0 0 0', + userCls: 'pve-hint', + value: 'Note: ZFS is not compatible with disks backed by a hardware ' + + 'RAID controller. For details see ' + + 'the reference documentation.', + } + ] + }); + + me.callParent(); + me.down('#disklist').getStore().load(); + } +}); + +Ext.define('PVE.node.ZFSDevices', { + extend: 'Ext.tree.Panel', + xtype: 'pveZFSDevices', + stateful: true, + stateId: 'grid-node-zfsstatus', + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + text: gettext('Health'), + renderer: PVE.Utils.render_zfs_health, + dataIndex: 'state' + }, + { + text: 'READ', + dataIndex: 'read' + }, + { + text: 'WRITE', + dataIndex: 'write' + }, + { + text: 'CKSUM', + dataIndex: 'cksum' + }, + { + text: gettext('Message'), + dataIndex: 'msg' + } + ], + + rootVisible: true, + + reload: function() { + var me = this; + var sm = me.getSelectionModel(); + Proxmox.Utils.API2Request({ + url: "/nodes/" + me.nodename + "/disks/zfs/" + me.zpool, + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + }, + success: function(response, opts) { + sm.deselectAll(); + me.setRootNode(response.result.data); + me.expandAll(); + } + }); + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.zpool) { + throw "no zpool specified"; + } + + var sm = Ext.create('Ext.selection.TreeModel', {}); + + Ext.apply(me, { + selModel: sm, + fields: ['name', 'status', + { + type: 'string', + name: 'iconCls', + calculate: function(data) { + var txt = 'fa x-fa-tree fa-'; + if (data.leaf) { + return txt + 'hdd-o'; + } + } + } + ], + sorters: 'name' + }); + + me.callParent(); + + me.reload(); + } +}); + +Ext.define('PVE.node.ZFSStatus', { + extend: 'Proxmox.grid.ObjectGrid', + xtype: 'pveZFSStatus', + layout: 'fit', + border: false, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.zpool) { + throw "no zpool specified"; + } + + me.url = "/api2/extjs/nodes/" + me.nodename + "/disks/zfs/" + me.zpool; + + me.rows = { + scan: { + header: gettext('Scan') + }, + status: { + header: gettext('Status') + }, + action: { + header: gettext('Action') + }, + errors: { + header: gettext('Errors') + } + }; + + me.callParent(); + me.reload(); + } +}); + +Ext.define('PVE.node.ZFSList', { + extend: 'Ext.grid.Panel', + xtype: 'pveZFSList', + + stateful: true, + stateId: 'grid-node-zfs', + columns: [ + { + text: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + header: gettext('Size'), + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + }, + { + header: gettext('Free'), + renderer: Proxmox.Utils.format_size, + dataIndex: 'free' + }, + { + header: gettext('Allocated'), + renderer: Proxmox.Utils.format_size, + dataIndex: 'alloc' + }, + { + header: gettext('Fragmentation'), + renderer: function(value) { + return value.toString() + '%'; + }, + dataIndex: 'frag' + }, + { + header: gettext('Health'), + renderer: PVE.Utils.render_zfs_health, + dataIndex: 'health' + }, + { + header: gettext('Deduplication'), + hidden: true, + renderer: function(value) { + return value.toFixed(2).toString() + 'x'; + }, + dataIndex: 'dedup' + } + ], + + rootVisible: false, + useArrows: true, + + tbar: [ + { + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + var me = this.up('panel'); + me.reload(); + } + }, + { + text: gettext('Create') + ': ZFS', + handler: function() { + var me = this.up('panel'); + var win = Ext.create('PVE.node.CreateZFS', { + nodename: me.nodename + }).show(); + win.on('destroy', function() { me.reload(); }); + } + }, + { + text: gettext('Detail'), + itemId: 'detailbtn', + disabled: true, + handler: function() { + var me = this.up('panel'); + var selection = me.getSelection(); + if (selection.length < 1) { + return; + } + me.show_detail(selection[0].get('name')); + } + } + ], + + show_detail: function(zpool) { + var me = this; + + var detailsgrid = Ext.create('PVE.node.ZFSStatus', { + layout: 'fit', + nodename: me.nodename, + flex: 0, + zpool: zpool + }); + + var devicetree = Ext.create('PVE.node.ZFSDevices', { + title: gettext('Devices'), + nodename: me.nodename, + flex: 1, + zpool: zpool + }); + + + var win = Ext.create('Ext.window.Window', { + modal: true, + width: 800, + height: 400, + resizable: true, + layout: 'fit', + title: gettext('Status') + ': ' + zpool, + items:[{ + xtype: 'panel', + region: 'center', + layout: { + type: 'vbox', + align: 'stretch' + }, + items: [detailsgrid, devicetree], + tbar: [{ + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: function() { + + devicetree.reload(); + detailsgrid.reload(); + } + }] + }] + }).show(); + }, + + set_button_status: function() { + var me = this; + var selection = me.getSelection(); + me.down('#detailbtn').setDisabled(selection.length === 0); + }, + + reload: function() { + var me = this; + me.store.load(); + me.store.sort(); + }, + + listeners: { + activate: function() { + var me = this; + me.reload(); + }, + selectionchange: function() { + this.set_button_status(); + }, + itemdblclick: function(grid, record) { + var me = this; + me.show_detail(record.get('name')); + } + }, + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.apply(me, { + store: { + fields: ['name', 'size', 'free', 'alloc', 'dedup', 'frag', 'health'], + proxy: { + type: 'proxmox', + url: "/api2/json/nodes/" + me.nodename + '/disks/zfs' + }, + sorters: 'name' + } + }); + + me.callParent(); + + Proxmox.Utils.monStoreErrors(me, me.getStore(), true); + me.reload(); + } +}); + +Ext.define('PVE.node.StatusView', { + extend: 'PVE.panel.StatusView', + alias: 'widget.pveNodeStatus', + + height: 300, + bodyPadding: '20 15 20 15', + + layout: { + type: 'table', + columns: 2, + tableAttrs: { + style: { + width: '100%' + } + } + }, + + defaults: { + xtype: 'pveInfoWidget', + padding: '0 15 5 15' + }, + + items: [ + { + itemId: 'cpu', + iconCls: 'fa fa-fw pve-itype-icon-processor pve-icon', + title: gettext('CPU usage'), + valueField: 'cpu', + maxField: 'cpuinfo', + renderer: PVE.Utils.render_node_cpu_usage + }, + { + itemId: 'wait', + iconCls: 'fa fa-fw fa-clock-o', + title: gettext('IO delay'), + valueField: 'wait', + rowspan: 2 + }, + { + itemId: 'load', + iconCls: 'fa fa-fw fa-tasks', + title: gettext('Load average'), + printBar: false, + textField: 'loadavg' + }, + { + xtype: 'box', + colspan: 2, + padding: '0 0 20 0' + }, + { + iconCls: 'fa fa-fw pve-itype-icon-memory pve-icon', + itemId: 'memory', + title: gettext('RAM usage'), + valueField: 'memory', + maxField: 'memory', + renderer: PVE.Utils.render_node_size_usage + }, + { + itemId: 'ksm', + printBar: false, + title: gettext('KSM sharing'), + textField: 'ksm', + renderer: function(record) { + return PVE.Utils.render_size(record.shared); + }, + padding: '0 15 10 15' + }, + { + iconCls: 'fa fa-fw fa-hdd-o', + itemId: 'rootfs', + title: gettext('HD space') + '(root)', + valueField: 'rootfs', + maxField: 'rootfs', + renderer: PVE.Utils.render_node_size_usage + }, + { + iconCls: 'fa fa-fw fa-refresh', + itemId: 'swap', + printSize: true, + title: gettext('SWAP usage'), + valueField: 'swap', + maxField: 'swap', + renderer: PVE.Utils.render_node_size_usage + }, + { + xtype: 'box', + colspan: 2, + padding: '0 0 20 0' + }, + { + itemId: 'cpus', + colspan: 2, + printBar: false, + title: gettext('CPU(s)'), + textField: 'cpuinfo', + renderer: function(cpuinfo) { + return cpuinfo.cpus + " x " + cpuinfo.model + " (" + + cpuinfo.sockets.toString() + " " + + (cpuinfo.sockets > 1 ? + gettext('Sockets') : + gettext('Socket') + ) + ")"; + }, + value: '' + }, + { + itemId: 'kversion', + colspan: 2, + title: gettext('Kernel Version'), + printBar: false, + textField: 'kversion', + value: '' + }, + { + itemId: 'version', + colspan: 2, + printBar: false, + title: gettext('PVE Manager Version'), + textField: 'pveversion', + value: '' + } + ], + + updateTitle: function() { + var me = this; + var uptime = Proxmox.Utils.render_uptime(me.getRecordValue('uptime')); + me.setTitle(me.pveSelNode.data.node + ' (' + gettext('Uptime') + ': ' + uptime + ')'); + } + +}); +Ext.define('PVE.node.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveNodeSummary', + + scrollable: true, + bodyPadding: 5, + + showVersions: function() { + var me = this; + + // Note: we use simply text/html here, because ExtJS grid has problems + // with cut&paste + + var nodename = me.pveSelNode.data.node; + + var view = Ext.createWidget('component', { + autoScroll: true, + padding: 5, + style: { + 'background-color': 'white', + 'white-space': 'pre', + 'font-family': 'monospace' + } + }); + + var win = Ext.create('Ext.window.Window', { + title: gettext('Package versions'), + width: 600, + height: 400, + layout: 'fit', + modal: true, + items: [ view ] + }); + + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: "/nodes/" + nodename + "/apt/versions", + method: 'GET', + failure: function(response, opts) { + win.close(); + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + win.show(); + var text = ''; + + Ext.Array.each(response.result.data, function(rec) { + var version = "not correctly installed"; + var pkg = rec.Package; + if (rec.OldVersion && rec.CurrentState === 'Installed') { + version = rec.OldVersion; + } + if (rec.RunningKernel) { + text += pkg + ': ' + version + ' (running kernel: ' + + rec.RunningKernel + ')\n'; + } else if (rec.ManagerVersion) { + text += pkg + ': ' + version + ' (running version: ' + + rec.ManagerVersion + ')\n'; + } else { + text += pkg + ': ' + version + '\n'; + } + }); + + view.update(Ext.htmlEncode(text)); + } + }); + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + if (!me.statusStore) { + throw "no status storage specified"; + } + + var rstore = me.statusStore; + + var version_btn = new Ext.Button({ + text: gettext('Package versions'), + handler: function(){ + Proxmox.Utils.checked_command(function() { me.showVersions(); }); + } + }); + + var rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/rrddata", + model: 'pve-rrd-node' + }); + + Ext.apply(me, { + tbar: [version_btn, '->', { xtype: 'proxmoxRRDTypeSelector' } ], + items: [ + { + xtype: 'container', + layout: 'column', + defaults: { + minHeight: 320, + padding: 5, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1 + }, + 'width >= 1900': { + columnWidth: 0.5 + } + } + }, + items: [ + { + xtype: 'pveNodeStatus', + rstore: rstore, + width: 770, + pveSelNode: me.pveSelNode + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('CPU usage'), + fields: ['cpu','iowait'], + fieldTitles: [gettext('CPU usage'), gettext('IO delay')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Server load'), + fields: ['loadavg'], + fieldTitles: [gettext('Load average')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Memory usage'), + fields: ['memtotal','memused'], + fieldTitles: [gettext('Total'), gettext('RAM usage')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Network traffic'), + fields: ['netin','netout'], + store: rrdstore + } + ] + } + ], + listeners: { + activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); }, + destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); } + } + }); + + me.callParent(); + } +}); +/*global Blob*/ +Ext.define('PVE.node.SubscriptionKeyEdit', { + extend: 'Proxmox.window.Edit', + title: gettext('Upload Subscription Key'), + width: 300, + items: { + xtype: 'textfield', + name: 'key', + value: '', + fieldLabel: gettext('Subscription Key') + }, + initComponent : function() { + var me = this; + + me.callParent(); + + me.load(); + } +}); + +Ext.define('PVE.node.Subscription', { + extend: 'Proxmox.grid.ObjectGrid', + + alias: ['widget.pveNodeSubscription'], + + onlineHelp: 'getting_help', + + viewConfig: { + enableTextSelection: true + }, + + showReport: function() { + var me = this; + var nodename = me.pveSelNode.data.node; + + var getReportFileName = function() { + var now = Ext.Date.format(new Date(), 'D-d-F-Y-G-i'); + return me.nodename + '-report-' + now + '.txt'; + }; + + var view = Ext.createWidget('component', { + itemId: 'system-report-view', + scrollable: true, + style: { + 'background-color': 'white', + 'white-space': 'pre', + 'font-family': 'monospace', + padding: '5px' + } + }); + + var reportWindow = Ext.create('Ext.window.Window', { + title: gettext('System Report'), + width: 1024, + height: 600, + layout: 'fit', + modal: true, + buttons: [ + '->', + { + text: gettext('Download'), + handler: function() { + var fileContent = reportWindow.getComponent('system-report-view').html; + var fileName = getReportFileName(); + + // Internet Explorer + if (window.navigator.msSaveOrOpenBlob) { + navigator.msSaveOrOpenBlob(new Blob([fileContent]), fileName); + } else { + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + + encodeURIComponent(fileContent)); + element.setAttribute('download', fileName); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + } + } + ], + items: view + }); + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/nodes/' + me.nodename + '/report', + method: 'GET', + waitMsgTarget: me, + failure: function(response) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response) { + var report = Ext.htmlEncode(response.result.data); + reportWindow.show(); + view.update(report); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var reload = function() { + me.rstore.load(); + }; + + var baseurl = '/nodes/' + me.nodename + '/subscription'; + + var render_status = function(value) { + + var message = me.getObjectValue('message'); + + if (message) { + return value + ": " + message; + } + return value; + }; + + var rows = { + productname: { + header: gettext('Type') + }, + key: { + header: gettext('Subscription Key') + }, + status: { + header: gettext('Status'), + renderer: render_status + }, + message: { + visible: false + }, + serverid: { + header: gettext('Server ID') + }, + sockets: { + header: gettext('Sockets') + }, + checktime: { + header: gettext('Last checked'), + renderer: Proxmox.Utils.render_timestamp + }, + nextduedate: { + header: gettext('Next due date') + } + }; + + Ext.apply(me, { + url: '/api2/json' + baseurl, + cwidth1: 170, + tbar: [ + { + text: gettext('Upload Subscription Key'), + handler: function() { + var win = Ext.create('PVE.node.SubscriptionKeyEdit', { + url: '/api2/extjs/' + baseurl + }); + win.show(); + win.on('destroy', reload); + } + }, + { + text: gettext('Check'), + handler: function() { + Proxmox.Utils.API2Request({ + params: { force: 1 }, + url: baseurl, + method: 'POST', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + callback: reload + }); + } + }, + { + text: gettext('System Report'), + handler: function() { + Proxmox.Utils.checked_command(function (){ me.showReport(); }); + } + } + ], + rows: rows, + listeners: { + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.node.CertificateView', { + extend: 'Ext.container.Container', + xtype: 'pveCertificatesView', + + onlineHelp: 'sysadmin_certificate_management', + + mixins: ['Proxmox.Mixin.CBind' ], + + items: [ + { + xtype: 'pveCertView', + border: 0, + cbind: { + nodename: '{nodename}' + } + }, + { + xtype: 'pveACMEView', + border: 0, + cbind: { + nodename: '{nodename}' + } + } + ] + +}); + +Ext.define('PVE.node.CertificateViewer', { + extend: 'Proxmox.window.Edit', + + title: gettext('Certificate'), + + fieldDefaults: { + labelWidth: 120 + }, + width: 800, + resizable: true, + + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Name'), + name: 'filename' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Fingerprint'), + name: 'fingerprint' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Issuer'), + name: 'issuer' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Subject'), + name: 'subject' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Valid Since'), + renderer: Proxmox.Utils.render_timestamp, + name: 'notbefore' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Expires'), + renderer: Proxmox.Utils.render_timestamp, + name: 'notafter' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Subject Alternative Names'), + name: 'san', + renderer: PVE.Utils.render_san + }, + { + xtype: 'textarea', + editable: false, + grow: true, + growMax: 200, + fieldLabel: gettext('Certificate'), + name: 'pem' + } + ], + + initComponent: function() { + var me = this; + + if (!me.cert) { + throw "no cert given"; + } + + if (!me.nodename) { + throw "no nodename given"; + } + + me.url = '/nodes/' + me.nodename + '/certificates/info'; + me.callParent(); + + // hide OK/Reset button, because we just want to show data + me.down('toolbar[dock=bottom]').setVisible(false); + + me.load({ + success: function(response) { + if (Ext.isArray(response.result.data)) { + Ext.Array.each(response.result.data, function(item) { + if (item.filename === me.cert) { + me.setValues(item); + return false; + } + }); + } + } + }); + } +}); + +Ext.define('PVE.node.CertUpload', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCertUpload', + + title: gettext('Upload Custom Certificate'), + resizable: false, + isCreate: true, + submitText: gettext('Upload'), + method: 'POST', + width: 600, + + apiCallDone: function(success, response, options) { + if (!success) { + return; + } + + var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); + Ext.getBody().mask(txt, ['pve-static-mask']); + // reload after 10 seconds automatically + Ext.defer(function() { + window.location.reload(true); + }, 10000); + }, + + items: [ + { + fieldLabel: gettext('Private Key (Optional)'), + labelAlign: 'top', + emptyText: gettext('No change'), + name: 'key', + xtype: 'textarea' + }, + { + xtype: 'filebutton', + text: gettext('From File'), + listeners: { + change: function(btn, e, value) { + var me = this.up('form'); + e = e.event; + Ext.Array.each(e.target.files, function(file) { + PVE.Utils.loadSSHKeyFromFile(file, function(res) { + me.down('field[name=key]').setValue(res); + }); + }); + btn.reset(); + } + } + }, + { + xtype: 'box', + autoEl: 'hr' + }, + { + fieldLabel: gettext('Certificate Chain'), + labelAlign: 'top', + allowBlank: false, + name: 'certificates', + xtype: 'textarea' + }, + { + xtype: 'filebutton', + text: gettext('From File'), + listeners: { + change: function(btn, e, value) { + var me = this.up('form'); + e = e.event; + Ext.Array.each(e.target.files, function(file) { + PVE.Utils.loadSSHKeyFromFile(file, function(res) { + me.down('field[name=certificates]').setValue(res); + }); + }); + btn.reset(); + } + } + }, + { + xtype: 'hidden', + name: 'restart', + value: '1' + }, + { + xtype: 'hidden', + name: 'force', + value: '1' + } + ], + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + me.url = '/nodes/' + me.nodename + '/certificates/custom'; + + me.callParent(); + } +}); + +Ext.define('pve-certificate', { + extend: 'Ext.data.Model', + + fields: [ 'filename', 'fingerprint', 'issuer', 'notafter', 'notbefore', 'subject', 'san' ], + idProperty: 'filename' +}); + +Ext.define('PVE.node.Certificates', { + extend: 'Ext.grid.Panel', + xtype: 'pveCertView', + + tbar: [ + { + xtype: 'button', + text: gettext('Upload Custom Certificate'), + handler: function() { + var me = this.up('grid'); + var win = Ext.create('PVE.node.CertUpload', { + nodename: me.nodename + }); + win.show(); + win.on('destroy', me.reload, me); + } + }, + { + xtype: 'button', + itemId: 'deletebtn', + text: gettext('Delete Custom Certificate'), + handler: function() { + var me = this.up('grid'); + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/certificates/custom?restart=1', + method: 'DELETE', + success: function(response, opt) { + var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); + Ext.getBody().mask(txt, ['pve-static-mask']); + // reload after 10 seconds automatically + Ext.defer(function() { + window.location.reload(true); + }, 10000); + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }, + '-', + { + xtype: 'proxmoxButton', + itemId: 'viewbtn', + disabled: true, + text: gettext('View Certificate'), + handler: function() { + var me = this.up('grid'); + me.view_certificate(); + } + } + ], + + columns: [ + { + header: gettext('File'), + width: 150, + dataIndex: 'filename' + }, + { + header: gettext('Issuer'), + flex: 1, + dataIndex: 'issuer' + }, + { + header: gettext('Subject'), + flex: 1, + dataIndex: 'subject' + }, + { + header: gettext('Valid Since'), + width: 150, + dataIndex: 'notbefore', + renderer: Proxmox.Utils.render_timestamp + }, + { + header: gettext('Expires'), + width: 150, + dataIndex: 'notafter', + renderer: Proxmox.Utils.render_timestamp + }, + { + header: gettext('Subject Alternative Names'), + flex: 1, + dataIndex: 'san', + renderer: PVE.Utils.render_san + }, + { + header: gettext('Fingerprint'), + dataIndex: 'fingerprint', + hidden: true + }, + { + header: gettext('PEM'), + dataIndex: 'pem', + hidden: true + } + ], + + reload: function() { + var me = this; + me.rstore.load(); + }, + + set_button_status: function() { + var me = this; + var rec = me.rstore.getById('pveproxy-ssl.pem'); + + me.down('#deletebtn').setDisabled(!rec); + }, + + view_certificate: function() { + var me = this; + var selection = me.getSelection(); + if (!selection || selection.length < 1) { + return; + } + var win = Ext.create('PVE.node.CertificateViewer', { + cert: selection[0].data.filename, + nodename : me.nodename + }); + win.show(); + }, + + listeners: { + itemdblclick: 'view_certificate' + }, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + me.rstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'certs-' + me.nodename, + model: 'pve-certificate', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/certificates/info' + } + }); + + me.store = { + type: 'diff', + rstore: me.rstore + }; + + me.callParent(); + + me.mon(me.rstore, 'load', me.set_button_status, me); + me.rstore.startUpdate(); + } +}); +Ext.define('PVE.node.ACMEEditor', { + extend: 'Proxmox.window.Edit', + xtype: 'pveACMEEditor', + + subject: gettext('Domains'), + items: [ + { + xtype: 'inputpanel', + items: [ + { + xtype: 'textarea', + fieldLabel: gettext('Domains'), + emptyText: "domain1.example.com\ndomain2.example.com", + name: 'domains' + } + ], + onGetValues: function(values) { + if (!values.domains) { + return { + 'delete': 'acme' + }; + } + var domains = values.domains.split(/\n/).join(';'); + return { + 'acme': 'domains=' + domains + }; + } + } + ], + + initComponent: function() { + var me = this; + me.callParent(); + + me.load({ + success: function(response, opts) { + var res = PVE.Parser.parseACME(response.result.data.acme); + if (res) { + res.domains = res.domains.join(' '); + me.setValues(res); + } + } + }); + } +}); + +Ext.define('PVE.node.ACMEAccountCreate', { + extend: 'Proxmox.window.Edit', + + width: 400, + title: gettext('Register Account'), + isCreate: true, + method: 'POST', + submitText: gettext('Register'), + url: '/cluster/acme/account', + showTaskViewer: true, + + items: [ + { + xtype: 'proxmoxComboGrid', + name: 'directory', + allowBlank: false, + valueField: 'url', + displayField: 'name', + fieldLabel: gettext('ACME Directory'), + store: { + autoLoad: true, + fields: ['name', 'url'], + idProperty: ['name'], + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/acme/directories' + }, + sorters: { + property: 'name', + order: 'ASC' + } + }, + listConfig: { + columns: [ + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + header: gettext('URL'), + dataIndex: 'url', + flex: 1 + } + ] + }, + listeners: { + change: function(combogrid, value) { + var me = this; + if (!value) { + return; + } + + var disp = me.up('window').down('#tos_url_display'); + var field = me.up('window').down('#tos_url'); + var checkbox = me.up('window').down('#tos_checkbox'); + + disp.setValue(gettext('Loading')); + field.setValue(undefined); + checkbox.setValue(undefined); + + Proxmox.Utils.API2Request({ + url: '/cluster/acme/tos', + method: 'GET', + params: { + directory: value + }, + success: function(response, opt) { + me.up('window').down('#tos_url').setValue(response.result.data); + me.up('window').down('#tos_url_display').setValue(response.result.data); + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + } + }, + { + xtype: 'displayfield', + itemId: 'tos_url_display', + fieldLabel: gettext('Terms of Service'), + renderer: PVE.Utils.render_optional_url, + name: 'tos_url_display' + }, + { + xtype: 'hidden', + itemId: 'tos_url', + name: 'tos_url' + }, + { + xtype: 'proxmoxcheckbox', + itemId: 'tos_checkbox', + fieldLabel: gettext('Accept TOS'), + submitValue: false, + validateValue: function(value) { + if (value && this.checked) { + return true; + } + return false; + } + }, + { + xtype: 'textfield', + name: 'contact', + vtype: 'email', + allowBlank: false, + fieldLabel: gettext('E-Mail') + } + ] + +}); + +Ext.define('PVE.node.ACMEAccountView', { + extend: 'Proxmox.window.Edit', + + width: 600, + fieldDefaults: { + labelWidth: 140 + }, + + title: gettext('Account'), + + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('E-Mail'), + name: 'email' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Created'), + name: 'createdAt' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Status'), + name: 'status' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Directory'), + renderer: PVE.Utils.render_optional_url, + name: 'directory' + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Terms of Services'), + renderer: PVE.Utils.render_optional_url, + name: 'tos' + } + ], + + initComponent: function() { + var me = this; + + if (!me.accountname) { + throw "no account name defined"; + } + + me.url = '/cluster/acme/account/' + me.accountname; + + me.callParent(); + + // hide OK/Reset button, because we just want to show data + me.down('toolbar[dock=bottom]').setVisible(false); + + me.load({ + success: function(response) { + var data = response.result.data; + data.email = data.account.contact[0]; + data.createdAt = data.account.createdAt; + data.status = data.account.status; + me.setValues(data); + } + }); + } +}); + +Ext.define('PVE.node.ACME', { + extend: 'Proxmox.grid.ObjectGrid', + xtype: 'pveACMEView', + + margin: '10 0 0 0', + title: 'ACME', + + tbar: [ + { + xtype: 'button', + itemId: 'edit', + text: gettext('Edit Domains'), + handler: function() { + this.up('grid').run_editor(); + } + }, + { + xtype: 'button', + itemId: 'createaccount', + text: gettext('Register Account'), + handler: function() { + var me = this.up('grid'); + var win = Ext.create('PVE.node.ACMEAccountCreate', { + taskDone: function() { + me.load_account(); + me.reload(); + } + }); + win.show(); + } + }, + { + xtype: 'button', + itemId: 'viewaccount', + text: gettext('View Account'), + handler: function() { + var me = this.up('grid'); + var win = Ext.create('PVE.node.ACMEAccountView', { + accountname: 'default' + }); + win.show(); + } + }, + { + xtype: 'button', + itemId: 'order', + text: gettext('Order Certificate'), + handler: function() { + var me = this.up('grid'); + + Proxmox.Utils.API2Request({ + method: 'POST', + params: { + force: 1 + }, + url: '/nodes/' + me.nodename + '/certificates/acme/certificate', + success: function(response, opt) { + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: response.result.data, + taskDone: function(success) { + me.certificate_order_finished(success); + } + }); + win.show(); + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + } + ], + + certificate_order_finished: function(success) { + if (!success) { + return; + } + var txt = gettext('pveproxy will be restarted with new certificates, please reload the GUI!'); + Ext.getBody().mask(txt, ['pve-static-mask']); + // reload after 10 seconds automatically + Ext.defer(function() { + window.location.reload(true); + }, 10000); + }, + + set_button_status: function() { + var me = this; + + var account = !!me.account; + var acmeObj = PVE.Parser.parseACME(me.getObjectValue('acme')); + var domains = acmeObj ? acmeObj.domains.length : 0; + + var order = me.down('#order'); + order.setVisible(account); + order.setDisabled(!account || !domains); + + me.down('#createaccount').setVisible(!account); + me.down('#viewaccount').setVisible(account); + }, + + load_account: function() { + var me = this; + + // for now we only use the 'default' account + Proxmox.Utils.API2Request({ + url: '/cluster/acme/account/default', + success: function(response, opt) { + me.account = response.result.data; + me.set_button_status(); + }, + failure: function(response, opt) { + me.account = undefined; + me.set_button_status(); + } + }); + }, + + run_editor: function() { + var me = this; + var win = Ext.create(me.rows.acme.editor, me.editorConfig); + win.show(); + win.on('destroy', me.reload, me); + }, + + listeners: { + itemdblclick: 'run_editor' + }, + + // account data gets loaded here + account: undefined, + + disableSelection: true, + + initComponent: function() { + var me = this; + + if (!me.nodename) { + throw "no nodename given"; + } + + me.url = '/api2/json/nodes/' + me.nodename + '/config'; + + me.editorConfig = { + url: '/api2/extjs/nodes/' + me.nodename + '/config' + }; + /*jslint confusion: true*/ + /*acme is a string above*/ + me.rows = { + acme: { + defaultValue: '', + header: gettext('Domains'), + editor: 'PVE.node.ACMEEditor', + renderer: function(value) { + var acmeObj = PVE.Parser.parseACME(value); + if (acmeObj) { + return acmeObj.domains.join('
'); + } + return Proxmox.Utils.noneText; + } + } + }; + /*jslint confusion: false*/ + + me.callParent(); + me.mon(me.rstore, 'load', me.set_button_status, me); + me.rstore.startUpdate(); + me.load_account(); + } +}); +Ext.define('PVE.node.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.node.Config', + + onlineHelp: 'chapter_system_administration', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + nodename + "/status", + interval: 1000 + }); + + var node_command = function(cmd) { + Proxmox.Utils.API2Request({ + params: { command: cmd }, + url: '/nodes/' + nodename + '/status', + method: 'POST', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + var actionBtn = Ext.create('Ext.Button', { + text: gettext('Bulk Actions'), + iconCls: 'fa fa-fw fa-ellipsis-v', + disabled: !caps.nodes['Sys.PowerMgmt'], + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Bulk Start'), + iconCls: 'fa fa-fw fa-play', + handler: function() { + var win = Ext.create('PVE.window.BulkAction', { + nodename: nodename, + title: gettext('Bulk Start'), + btnText: gettext('Start'), + action: 'startall' + }); + win.show(); + } + }, + { + text: gettext('Bulk Stop'), + iconCls: 'fa fa-fw fa-stop', + handler: function() { + var win = Ext.create('PVE.window.BulkAction', { + nodename: nodename, + title: gettext('Bulk Stop'), + btnText: gettext('Stop'), + action: 'stopall' + }); + win.show(); + } + }, + { + text: gettext('Bulk Migrate'), + iconCls: 'fa fa-fw fa-send-o', + handler: function() { + var win = Ext.create('PVE.window.BulkAction', { + nodename: nodename, + title: gettext('Bulk Migrate'), + btnText: gettext('Migrate'), + action: 'migrateall' + }); + win.show(); + } + } + ] + }) + }); + + var restartBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Reboot'), + disabled: !caps.nodes['Sys.PowerMgmt'], + dangerous: true, + confirmMsg: Ext.String.format(gettext("Reboot node '{0}'?"), nodename), + handler: function() { + node_command('reboot'); + }, + iconCls: 'fa fa-undo' + }); + + var shutdownBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Shutdown'), + disabled: !caps.nodes['Sys.PowerMgmt'], + dangerous: true, + confirmMsg: Ext.String.format(gettext("Shutdown node '{0}'?"), nodename), + handler: function() { + node_command('shutdown'); + }, + iconCls: 'fa fa-power-off' + }); + + var shellBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.nodes['Sys.Console'], + text: gettext('Shell'), + consoleType: 'shell', + nodename: nodename + }); + + me.items = []; + + Ext.apply(me, { + title: gettext('Node') + " '" + nodename + "'", + hstateid: 'nodetab', + defaults: { statusStore: me.statusStore }, + tbar: [ restartBtn, shutdownBtn, shellBtn, actionBtn] + }); + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + title: gettext('Summary'), + iconCls: 'fa fa-book', + itemId: 'summary', + xtype: 'pveNodeSummary' + }, + { + title: gettext('Notes'), + iconCls: 'fa fa-sticky-note-o', + itemId: 'notes', + xtype: 'pveNotesView' + } + ); + } + + if (caps.nodes['Sys.Console']) { + me.items.push( + { + title: gettext('Shell'), + iconCls: 'fa fa-terminal', + itemId: 'jsconsole', + xtype: 'pveNoVncConsole', + consoleType: 'shell', + xtermjs: true, + nodename: nodename + } + ); + } + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + title: gettext('System'), + iconCls: 'fa fa-cogs', + itemId: 'services', + expandedOnInit: true, + startOnlyServices: { + 'pveproxy': true, + 'pvedaemon': true, + 'pve-cluster': true + }, + nodename: nodename, + onlineHelp: 'pve_service_daemons', + xtype: 'proxmoxNodeServiceView' + }, + { + title: gettext('Network'), + iconCls: 'fa fa-exchange', + itemId: 'network', + groups: ['services'], + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + xtype: 'proxmoxNodeNetworkView' + }, + { + title: gettext('Certificates'), + iconCls: 'fa fa-certificate', + itemId: 'certificates', + groups: ['services'], + nodename: nodename, + xtype: 'pveCertificatesView' + }, + { + title: gettext('DNS'), + iconCls: 'fa fa-globe', + groups: ['services'], + itemId: 'dns', + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + xtype: 'proxmoxNodeDNSView' + }, + { + title: gettext('Hosts'), + iconCls: 'fa fa-globe', + groups: ['services'], + itemId: 'hosts', + nodename: nodename, + onlineHelp: 'sysadmin_network_configuration', + xtype: 'proxmoxNodeHostsView' + }, + { + title: gettext('Time'), + itemId: 'time', + groups: ['services'], + nodename: nodename, + xtype: 'proxmoxNodeTimeView', + iconCls: 'fa fa-clock-o' + }); + } + + if (caps.nodes['Sys.Syslog']) { + me.items.push({ + title: 'Syslog', + iconCls: 'fa fa-list', + groups: ['services'], + disabled: !caps.nodes['Sys.Syslog'], + itemId: 'syslog', + xtype: 'proxmoxJournalView', + url: "/api2/extjs/nodes/" + nodename + "/journal" + }); + + if (caps.nodes['Sys.Modify']) { + me.items.push({ + title: gettext('Updates'), + iconCls: 'fa fa-refresh', + disabled: !caps.nodes['Sys.Console'], + // do we want to link to system updates instead? + itemId: 'apt', + xtype: 'proxmoxNodeAPT', + upgradeBtn: { + xtype: 'pveConsoleButton', + disabled: Proxmox.UserName !== 'root@pam', + text: gettext('Upgrade'), + consoleType: 'upgrade', + nodename: nodename + }, + nodename: nodename + }); + } + } + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + xtype: 'pveFirewallRules', + iconCls: 'fa fa-shield', + title: gettext('Firewall'), + allow_iface: true, + base_url: '/nodes/' + nodename + '/firewall/rules', + list_refs_url: '/cluster/firewall/refs', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + title: gettext('Options'), + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_host_specific_configuration', + groups: ['firewall'], + base_url: '/nodes/' + nodename + '/firewall/options', + fwtype: 'node', + itemId: 'firewall-options' + }); + } + + + if (caps.nodes['Sys.Audit']) { + me.items.push( + { + title: gettext('Disks'), + itemId: 'storage', + expandedOnInit: true, + iconCls: 'fa fa-hdd-o', + xtype: 'pveNodeDiskList' + }, + { + title: 'LVM', + itemId: 'lvm', + onlineHelp: 'chapter_lvm', + iconCls: 'fa fa-square', + groups: ['storage'], + xtype: 'pveLVMList' + }, + { + title: 'LVM-Thin', + itemId: 'lvmthin', + onlineHelp: 'chapter_lvm', + iconCls: 'fa fa-square-o', + groups: ['storage'], + xtype: 'pveLVMThinList' + }, + { + title: Proxmox.Utils.directoryText, + itemId: 'directory', + onlineHelp: 'chapter_storage', + iconCls: 'fa fa-folder', + groups: ['storage'], + xtype: 'pveDirectoryList' + }, + { + title: 'ZFS', + itemId: 'zfs', + onlineHelp: 'chapter_zfs', + iconCls: 'fa fa-th-large', + groups: ['storage'], + xtype: 'pveZFSList' + }, + { + title: 'Ceph', + itemId: 'ceph', + iconCls: 'fa fa-ceph', + xtype: 'pveNodeCephStatus' + }, + { + xtype: 'pveReplicaView', + iconCls: 'fa fa-retweet', + title: gettext('Replication'), + itemId: 'replication' + }, + { + xtype: 'pveNodeCephConfigCrush', + title: gettext('Configuration'), + iconCls: 'fa fa-gear', + groups: ['ceph'], + itemId: 'ceph-config' + }, + { + xtype: 'pveNodeCephMonMgr', + title: gettext('Monitor'), + iconCls: 'fa fa-tv', + groups: ['ceph'], + itemId: 'ceph-monlist' + }, + { + xtype: 'pveNodeCephOsdTree', + title: 'OSD', + iconCls: 'fa fa-hdd-o', + groups: ['ceph'], + itemId: 'ceph-osdtree' + }, + { + xtype: 'pveNodeCephFSPanel', + title: 'CephFS', + iconCls: 'fa fa-folder', + groups: ['ceph'], + nodename: nodename, + itemId: 'ceph-cephfspanel' + }, + { + xtype: 'pveNodeCephPoolList', + title: 'Pools', + iconCls: 'fa fa-sitemap', + groups: ['ceph'], + itemId: 'ceph-pools' + } + ); + } + + if (caps.nodes['Sys.Syslog']) { + me.items.push( + { + xtype: 'proxmoxLogView', + title: gettext('Log'), + iconCls: 'fa fa-list', + groups: ['firewall'], + onlineHelp: 'chapter_pve_firewall', + url: '/api2/extjs/nodes/' + nodename + '/firewall/log', + itemId: 'firewall-fwlog' + }, + { + title: gettext('Log'), + itemId: 'ceph-log', + iconCls: 'fa fa-list', + groups: ['ceph'], + onlineHelp: 'chapter_pveceph', + xtype: 'cephLogView', + url: "/api2/extjs/nodes/" + nodename + "/ceph/log", + nodename: nodename + }); + } + + me.items.push( + { + title: gettext('Task History'), + iconCls: 'fa fa-list', + itemId: 'tasks', + nodename: nodename, + xtype: 'proxmoxNodeTasks' + }, + { + title: gettext('Subscription'), + iconCls: 'fa fa-support', + itemId: 'support', + xtype: 'pveNodeSubscription', + nodename: nodename + } + ); + + me.callParent(); + + me.mon(me.statusStore, 'load', function(s, records, success) { + var uptimerec = s.data.get('uptime'); + var powermgmt = uptimerec ? uptimerec.data.value : false; + if (!caps.nodes['Sys.PowerMgmt']) { + powermgmt = false; + } + restartBtn.setDisabled(!powermgmt); + shutdownBtn.setDisabled(!powermgmt); + shellBtn.setDisabled(!powermgmt); + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.window.Migrate', { + extend: 'Ext.window.Window', + + vmtype: undefined, + nodename: undefined, + vmid: undefined, + + viewModel: { + data: { + vmid: undefined, + nodename: undefined, + vmtype: undefined, + running: false, + qemu: { + onlineHelp: 'qm_migration', + commonName: 'VM' + }, + lxc: { + onlineHelp: 'pct_migration', + commonName: 'CT' + }, + migration: { + possible: true, + preconditions: [], + 'with-local-disks': 0, + mode: undefined, + allowedNodes: undefined + } + + }, + + formulas: { + setMigrationMode: function(get) { + if (get('running')){ + if (get('vmtype') === 'qemu') { + return gettext('Online'); + } else { + return gettext('Restart Mode'); + } + } else { + return gettext('Offline'); + } + }, + setStorageselectorHidden: function(get) { + if (get('migration.with-local-disks') && get('running')) { + return false; + } else { + return true; + } + } + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'panel[reference=formPanel]': { + validityChange: function(panel, isValid) { + this.getViewModel().set('migration.possible', isValid); + this.checkMigratePreconditions(); + } + } + }, + + init: function(view) { + var me = this, + vm = view.getViewModel(); + + if (!view.nodename) { + throw "missing custom view config: nodename"; + } + vm.set('nodename', view.nodename); + + if (!view.vmid) { + throw "missing custom view config: vmid"; + } + vm.set('vmid', view.vmid); + + if (!view.vmtype) { + throw "missing custom view config: vmtype"; + } + vm.set('vmtype', view.vmtype); + + + view.setTitle( + Ext.String.format('{0} {1}{2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid) + ); + me.lookup('proxmoxHelpButton').setHelpConfig({ + onlineHelp: vm.get(view.vmtype).onlineHelp + }); + me.checkMigratePreconditions(); + me.lookup('formPanel').isValid(); + + }, + + onTargetChange: function (nodeSelector) { + //Always display the storages of the currently seleceted migration target + this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value); + this.checkMigratePreconditions(); + }, + + startMigration: function() { + var me = this, + view = me.getView(), + vm = me.getViewModel(); + + var values = me.lookup('formPanel').getValues(); + var params = { + target: values.target + }; + + if (vm.get('migration.mode')) { + params[vm.get('migration.mode')] = 1; + } + if (vm.get('migration.with-local-disks')) { + params['with-local-disks'] = 1; + } + //only submit targetstorage if vm is running, storage migration to different storage is only possible online + if (vm.get('migration.with-local-disks') && vm.get('running')) { + params.targetstorage = values.targetstorage; + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate', + waitMsgTarget: view, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var extraTitle = Ext.String.format(' ({0} ---> {1})', vm.get('nodename'), params.target); + + Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + extraTitle: extraTitle + }).show(); + + view.close(); + } + }); + + }, + + checkMigratePreconditions: function() { + var me = this, + vm = me.getViewModel(); + + + var vmrec = PVE.data.ResourceStore.findRecord('vmid', vm.get('vmid'), + 0, false, false, true); + if (vmrec && vmrec.data && vmrec.data.running) { + vm.set('running', true); + } + + if (vm.get('vmtype') === 'qemu') { + me.checkQemuPreconditions(); + } else { + me.checkLxcPreconditions(); + } + me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')]; + + // Only allow nodes where the local storage is available in case of offline migration + // where storage migration is not possible + me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes'); + + me.lookup('formPanel').isValid(); + + }, + + checkQemuPreconditions: function() { + var me = this, + vm = me.getViewModel(), + migrateStats; + + if (vm.get('running')) { + vm.set('migration.mode', 'online'); + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate', + method: 'GET', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + migrateStats = response.result.data; + if (migrateStats.running) { + vm.set('running', true); + } + // Get migration object from viewmodel to prevent + // to many bind callbacks + var migration = vm.get('migration'); + migration.preconditions = []; + + if (migrateStats.allowed_nodes) { + migration.allowedNodes = migrateStats.allowed_nodes; + var target = me.lookup('pveNodeSelector').value; + if (target.length && !migrateStats.allowed_nodes.includes(target)) { + let disallowed = migrateStats.not_allowed_nodes[target]; + let missing_storages = disallowed.unavailable_storages.join(', '); + + migration.possible = false; + migration.preconditions.push({ + text: 'Storage (' + missing_storages + ') not available on selected target. ' + + 'Start VM to use live storage migration or select other target node', + severity: 'error' + }); + } + } + + if (migrateStats.local_resources.length) { + migration.possible = false; + migration.preconditions.push({ + text: 'Can\'t migrate VM with local resources: '+ migrateStats.local_resources.join(', '), + severity: 'error' + }); + } + + if (migrateStats.local_disks.length) { + + migrateStats.local_disks.forEach(function (disk) { + if (disk.cdrom && disk.cdrom === 1) { + migration.possible = false; + migration.preconditions.push({ + text: "Can't migrate VM with local CD/DVD", + severity: 'error' + }); + + } else if (!disk.referenced_in_config) { + migration.possible = false; + migration.preconditions.push({ + text: 'Found not referenced/unused disk via storage: '+ disk.volid, + severity: 'error' + }); + } else { + migration['with-local-disks'] = 1; + migration.preconditions.push({ + text:'Migration with local disk might take long: ' + disk.volid + +' (' + PVE.Utils.render_size(disk.size) + ')', + severity: 'warning' + }); + } + }); + + } + + vm.set('migration', migration); + + } + }); + }, + checkLxcPreconditions: function() { + var me = this, + vm = me.getViewModel(); + if (vm.get('running')) { + vm.set('migration.mode', 'restart'); + } + } + + + }, + + width: 600, + modal: true, + layout: { + type: 'vbox', + align: 'stretch' + }, + border: false, + items: [ + { + xtype: 'form', + reference: 'formPanel', + bodyPadding: 10, + border: false, + layout: { + type: 'column' + }, + items: [ + { + xtype: 'container', + columnWidth: 0.5, + items: [{ + xtype: 'displayfield', + name: 'source', + fieldLabel: gettext('Source node'), + bind: { + value: '{nodename}' + } + }, + { + xtype: 'displayfield', + reference: 'migrationMode', + fieldLabel: gettext('Mode'), + bind: { + value: '{setMigrationMode}' + } + }] + }, + { + xtype: 'container', + columnWidth: 0.5, + items: [{ + xtype: 'pveNodeSelector', + reference: 'pveNodeSelector', + name: 'target', + fieldLabel: gettext('Target node'), + allowBlank: false, + disallowedNodes: undefined, + onlineValidator: true, + listeners: { + change: 'onTargetChange' + } + }, + { + xtype: 'pveStorageSelector', + reference: 'pveDiskStorageSelector', + name: 'targetstorage', + fieldLabel: gettext('Target storage'), + storageContent: 'images', + bind: { + hidden: '{setStorageselectorHidden}' + } + }] + } + ] + }, + { + xtype: 'gridpanel', + reference: 'preconditionGrid', + selectable: false, + flex: 1, + columns: [{ + text: '', + dataIndex: 'severity', + renderer: function(v) { + switch (v) { + case 'warning': + return ' '; + case 'error': + return ''; + default: + return v; + } + }, + width: 35 + }, + { + text: 'Info', + dataIndex: 'text', + cellWrap: true, + flex: 1 + }], + bind: { + hidden: '{!migration.preconditions.length}', + store: { + fields: ['severity','text'], + data: '{migration.preconditions}' + } + } + } + + ], + buttons: [ + { + xtype: 'proxmoxHelpButton', + reference: 'proxmoxHelpButton', + onlineHelp: 'pct_migration', + listenToGlobalEvent: false, + hidden: false + }, + '->', + { + xtype: 'button', + reference: 'submitButton', + text: gettext('Migrate'), + handler: 'startMigration', + bind: { + disabled: '{!migration.possible}' + } + } + ] +}); +Ext.define('PVE.window.BulkAction', { + extend: 'Ext.window.Window', + + resizable: true, + width: 800, + modal: true, + layout: { + type: 'fit' + }, + border: false, + + // the action to be set + // currently there are + // startall + // migrateall + // stopall + action: undefined, + + submit: function(params) { + var me = this; + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/' + me.action, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + me.hide(); + win.on('destroy', function() { + me.close(); + }); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.action) { + throw "no action specified"; + } + + if (!me.btnText) { + throw "no button text specified"; + } + + if (!me.title) { + throw "no title specified"; + } + + var items = []; + + if (me.action === 'migrateall') { + /*jslint confusion: true*/ + /*value is string and number*/ + items.push( + { + xtype: 'pveNodeSelector', + name: 'target', + disallowedNodes: [me.nodename], + fieldLabel: gettext('Target node'), + allowBlank: false, + onlineValidator: true + }, + { + xtype: 'proxmoxintegerfield', + name: 'maxworkers', + minValue: 1, + maxValue: 100, + value: 1, + fieldLabel: gettext('Parallel jobs'), + allowBlank: false + }, + { + itemId: 'lxcwarning', + xtype: 'displayfield', + userCls: 'pve-hint', + value: 'Warning: Running CTs will be migrated in Restart Mode.', + hidden: true // only visible if running container chosen + } + ); + /*jslint confusion: false*/ + } else if (me.action === 'startall') { + items.push({ + xtype: 'hiddenfield', + name: 'force', + value: 1 + }); + } + + items.push({ + xtype: 'vmselector', + itemId: 'vms', + name: 'vms', + flex: 1, + height: 300, + selectAll: true, + allowBlank: false, + nodename: me.nodename, + action: me.action, + listeners: { + selectionchange: function(vmselector, records) { + if (me.action == 'migrateall') { + var showWarning = records.some(function(item) { + return (item.data.type == 'lxc' && + item.data.status == 'running'); + }); + me.down('#lxcwarning').setVisible(showWarning); + } + } + } + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + layout: { + type: 'vbox', + align: 'stretch' + }, + fieldDefaults: { + labelWidth: 300, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn = Ext.create('Ext.Button', { + text: me.btnText, + handler: function() { + form.isValid(); + me.submit(form.getValues()); + } + }); + + Ext.apply(me, { + items: [ me.formPanel ], + buttons: [ submitBtn ] + }); + + me.callParent(); + + form.on('validitychange', function() { + var valid = form.isValid(); + submitBtn.setDisabled(!valid); + }); + form.isValid(); + } +}); +Ext.define('PVE.window.Clone', { + extend: 'Ext.window.Window', + + resizable: false, + + isTemplate: false, + + onlineHelp: 'qm_copy_and_clone', + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'panel[reference=cloneform]': { + validitychange: 'disableSubmit' + } + }, + disableSubmit: function(form) { + this.lookupReference('submitBtn').setDisabled(!form.isValid()); + } + }, + + statics: { + // display a snapshot selector only if needed + wrap: function(nodename, vmid, isTemplate, guestType) { + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/' + guestType + '/' + vmid +'/snapshot', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, opts) { + var snapshotList = response.result.data; + var hasSnapshots = snapshotList.length === 1 && + snapshotList[0].name === 'current' ? false : true; + + Ext.create('PVE.window.Clone', { + nodename: nodename, + guestType: guestType, + vmid: vmid, + isTemplate: isTemplate, + hasSnapshots: hasSnapshots + }).show(); + } + }); + } + }, + + create_clone: function(values) { + var me = this; + + var params = { newid: values.newvmid }; + + if (values.snapname && values.snapname !== 'current') { + params.snapname = values.snapname; + } + + if (values.pool) { + params.pool = values.pool; + } + + if (values.name) { + if (me.guestType === 'lxc') { + params.hostname = values.name; + } else { + params.name = values.name; + } + } + + if (values.target) { + params.target = values.target; + } + + if (values.clonemode === 'copy') { + params.full = 1; + if (values.hdstorage) { + params.storage = values.hdstorage; + if (values.diskformat && me.guestType !== 'lxc') { + params.format = values.diskformat; + } + } + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/clone', + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + me.close(); + } + }); + + }, + + // disable the Storage selector when clone mode is linked clone + updateVisibility: function() { + var me = this; + var clonemode = me.lookupReference('clonemodesel').getValue(); + var disksel = me.lookup('diskselector'); + disksel.setDisabled(clonemode === 'clone'); + }, + + // add to the list of valid nodes each node where + // all the VM disks are available + verifyFeature: function() { + var me = this; + + var snapname = me.lookupReference('snapshotsel').getValue(); + var clonemode = me.lookupReference('clonemodesel').getValue(); + + var params = { feature: clonemode }; + if (snapname !== 'current') { + params.snapname = snapname; + } + + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: '/nodes/' + me.nodename + '/' + me.guestType + '/' + me.vmid + '/feature', + params: params, + method: 'GET', + failure: function(response, opts) { + me.lookupReference('submitBtn').setDisabled(true); + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var res = response.result.data; + + me.lookupReference('targetsel').allowedNodes = res.nodes; + me.lookupReference('targetsel').validate(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.snapname) { + me.snapname = 'current'; + } + + if (!me.guestType) { + throw "no Guest Type specified"; + } + + var titletext = me.guestType === 'lxc' ? 'CT' : 'VM'; + if (me.isTemplate) { + titletext += ' Template'; + } + me.title = "Clone " + titletext + " " + me.vmid; + + var col1 = []; + var col2 = []; + + col1.push({ + xtype: 'pveNodeSelector', + name: 'target', + reference: 'targetsel', + fieldLabel: gettext('Target node'), + selectCurNode: true, + allowBlank: false, + onlineValidator: true, + listeners: { + change: function(f, value) { + me.lookupReference('hdstorage').setTargetNode(value); + } + } + }); + + var modelist = [['copy', gettext('Full Clone')]]; + if (me.isTemplate) { + modelist.push(['clone', gettext('Linked Clone')]); + } + + col1.push({ + xtype: 'pveGuestIDSelector', + name: 'newvmid', + guestType: me.guestType, + value: '', + loadNextFreeID: true, + validateExists: false + }, + { + xtype: 'textfield', + name: 'name', + allowBlank: true, + fieldLabel: me.guestType === 'lxc' ? gettext('Hostname') : gettext('Name') + }, + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true + } + ); + + col2.push({ + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Mode'), + name: 'clonemode', + reference: 'clonemodesel', + allowBlank: false, + hidden: !me.isTemplate, + value: me.isTemplate ? 'clone' : 'copy', + comboItems: modelist, + listeners: { + change: function(t, value) { + me.updateVisibility(); + me.verifyFeature(); + } + } + }, + { + xtype: 'PVE.form.SnapshotSelector', + name: 'snapname', + reference: 'snapshotsel', + fieldLabel: gettext('Snapshot'), + nodename: me.nodename, + guestType: me.guestType, + vmid: me.vmid, + hidden: me.isTemplate || !me.hasSnapshots ? true : false, + disabled: false, + allowBlank: false, + value : me.snapname, + listeners: { + change: function(f, value) { + me.verifyFeature(); + } + } + }, + { + xtype: 'pveDiskStorageSelector', + reference: 'diskselector', + nodename: me.nodename, + autoSelect: false, + hideSize: true, + hideSelection: true, + storageLabel: gettext('Target Storage'), + allowBlank: true, + storageContent: me.guestType === 'qemu' ? 'images' : 'rootdir', + emptyText: gettext('Same as source'), + disabled: me.isTemplate ? true : false // because default mode is clone for templates + }); + + var formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + reference: 'cloneform', + border: false, + layout: 'column', + defaultType: 'container', + columns: 2, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [ + { + columnWidth: 0.5, + padding: '0 10 0 0', + layout: 'anchor', + items: col1 + }, + { + columnWidth: 0.5, + padding: '0 0 0 10', + layout: 'anchor', + items: col2 + } + ] + }); + + Ext.apply(me, { + modal: true, + width: 600, + height: 250, + border: false, + layout: 'fit', + buttons: [ { + xtype: 'proxmoxHelpButton', + listenToGlobalEvent: false, + hidden: false, + onlineHelp: me.onlineHelp + }, + '->', + { + reference: 'submitBtn', + text: gettext('Clone'), + disabled: true, + handler: function() { + var cloneForm = me.lookupReference('cloneform'); + if (cloneForm.isValid()) { + me.create_clone(cloneForm.getValues()); + } + } + } ], + items: [ formPanel ] + }); + + me.callParent(); + + me.verifyFeature(); + } +}); +Ext.define('PVE.qemu.Monitor', { + extend: 'Ext.panel.Panel', + + alias: 'widget.pveQemuMonitor', + + maxLines: 500, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var history = []; + var histNum = -1; + var lines = []; + + var textbox = Ext.createWidget('panel', { + region: 'center', + xtype: 'panel', + autoScroll: true, + border: true, + margins: '5 5 5 5', + bodyStyle: 'font-family: monospace;' + }); + + var scrollToEnd = function() { + var el = textbox.getTargetEl(); + var dom = Ext.getDom(el); + + var clientHeight = dom.clientHeight; + // BrowserBug: clientHeight reports 0 in IE9 StrictMode + // Instead we are using offsetHeight and hardcoding borders + if (Ext.isIE9 && Ext.isStrict) { + clientHeight = dom.offsetHeight + 2; + } + dom.scrollTop = dom.scrollHeight - clientHeight; + }; + + var refresh = function() { + textbox.update('
' + lines.join('\n') + '
'); + scrollToEnd(); + }; + + var addLine = function(line) { + lines.push(line); + if (lines.length > me.maxLines) { + lines.shift(); + } + }; + + var executeCmd = function(cmd) { + addLine("# " + Ext.htmlEncode(cmd)); + if (cmd) { + history.unshift(cmd); + if (history.length > 20) { + history.splice(20); + } + } + histNum = -1; + + refresh(); + Proxmox.Utils.API2Request({ + params: { command: cmd }, + url: '/nodes/' + nodename + '/qemu/' + vmid + "/monitor", + method: 'POST', + waitMsgTarget: me, + success: function(response, opts) { + var res = response.result.data; + Ext.Array.each(res.split('\n'), function(line) { + addLine(Ext.htmlEncode(line)); + }); + refresh(); + }, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + Ext.apply(me, { + layout: { type: 'border' }, + border: false, + items: [ + textbox, + { + region: 'south', + margins:'0 5 5 5', + border: false, + xtype: 'textfield', + name: 'cmd', + value: '', + fieldStyle: 'font-family: monospace;', + allowBlank: true, + listeners: { + afterrender: function(f) { + f.focus(false); + addLine("Type 'help' for help."); + refresh(); + }, + specialkey: function(f, e) { + var key = e.getKey(); + switch (key) { + case e.ENTER: + var cmd = f.getValue(); + f.setValue(''); + executeCmd(cmd); + break; + case e.PAGE_UP: + textbox.scrollBy(0, -0.9*textbox.getHeight(), false); + break; + case e.PAGE_DOWN: + textbox.scrollBy(0, 0.9*textbox.getHeight(), false); + break; + case e.UP: + if (histNum + 1 < history.length) { + f.setValue(history[++histNum]); + } + e.preventDefault(); + break; + case e.DOWN: + if (histNum > 0) { + f.setValue(history[--histNum]); + } + e.preventDefault(); + break; + default: + break; + } + } + } + } + ], + listeners: { + show: function() { + var field = me.query('textfield[name="cmd"]')[0]; + field.focus(false, true); + } + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.qemu.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveQemuSummary', + + scrollable: true, + bodyPadding: 5, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + if (!me.workspace) { + throw "no workspace specified"; + } + + if (!me.statusStore) { + throw "no status storage specified"; + } + + var template = !!me.pveSelNode.data.template; + var rstore = me.statusStore; + + var width = template ? 1 : 0.5; + var items = [ + { + xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView', + responsiveConfig: { + 'width < 1900': { + columnWidth: width + }, + 'width >= 1900': { + columnWidth: width / 2 + } + }, + itemId: 'gueststatus', + pveSelNode: me.pveSelNode, + rstore: rstore + }, + { + xtype: 'pveNotesView', + maxHeight: 330, + itemId: 'notesview', + pveSelNode: me.pveSelNode, + responsiveConfig: { + 'width < 1900': { + columnWidth: width + }, + 'width >= 1900': { + columnWidth: width / 2 + } + } + } + ]; + + var rrdstore; + if (!template) { + + rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/rrddata", + model: 'pve-rrd-guest' + }); + + items.push( + { + xtype: 'proxmoxRRDChart', + title: gettext('CPU usage'), + pveSelNode: me.pveSelNode, + fields: ['cpu'], + fieldTitles: [gettext('CPU usage')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Memory usage'), + pveSelNode: me.pveSelNode, + fields: ['maxmem', 'mem'], + fieldTitles: [gettext('Total'), gettext('RAM usage')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Network traffic'), + pveSelNode: me.pveSelNode, + fields: ['netin','netout'], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Disk IO'), + pveSelNode: me.pveSelNode, + fields: ['diskread','diskwrite'], + store: rrdstore + } + ); + + } + + Ext.apply(me, { + tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ], + items: [ + { + xtype: 'container', + layout: { + type: 'column' + }, + defaults: { + minHeight: 330, + padding: 5, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1 + }, + 'width >= 1900': { + columnWidth: 0.5 + } + } + }, + items: items + } + ] + }); + + me.callParent(); + if (!template) { + rrdstore.startUpdate(); + me.on('destroy', rrdstore.stopUpdate); + } + } +}); +Ext.define('PVE.qemu.OSTypeInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuOSTypePanel', + onlineHelp: 'qm_os_settings', + insideWizard: false, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'combobox[name=osbase]': { + change: 'onOSBaseChange' + }, + 'combobox[name=ostype]': { + afterrender: 'onOSTypeChange', + change: 'onOSTypeChange' + } + }, + onOSBaseChange: function(field, value) { + this.lookup('ostype').getStore().setData(PVE.Utils.kvm_ostypes[value]); + }, + onOSTypeChange: function(field) { + var me = this, ostype = field.getValue(); + if (!me.getView().insideWizard) { + return; + } + var targetValues = PVE.qemu.OSDefaults.getDefaults(ostype); + + me.setWidget('pveBusSelector', targetValues.busType); + me.setWidget('pveNetworkCardSelector', targetValues.networkCard); + var scsihw = targetValues.scsihw || '__default__'; + this.getViewModel().set('current.scsihw', scsihw); + }, + setWidget: function(widget, newValue) { + // changing a widget is safe only if ComponentQuery.query returns us + // a single value array + var widgets = Ext.ComponentQuery.query('pveQemuCreateWizard ' + widget); + if (widgets.length === 1) { + widgets[0].setValue(newValue); + } else { + throw 'non unique widget :' + widget + ' in Wizard'; + } + } + }, + + initComponent : function() { + var me = this; + + /*jslint confusion: true */ + me.items = [ + { + xtype: 'displayfield', + value: gettext('Guest OS') + ':', + hidden: !me.insideWizard + }, + { + xtype: 'combobox', + submitValue: false, + name: 'osbase', + fieldLabel: gettext('Type'), + editable: false, + queryMode: 'local', + value: 'Linux', + store: Object.keys(PVE.Utils.kvm_ostypes) + }, + { + xtype: 'combobox', + name: 'ostype', + reference: 'ostype', + fieldLabel: gettext('Version'), + value: 'l26', + allowBlank : false, + editable: false, + queryMode: 'local', + valueField: 'val', + displayField: 'desc', + store: { + fields: ['desc', 'val'], + data: PVE.Utils.kvm_ostypes.Linux, + listeners: { + datachanged: function (store) { + var ostype = me.lookup('ostype'); + var old_val = ostype.getValue(); + if (!me.insideWizard && old_val && store.find('val', old_val) != -1) { + ostype.setValue(old_val); + } else { + ostype.setValue(store.getAt(0)); + } + } + } + } + } + ]; + /*jslint confusion: false */ + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.OSTypeEdit', { + extend: 'Proxmox.window.Edit', + + subject: 'OS Type', + + items: [{ xtype: 'pveQemuOSTypePanel' }], + + initComponent : function() { + var me = this; + + me.callParent(); + + me.load({ + success: function(response, options) { + var value = response.result.data.ostype || 'other'; + var osinfo = PVE.Utils.get_kvm_osinfo(value); + me.setValues({ ostype: value, osbase: osinfo.base }); + } + }); + } +}); +/* + * This class holds performance *recommended* settings for the PVE Qemu wizards + * the *mandatory* settings are set in the PVE::QemuServer + * config_to_command sub + * We store this here until we get the data from the API server +*/ + +// this is how you would add an hypothetic FreeBSD > 10 entry +// +//virtio-blk is stable but virtIO net still +// problematic as of 10.3 +// see https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=165059 +// addOS({ +// parent: 'generic', // inherits defaults +// pveOS: 'freebsd10', // must match a radiofield in OSTypeEdit.js +// busType: 'virtio' // must match a pveBusController value +// // networkCard muss match a pveNetworkCardSelector + + +Ext.define('PVE.qemu.OSDefaults', { + singleton: true, // will also force creation when loaded + + constructor: function() { + var me = this; + + var addOS = function(settings) { + if (me.hasOwnProperty(settings.parent)) { + var child = Ext.clone(me[settings.parent]); + me[settings.pveOS] = Ext.apply(child, settings); + + } else { + throw("Could not find your genitor"); + } + }; + + // default values + me.generic = { + busType: 'ide', + networkCard: 'e1000', + busPriority: { + ide: 4, + sata: 3, + scsi: 2, + virtio: 1 + }, + scsihw: 'virtio-scsi-pci' + }; + + // virtio-net is in kernel since 2.6.25 + // virtio-scsi since 3.2 but backported in RHEL with 2.6 kernel + addOS({ + pveOS: 'l26', + parent : 'generic', + busType: 'scsi', + busPriority: { + scsi: 4, + virtio: 3, + sata: 2, + ide: 1 + }, + networkCard: 'virtio' + }); + + // recommandation from http://wiki.qemu.org/Windows2000 + addOS({ + pveOS: 'w2k', + parent : 'generic', + networkCard: 'rtl8139', + scsihw: '' + }); + // https://pve.proxmox.com/wiki/Windows_XP_Guest_Notes + addOS({ + pveOS: 'wxp', + parent : 'w2k' + }); + + me.getDefaults = function(ostype) { + if (PVE.qemu.OSDefaults[ostype]) { + return PVE.qemu.OSDefaults[ostype]; + } else { + return PVE.qemu.OSDefaults.generic; + } + }; + } +}); +Ext.define('PVE.qemu.ProcessorInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuProcessorPanel', + onlineHelp: 'qm_cpu', + + insideWizard: false, + + controller: { + xclass: 'Ext.app.ViewController', + + updateCores: function() { + var me = this.getView(); + var sockets = me.down('field[name=sockets]').getValue(); + var cores = me.down('field[name=cores]').getValue(); + me.down('field[name=totalcores]').setValue(sockets*cores); + var vcpus = me.down('field[name=vcpus]'); + vcpus.setMaxValue(sockets*cores); + vcpus.setEmptyText(sockets*cores); + vcpus.validate(); + }, + + control: { + 'field[name=sockets]': { + change: 'updateCores' + }, + 'field[name=cores]': { + change: 'updateCores' + } + } + }, + + onGetValues: function(values) { + var me = this; + + if (Array.isArray(values['delete'])) { + values['delete'] = values['delete'].join(','); + } + + PVE.Utils.delete_if_default(values, 'cpulimit', '0', 0); + PVE.Utils.delete_if_default(values, 'cpuunits', '1024', 0); + + // build the cpu options: + me.cpu.cputype = values.cputype; + + if (values.flags) { + me.cpu.flags = values.flags; + } else { + delete me.cpu.flags; + } + + delete values.cputype; + delete values.flags; + var cpustring = PVE.Parser.printQemuCpu(me.cpu); + + // remove cputype delete request: + var del = values['delete']; + delete values['delete']; + if (del) { + del = del.split(','); + Ext.Array.remove(del, 'cputype'); + } else { + del = []; + } + + if (cpustring) { + values.cpu = cpustring; + } else { + del.push('cpu'); + } + + var delarr = del.join(','); + if (delarr) { + values['delete'] = delarr; + } + + return values; + }, + + cpu: {}, + + column1: [ + { + xtype: 'proxmoxintegerfield', + name: 'sockets', + minValue: 1, + maxValue: 4, + value: '1', + fieldLabel: gettext('Sockets'), + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + name: 'cores', + minValue: 1, + maxValue: 128, + value: '1', + fieldLabel: gettext('Cores'), + allowBlank: false + } + ], + + column2: [ + { + xtype: 'CPUModelSelector', + name: 'cputype', + value: '__default__', + fieldLabel: gettext('Type') + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Total cores'), + name: 'totalcores', + value: '1' + } + ], + + advancedColumn1: [ + { + xtype: 'proxmoxintegerfield', + name: 'vcpus', + minValue: 1, + maxValue: 1, + value: '', + fieldLabel: gettext('VCPUs'), + deleteEmpty: true, + allowBlank: true, + emptyText: '1' + }, + { + xtype: 'numberfield', + name: 'cpulimit', + minValue: 0, + maxValue: 128, // api maximum + value: '', + step: 1, + fieldLabel: gettext('CPU limit'), + allowBlank: true, + emptyText: gettext('unlimited') + } + ], + + advancedColumn2: [ + { + xtype: 'proxmoxintegerfield', + name: 'cpuunits', + fieldLabel: gettext('CPU units'), + minValue: 8, + maxValue: 500000, + value: '1024', + deleteEmpty: true, + allowBlank: true + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enable NUMA'), + name: 'numa', + uncheckedValue: 0 + } + ], + advancedColumnB: [ + { + xtype: 'label', + text: 'Extra CPU Flags:' + }, + { + xtype: 'vmcpuflagselector', + name: 'flags' + } + ] +}); + +Ext.define('PVE.qemu.ProcessorEdit', { + extend: 'Proxmox.window.Edit', + + width: 700, + + initComponent : function() { + var me = this; + + var ipanel = Ext.create('PVE.qemu.ProcessorInputPanel'); + + Ext.apply(me, { + subject: gettext('Processors'), + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var data = response.result.data; + var value = data.cpu; + if (value) { + var cpu = PVE.Parser.parseQemuCpu(value); + ipanel.cpu = cpu; + data.cputype = cpu.cputype; + if (cpu.flags) { + data.flags = cpu.flags; + } + } + me.setValues(data); + } + }); + } +}); +Ext.define('PVE.qemu.BootOrderPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuBootOrderPanel', + vmconfig: {}, // store loaded vm config + + bootdisk: undefined, + selection: [], + list: [], + comboboxes: [], + + isBootDisk: function(value) { + return PVE.Utils.bus_match.test(value); + }, + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + var order = me.vmconfig.boot || 'cdn'; + me.bootdisk = me.vmconfig.bootdisk || undefined; + + // get the first 3 characters + // ignore the rest (there should never be more than 3) + me.selection = order.split('').slice(0,3); + + // build bootdev list + me.list = []; + Ext.Object.each(me.vmconfig, function(key, value) { + if (me.isBootDisk(key) && + !(/media=cdrom/).test(value)) { + me.list.push([key, "Disk '" + key + "'"]); + } + }); + + me.list.push(['d', 'CD-ROM']); + me.list.push(['n', gettext('Network')]); + me.list.push(['__none__', Proxmox.Utils.noneText]); + + me.recomputeList(); + + me.comboboxes.forEach(function(box) { + box.resetOriginalValue(); + }); + }, + + onGetValues: function(values) { + var me = this; + var order = me.selection.join(''); + var res = { boot: order }; + + if (me.bootdisk && order.indexOf('c') !== -1) { + res.bootdisk = me.bootdisk; + } else { + res['delete'] = 'bootdisk'; + } + + return res; + }, + + recomputeSelection: function(combobox, newVal, oldVal) { + var me = this.up('#inputpanel'); + me.selection = []; + me.comboboxes.forEach(function(item) { + var val = item.getValue(); + + // when selecting an already selected item, + // switch it around + if ((val === newVal || (me.isBootDisk(val) && me.isBootDisk(newVal))) && + item.name !== combobox.name && + newVal !== '__none__') { + // swap items + val = oldVal; + } + + // push 'c','d' or 'n' in the array + if (me.isBootDisk(val)) { + me.selection.push('c'); + me.bootdisk = val; + } else if (val === 'd' || + val === 'n') { + me.selection.push(val); + } + }); + + me.recomputeList(); + }, + + recomputeList: function(){ + var me = this; + // set the correct values in the kvcomboboxes + var cnt = 0; + me.comboboxes.forEach(function(item) { + if (cnt === 0) { + // never show 'none' on first combobox + item.store.loadData(me.list.slice(0, me.list.length-1)); + } else { + item.store.loadData(me.list); + } + item.suspendEvent('change'); + if (cnt < me.selection.length) { + item.setValue((me.selection[cnt] !== 'c')?me.selection[cnt]:me.bootdisk); + } else if (cnt === 0){ + item.setValue(''); + } else { + item.setValue('__none__'); + } + cnt++; + item.resumeEvent('change'); + item.validate(); + }); + }, + + initComponent : function() { + var me = this; + + // this has to be done here, because of + // the way our inputPanel class handles items + me.comboboxes = [ + Ext.createWidget('proxmoxKVComboBox', { + fieldLabel: gettext('Boot device') + " 1", + labelWidth: 120, + name: 'bd1', + allowBlank: false, + listeners: { + change: me.recomputeSelection + } + }), + Ext.createWidget('proxmoxKVComboBox', { + fieldLabel: gettext('Boot device') + " 2", + labelWidth: 120, + name: 'bd2', + allowBlank: false, + listeners: { + change: me.recomputeSelection + } + }), + Ext.createWidget('proxmoxKVComboBox', { + fieldLabel: gettext('Boot device') + " 3", + labelWidth: 120, + name: 'bd3', + allowBlank: false, + listeners: { + change: me.recomputeSelection + } + }) + ]; + Ext.apply(me, { items: me.comboboxes }); + me.callParent(); + } +}); + +Ext.define('PVE.qemu.BootOrderEdit', { + extend: 'Proxmox.window.Edit', + + items: [{ + xtype: 'pveQemuBootOrderPanel', + itemId: 'inputpanel' + }], + + subject: gettext('Boot Order'), + + initComponent : function() { + var me = this; + me.callParent(); + me.load({ + success: function(response, options) { + me.down('#inputpanel').setVMConfig(response.result.data); + } + }); + } +}); +Ext.define('PVE.qemu.MemoryInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuMemoryPanel', + onlineHelp: 'qm_memory', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + var res = {}; + + res.memory = values.memory; + res.balloon = values.balloon; + + if (!values.ballooning) { + res.balloon = 0; + res['delete'] = 'shares'; + } else if (values.memory === values.balloon) { + delete res.balloon; + res['delete'] = 'balloon,shares'; + } else if (Ext.isDefined(values.shares) && (values.shares !== "")) { + res.shares = values.shares; + } else { + res['delete'] = "shares"; + } + + return res; + }, + + initComponent: function() { + var me = this; + var labelWidth = 160; + + me.items= [ + { + xtype: 'pveMemoryField', + labelWidth: labelWidth, + fieldLabel: gettext('Memory') + ' (MiB)', + name: 'memory', + minValue: 1, + step: 32, + hotplug: me.hotplug, + listeners: { + change: function(f, value, old) { + var bf = me.down('field[name=balloon]'); + var balloon = bf.getValue(); + bf.setMaxValue(value); + if (balloon === old) { + bf.setValue(value); + } + bf.validate(); + } + } + } + ]; + + me.advancedItems= [ + { + xtype: 'pveMemoryField', + name: 'balloon', + minValue: 1, + step: 32, + fieldLabel: gettext('Minimum memory') + ' (MiB)', + hotplug: me.hotplug, + labelWidth: labelWidth, + allowBlank: false, + listeners: { + change: function(f, value) { + var memory = me.down('field[name=memory]').getValue(); + var shares = me.down('field[name=shares]'); + shares.setDisabled(value === memory); + } + } + }, + { + xtype: 'proxmoxintegerfield', + name: 'shares', + disabled: true, + minValue: 0, + maxValue: 50000, + value: '', + step: 10, + fieldLabel: gettext('Shares'), + labelWidth: labelWidth, + allowBlank: true, + emptyText: Proxmox.Utils.defaultText + ' (1000)', + submitEmptyText: false + }, + { + xtype: 'proxmoxcheckbox', + labelWidth: labelWidth, + value: '1', + name: 'ballooning', + fieldLabel: gettext('Ballooning Device'), + listeners: { + change: function(f, value) { + var bf = me.down('field[name=balloon]'); + var shares = me.down('field[name=shares]'); + var memory = me.down('field[name=memory]'); + bf.setDisabled(!value); + shares.setDisabled(!value || (bf.getValue() === memory.getValue())); + } + } + } + ]; + + if (me.insideWizard) { + me.column1 = me.items; + me.items = undefined; + me.advancedColumn1 = me.advancedItems; + me.advancedItems = undefined; + } + me.callParent(); + } + +}); + +Ext.define('PVE.qemu.MemoryEdit', { + extend: 'Proxmox.window.Edit', + + initComponent: function() { + var me = this; + + var memoryhotplug; + if(me.hotplug) { + Ext.each(me.hotplug.split(','), function(el) { + if (el === 'memory') { + memoryhotplug = 1; + } + }); + } + + var ipanel = Ext.create('PVE.qemu.MemoryInputPanel', { + hotplug: memoryhotplug + }); + + Ext.apply(me, { + subject: gettext('Memory'), + items: [ ipanel ], + // uncomment the following to use the async configiguration API + // backgroundDelay: 5, + width: 400 + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var data = response.result.data; + + var values = { + ballooning: data.balloon === 0 ? '0' : '1', + shares: data.shares, + memory: data.memory || '512', + balloon: data.balloon > 0 ? data.balloon : (data.memory || '512') + }; + + ipanel.setValues(values); + } + }); + } +}); +Ext.define('PVE.qemu.NetworkInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuNetworkInputPanel', + onlineHelp: 'qm_network_device', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + me.network.model = values.model; + if (values.nonetwork) { + return {}; + } else { + me.network.bridge = values.bridge; + me.network.tag = values.tag; + me.network.firewall = values.firewall; + } + me.network.macaddr = values.macaddr; + me.network.disconnect = values.disconnect; + me.network.queues = values.queues; + + if (values.rate) { + me.network.rate = values.rate; + } else { + delete me.network.rate; + } + + var params = {}; + + params[me.confid] = PVE.Parser.printQemuNetwork(me.network); + + return params; + }, + + setNetwork: function(confid, data) { + var me = this; + + me.confid = confid; + + if (data) { + data.networkmode = data.bridge ? 'bridge' : 'nat'; + } else { + data = {}; + data.networkmode = 'bridge'; + } + me.network = data; + + me.setValues(me.network); + }, + + setNodename: function(nodename) { + var me = this; + + me.bridgesel.setNodename(nodename); + }, + + initComponent : function() { + var me = this; + + me.network = {}; + me.confid = 'net0'; + + me.column1 = []; + me.column2 = []; + + me.bridgesel = Ext.create('PVE.form.BridgeSelector', { + name: 'bridge', + fieldLabel: gettext('Bridge'), + nodename: me.nodename, + autoSelect: true, + allowBlank: false + }); + + me.column1 = [ + me.bridgesel, + { + xtype: 'pveVlanField', + name: 'tag', + value: '' + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Firewall'), + name: 'firewall', + checked: (me.insideWizard || me.isCreate) + } + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Disconnect'), + name: 'disconnect' + } + ]; + + if (me.insideWizard) { + me.column1.unshift({ + xtype: 'checkbox', + name: 'nonetwork', + inputValue: 'none', + boxLabel: gettext('No network device'), + listeners: { + change: function(cb, value) { + var fields = [ + 'disconnect', + 'bridge', + 'tag', + 'firewall', + 'model', + 'macaddr', + 'rate', + 'queues' + ]; + fields.forEach(function(fieldname) { + me.down('field[name='+fieldname+']').setDisabled(value); + }); + me.down('field[name=bridge]').validate(); + } + } + }); + me.column2.unshift({ + xtype: 'displayfield' + }); + } + + me.column2.push( + { + xtype: 'pveNetworkCardSelector', + name: 'model', + fieldLabel: gettext('Model'), + value: PVE.qemu.OSDefaults.generic.networkCard, + allowBlank: false + }, + { + xtype: 'textfield', + name: 'macaddr', + fieldLabel: gettext('MAC address'), + vtype: 'MacAddress', + allowBlank: true, + emptyText: 'auto' + }); + me.advancedColumn2 = [ + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Rate limit') + ' (MB/s)', + minValue: 0, + maxValue: 10*1024, + value: '', + emptyText: 'unlimited', + allowBlank: true + }, + { + xtype: 'proxmoxintegerfield', + name: 'queues', + fieldLabel: 'Multiqueue', + minValue: 1, + maxValue: 8, + value: '', + allowBlank: true + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.NetworkEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.isCreate = me.confid ? false : true; + + var ipanel = Ext.create('PVE.qemu.NetworkInputPanel', { + confid: me.confid, + nodename: nodename, + isCreate: me.isCreate + }); + + Ext.applyIf(me, { + subject: gettext('Network Device'), + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var i, confid; + me.vmconfig = response.result.data; + if (!me.isCreate) { + var value = me.vmconfig[me.confid]; + var network = PVE.Parser.parseQemuNetwork(me.confid, value); + if (!network) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse network options'); + me.close(); + return; + } + ipanel.setNetwork(me.confid, network); + } else { + for (i = 0; i < 100; i++) { + confid = 'net' + i.toString(); + if (!Ext.isDefined(me.vmconfig[confid])) { + me.confid = confid; + break; + } + } + ipanel.setNetwork(me.confid); + } + } + }); + } +}); +Ext.define('PVE.qemu.Smbios1InputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.PVE.qemu.Smbios1InputPanel', + + insideWizard: false, + + smbios1: {}, + + onGetValues: function(values) { + var me = this; + + var params = { + smbios1: PVE.Parser.printQemuSmbios1(values) + }; + + return params; + }, + + setSmbios1: function(data) { + var me = this; + + me.smbios1 = data; + + me.setValues(me.smbios1); + }, + + items: [ + { + xtype: 'textfield', + fieldLabel: 'UUID', + regex: /^[a-fA-F0-9]{8}(?:-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$/, + name: 'uuid' + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Manufacturer'), + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'manufacturer' + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Product'), + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'product' + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Version'), + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'version' + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Serial'), + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'serial' + }, + { + xtype: 'textareafield', + fieldLabel: 'SKU', + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'sku' + }, + { + xtype: 'textareafield', + fieldLabel: gettext('Family'), + fieldStyle: { + height: '2em', + minHeight: '2em' + }, + name: 'family' + } + ] +}); + +Ext.define('PVE.qemu.Smbios1Edit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + var ipanel = Ext.create('PVE.qemu.Smbios1InputPanel', {}); + + Ext.applyIf(me, { + subject: gettext('SMBIOS settings (type1)'), + width: 450, + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + var i, confid; + me.vmconfig = response.result.data; + var value = me.vmconfig.smbios1; + if (value) { + var data = PVE.Parser.parseQemuSmbios1(value); + if (!data) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse smbios options'); + me.close(); + return; + } + ipanel.setSmbios1(data); + } + } + }); + } +}); +Ext.define('PVE.qemu.CDInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuCDInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + var confid = me.confid || (values.controller + values.deviceid); + + me.drive.media = 'cdrom'; + if (values.mediaType === 'iso') { + me.drive.file = values.cdimage; + } else if (values.mediaType === 'cdrom') { + me.drive.file = 'cdrom'; + } else { + me.drive.file = 'none'; + } + + var params = {}; + + params[confid] = PVE.Parser.printQemuDrive(me.drive); + + return params; + }, + + setVMConfig: function(vmconfig) { + var me = this; + + if (me.bussel) { + me.bussel.setVMConfig(vmconfig, 'cdrom'); + } + }, + + setDrive: function(drive) { + var me = this; + + var values = {}; + if (drive.file === 'cdrom') { + values.mediaType = 'cdrom'; + } else if (drive.file === 'none') { + values.mediaType = 'none'; + } else { + values.mediaType = 'iso'; + var match = drive.file.match(/^([^:]+):/); + if (match) { + values.cdstorage = match[1]; + values.cdimage = drive.file; + } + } + + me.drive = drive; + + me.setValues(values); + }, + + setNodename: function(nodename) { + var me = this; + + me.cdstoragesel.setNodename(nodename); + me.cdfilesel.setStorage(undefined, nodename); + }, + + initComponent : function() { + var me = this; + + me.drive = {}; + + var items = []; + + if (!me.confid) { + me.bussel = Ext.create('PVE.form.ControllerSelector', { + noVirtIO: true + }); + items.push(me.bussel); + } + + items.push({ + xtype: 'radiofield', + name: 'mediaType', + inputValue: 'iso', + boxLabel: gettext('Use CD/DVD disc image file (iso)'), + checked: true, + listeners: { + change: function(f, value) { + if (!me.rendered) { + return; + } + me.down('field[name=cdstorage]').setDisabled(!value); + me.down('field[name=cdimage]').setDisabled(!value); + me.down('field[name=cdimage]').validate(); + } + } + }); + + me.cdfilesel = Ext.create('PVE.form.FileSelector', { + name: 'cdimage', + nodename: me.nodename, + storageContent: 'iso', + fieldLabel: gettext('ISO image'), + labelAlign: 'right', + allowBlank: false + }); + + me.cdstoragesel = Ext.create('PVE.form.StorageSelector', { + name: 'cdstorage', + nodename: me.nodename, + fieldLabel: gettext('Storage'), + labelAlign: 'right', + storageContent: 'iso', + allowBlank: false, + autoSelect: me.insideWizard, + listeners: { + change: function(f, value) { + me.cdfilesel.setStorage(value); + } + } + }); + + items.push(me.cdstoragesel); + items.push(me.cdfilesel); + + items.push({ + xtype: 'radiofield', + name: 'mediaType', + inputValue: 'cdrom', + boxLabel: gettext('Use physical CD/DVD Drive') + }); + + items.push({ + xtype: 'radiofield', + name: 'mediaType', + inputValue: 'none', + boxLabel: gettext('Do not use any media') + }); + + me.items = items; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.CDEdit', { + extend: 'Proxmox.window.Edit', + + width: 400, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.isCreate = me.confid ? false : true; + + var ipanel = Ext.create('PVE.qemu.CDInputPanel', { + confid: me.confid, + nodename: nodename + }); + + Ext.applyIf(me, { + subject: 'CD/DVD Drive', + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var value = response.result.data[me.confid]; + var drive = PVE.Parser.parseQemuDrive(me.confid, value); + if (!drive) { + Ext.Msg.alert('Error', 'Unable to parse drive options'); + me.close(); + return; + } + ipanel.setDrive(drive); + } + } + }); + } +}); +/*jslint confusion: true */ +/* 'change' property is assigned a string and then a function */ +Ext.define('PVE.qemu.HDInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveQemuHDInputPanel', + onlineHelp: 'qm_hard_disk', + + insideWizard: false, + + unused: false, // ADD usused disk imaged + + vmconfig: {}, // used to select usused disks + + controller: { + + xclass: 'Ext.app.ViewController', + + onControllerChange: function(field) { + var value = field.getValue(); + + var allowIOthread = value.match(/^(virtio|scsi)/); + this.lookup('iothread').setDisabled(!allowIOthread); + if (!allowIOthread) { + this.lookup('iothread').setValue(false); + } + + var virtio = value.match(/^virtio/); + this.lookup('discard').setDisabled(virtio); + this.lookup('ssd').setDisabled(virtio); + if (virtio) { + this.lookup('discard').setValue(false); + this.lookup('ssd').setValue(false); + } + + this.lookup('scsiController').setVisible(value.match(/^scsi/)); + }, + + control: { + 'field[name=controller]': { + change: 'onControllerChange', + afterrender: 'onControllerChange' + }, + 'field[name=iothread]' : { + change: function(f, value) { + if (!this.getView().insideWizard) { + return; + } + var vmScsiType = value ? 'virtio-scsi-single': 'virtio-scsi-pci'; + this.lookupReference('scsiController').setValue(vmScsiType); + } + } + } + }, + + onGetValues: function(values) { + var me = this; + + var params = {}; + var confid = me.confid || (values.controller + values.deviceid); + + if (me.unused) { + me.drive.file = me.vmconfig[values.unusedId]; + confid = values.controller + values.deviceid; + } else if (me.isCreate) { + if (values.hdimage) { + me.drive.file = values.hdimage; + } else { + me.drive.file = values.hdstorage + ":" + values.disksize; + } + me.drive.format = values.diskformat; + } + + if (values.nobackup) { + me.drive.backup = 'no'; + } else { + delete me.drive.backup; + } + + if (values.noreplicate) { + me.drive.replicate = 'no'; + } else { + delete me.drive.replicate; + } + + if (values.discard) { + me.drive.discard = 'on'; + } else { + delete me.drive.discard; + } + + if (values.ssd) { + me.drive.ssd = 'on'; + } else { + delete me.drive.ssd; + } + + if (values.iothread) { + me.drive.iothread = 'on'; + } else { + delete me.drive.iothread; + } + + if (values.cache) { + me.drive.cache = values.cache; + } else { + delete me.drive.cache; + } + + var names = ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr']; + Ext.Array.each(names, function(name) { + if (values[name]) { + me.drive[name] = values[name]; + } else { + delete me.drive[name]; + } + var burst_name = name + '_max'; + if (values[burst_name] && values[name]) { + me.drive[burst_name] = values[burst_name]; + } else { + delete me.drive[burst_name]; + } + }); + + + params[confid] = PVE.Parser.printQemuDrive(me.drive); + + return params; + }, + + setVMConfig: function(vmconfig) { + var me = this; + + me.vmconfig = vmconfig; + + if (me.bussel) { + me.bussel.setVMConfig(vmconfig); + me.scsiController.setValue(vmconfig.scsihw); + } + if (me.unusedDisks) { + var disklist = []; + Ext.Object.each(vmconfig, function(key, value) { + if (key.match(/^unused\d+$/)) { + disklist.push([key, value]); + } + }); + me.unusedDisks.store.loadData(disklist); + me.unusedDisks.setValue(me.confid); + } + }, + + setDrive: function(drive) { + var me = this; + + me.drive = drive; + + var values = {}; + var match = drive.file.match(/^([^:]+):/); + if (match) { + values.hdstorage = match[1]; + } + + values.hdimage = drive.file; + values.nobackup = !PVE.Parser.parseBoolean(drive.backup, 1); + values.noreplicate = !PVE.Parser.parseBoolean(drive.replicate, 1); + values.diskformat = drive.format || 'raw'; + values.cache = drive.cache || '__default__'; + values.discard = (drive.discard === 'on'); + values.ssd = PVE.Parser.parseBoolean(drive.ssd); + values.iothread = PVE.Parser.parseBoolean(drive.iothread); + + values.mbps_rd = drive.mbps_rd; + values.mbps_wr = drive.mbps_wr; + values.iops_rd = drive.iops_rd; + values.iops_wr = drive.iops_wr; + values.mbps_rd_max = drive.mbps_rd_max; + values.mbps_wr_max = drive.mbps_wr_max; + values.iops_rd_max = drive.iops_rd_max; + values.iops_wr_max = drive.iops_wr_max; + + me.setValues(values); + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + initComponent : function() { + var me = this; + + var labelWidth = 140; + + me.drive = {}; + + me.column1 = []; + me.column2 = []; + + me.advancedColumn1 = []; + me.advancedColumn2 = []; + + if (!me.confid || me.unused) { + me.bussel = Ext.create('PVE.form.ControllerSelector', { + vmconfig: me.insideWizard ? {ide2: 'cdrom'} : {} + }); + me.column1.push(me.bussel); + + me.scsiController = Ext.create('Ext.form.field.Display', { + fieldLabel: gettext('SCSI Controller'), + reference: 'scsiController', + bind: me.insideWizard ? { + value: '{current.scsihw}' + } : undefined, + renderer: PVE.Utils.render_scsihw, + submitValue: false, + hidden: true + }); + me.column1.push(me.scsiController); + } + + if (me.unused) { + me.unusedDisks = Ext.create('Proxmox.form.KVComboBox', { + name: 'unusedId', + fieldLabel: gettext('Disk image'), + matchFieldWidth: false, + listConfig: { + width: 350 + }, + data: [], + allowBlank: false + }); + me.column1.push(me.unusedDisks); + } else if (me.isCreate) { + me.column1.push({ + xtype: 'pveDiskStorageSelector', + storageContent: 'images', + name: 'disk', + nodename: me.nodename, + autoSelect: me.insideWizard + }); + } else { + me.column1.push({ + xtype: 'textfield', + disabled: true, + submitValue: false, + fieldLabel: gettext('Disk image'), + name: 'hdimage' + }); + } + + me.column2.push( + { + xtype: 'CacheTypeSelector', + name: 'cache', + value: '__default__', + fieldLabel: gettext('Cache') + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Discard'), + disabled: me.confid && me.confid.match(/^virtio/), + reference: 'discard', + name: 'discard' + } + ); + + me.advancedColumn1.push( + { + xtype: 'proxmoxcheckbox', + disabled: me.confid && me.confid.match(/^virtio/), + fieldLabel: gettext('SSD emulation'), + labelWidth: labelWidth, + name: 'ssd', + reference: 'ssd' + }, + { + xtype: 'proxmoxcheckbox', + disabled: me.confid && !me.confid.match(/^(virtio|scsi)/), + fieldLabel: 'IO thread', + labelWidth: labelWidth, + reference: 'iothread', + name: 'iothread' + }, + { + xtype: 'numberfield', + name: 'mbps_rd', + minValue: 1, + step: 1, + fieldLabel: gettext('Read limit') + ' (MB/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited') + }, + { + xtype: 'numberfield', + name: 'mbps_wr', + minValue: 1, + step: 1, + fieldLabel: gettext('Write limit') + ' (MB/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited') + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_rd', + minValue: 10, + step: 10, + fieldLabel: gettext('Read limit') + ' (ops/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited') + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_wr', + minValue: 10, + step: 10, + fieldLabel: gettext('Write limit') + ' (ops/s)', + labelWidth: labelWidth, + emptyText: gettext('unlimited') + } + ); + + me.advancedColumn2.push( + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('No backup'), + labelWidth: labelWidth, + name: 'nobackup' + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Skip replication'), + labelWidth: labelWidth, + name: 'noreplicate' + }, + { + xtype: 'numberfield', + name: 'mbps_rd_max', + minValue: 1, + step: 1, + fieldLabel: gettext('Read max burst') + ' (MB)', + labelWidth: labelWidth, + emptyText: gettext('default') + }, + { + xtype: 'numberfield', + name: 'mbps_wr_max', + minValue: 1, + step: 1, + fieldLabel: gettext('Write max burst') + ' (MB)', + labelWidth: labelWidth, + emptyText: gettext('default') + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_rd_max', + minValue: 10, + step: 10, + fieldLabel: gettext('Read max burst') + ' (ops)', + labelWidth: labelWidth, + emptyText: gettext('default') + }, + { + xtype: 'proxmoxintegerfield', + name: 'iops_wr_max', + minValue: 10, + step: 10, + fieldLabel: gettext('Write max burst') + ' (ops)', + labelWidth: labelWidth, + emptyText: gettext('default') + } + ); + + me.callParent(); + } +}); +/*jslint confusion: false */ + +Ext.define('PVE.qemu.HDEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + backgroundDelay: 5, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var unused = me.confid && me.confid.match(/^unused\d+$/); + + me.isCreate = me.confid ? unused : true; + + var ipanel = Ext.create('PVE.qemu.HDInputPanel', { + confid: me.confid, + nodename: nodename, + unused: unused, + isCreate: me.isCreate + }); + + var subject; + if (unused) { + me.subject = gettext('Unused Disk'); + } else if (me.isCreate) { + me.subject = gettext('Hard Disk'); + } else { + me.subject = gettext('Hard Disk') + ' (' + me.confid + ')'; + } + + me.items = [ ipanel ]; + + me.callParent(); + /*jslint confusion: true*/ + /* 'data' is assigned an empty array in same file, and here we + * use it like an object + */ + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var value = response.result.data[me.confid]; + var drive = PVE.Parser.parseQemuDrive(me.confid, value); + if (!drive) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse drive options'); + me.close(); + return; + } + ipanel.setDrive(drive); + me.isValid(); // trigger validation + } + } + }); + /*jslint confusion: false*/ + } +}); +Ext.define('PVE.window.HDResize', { + extend: 'Ext.window.Window', + + resizable: false, + + resize_disk: function(disk, size) { + var me = this; + var params = { disk: disk, size: '+' + size + 'G' }; + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/resize', + waitMsgTarget: me, + method: 'PUT', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + me.close(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + var items = [ + { + xtype: 'displayfield', + name: 'disk', + value: me.disk, + fieldLabel: gettext('Disk'), + vtype: 'StorageId', + allowBlank: false + } + ]; + + me.hdsizesel = Ext.createWidget('numberfield', { + name: 'size', + minValue: 0, + maxValue: 128*1024, + decimalPrecision: 3, + value: '0', + fieldLabel: gettext('Size Increment') + ' (GiB)', + allowBlank: false + }); + + items.push(me.hdsizesel); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 140, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + me.title = gettext('Resize disk'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Resize disk'), + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.resize_disk(me.disk, values.size); + } + } + }); + + Ext.apply(me, { + modal: true, + width: 250, + height: 150, + border: false, + layout: 'fit', + buttons: [ submitBtn ], + items: [ me.formPanel ] + }); + + + me.callParent(); + + if (!me.disk) { + return; + } + + } +}); +Ext.define('PVE.window.HDMove', { + extend: 'Ext.window.Window', + + resizable: false, + + + move_disk: function(disk, storage, format, delete_disk) { + var me = this; + var qemu = (me.type === 'qemu'); + var params = {}; + params.storage = storage; + params[qemu ? 'disk':'volume'] = disk; + + if (format && qemu) { + params.format = format; + } + + if (delete_disk) { + params['delete'] = 1; + } + + var url = '/nodes/' + me.nodename + '/' + me.type + '/' + me.vmid + '/'; + url += qemu ? 'move_disk' : 'move_volume'; + + Proxmox.Utils.API2Request({ + params: params, + url: url, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + win.on('destroy', function() { me.close(); }); + } + }); + + }, + + initComponent : function() { + var me = this; + + var diskarray = []; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + if (!me.type) { + me.type = 'qemu'; + } + + var qemu = (me.type === 'qemu'); + + var items = [ + { + xtype: 'displayfield', + name: qemu ? 'disk' : 'volume', + value: me.disk, + fieldLabel: qemu ? gettext('Disk') : gettext('Mount Point'), + vtype: 'StorageId', + allowBlank: false + } + ]; + + items.push({ + xtype: 'pveDiskStorageSelector', + storageLabel: gettext('Target Storage'), + nodename: me.nodename, + storageContent: qemu ? 'images' : 'rootdir', + hideSize: true + }); + + items.push({ + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Delete source'), + name: 'deleteDisk', + uncheckedValue: 0, + checked: false + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + me.title = qemu ? gettext("Move disk") : gettext('Move Volume'); + submitBtn = Ext.create('Ext.Button', { + text: me.title, + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.move_disk(me.disk, values.hdstorage, values.diskformat, + values.deleteDisk); + } + } + }); + + Ext.apply(me, { + modal: true, + width: 350, + border: false, + layout: 'fit', + buttons: [ submitBtn ], + items: [ me.formPanel ] + }); + + + me.callParent(); + + me.mon(me.formPanel, 'validitychange', function(fp, isValid) { + submitBtn.setDisabled(!isValid); + }); + + me.formPanel.isValid(); + } +}); +Ext.define('PVE.qemu.EFIDiskInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveEFIDiskInputPanel', + + insideWizard: false, + + unused: false, // ADD usused disk imaged + + vmconfig: {}, // used to select usused disks + + onGetValues: function(values) { + var me = this; + + var confid = 'efidisk0'; + + if (values.hdimage) { + me.drive.file = values.hdimage; + } else { + // we use 1 here, because for efi the size gets overridden from the backend + me.drive.file = values.hdstorage + ":1"; + } + + me.drive.format = values.diskformat; + var params = {}; + params[confid] = PVE.Parser.printQemuDrive(me.drive); + return params; + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + initComponent : function() { + var me = this; + + me.drive = {}; + + me.items= []; + + me.items.push({ + xtype: 'pveDiskStorageSelector', + name: 'efidisk0', + storageContent: 'images', + nodename: me.nodename, + hideSize: true + }); + me.callParent(); + } +}); + +Ext.define('PVE.qemu.EFIDiskEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + subject: gettext('EFI Disk'), + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.items = [{ + xtype: 'pveEFIDiskInputPanel', + onlineHelp: 'qm_bios_and_uefi', + confid: me.confid, + nodename: nodename, + isCreate: true + }]; + + me.callParent(); + } +}); +Ext.define('PVE.qemu.DisplayInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveDisplayInputPanel', + + onGetValues: function(values) { + var ret = PVE.Parser.printPropertyString(values, 'type'); + if (ret === '') { + return { + 'delete': 'vga' + }; + } + return { + vga: ret + }; + }, + + items: [{ + name: 'type', + xtype: 'proxmoxKVComboBox', + value: '__default__', + deleteEmpty: false, + fieldLabel: gettext('Graphic card'), + comboItems: PVE.Utils.kvm_vga_driver_array(), + validator: function() { + var v = this.getValue(); + var cfg = this.up('proxmoxWindowEdit').vmconfig || {}; + + if (v.match(/^serial\d+$/) && (!cfg[v] || cfg[v] !== 'socket')) { + var fmt = gettext("Serial interface '{0}' is not correctly configured."); + return Ext.String.format(fmt, v); + } + return true; + }, + listeners: { + change: function(cb, val) { + var me = this.up('panel'); + if (!val) { + return; + } + var disable = false; + var emptyText = Proxmox.Utils.defaultText; + switch (val) { + case "cirrus": + emptyText = "4"; + break; + case "std": + emptyText = "16"; + break; + case "qxl": + case "qxl2": + case "qxl3": + case "qxl4": + emptyText = "16"; + break; + case "vmware": + emptyText = "16"; + break; + case "none": + case "serial0": + case "serial1": + case "serial2": + case "serial3": + emptyText = 'N/A'; + disable = true; + break; + case "virtio": + emptyText = "256"; + break; + default: + break; + } + var memoryfield = me.down('field[name=memory]'); + memoryfield.setEmptyText(emptyText); + memoryfield.setDisabled(disable); + } + } + },{ + xtype: 'proxmoxintegerfield', + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('Memory') + ' (MiB)', + minValue: 4, + maxValue: 512, + step: 4, + name: 'memory' + }] +}); + +Ext.define('PVE.qemu.DisplayEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + subject: gettext('Display'), + width: 350, + + items: [{ + xtype: 'pveDisplayInputPanel' + }], + + initComponent : function() { + var me = this; + + me.callParent(); + + me.load({ + success: function(response) { + me.vmconfig = response.result.data; + var vga = me.vmconfig.vga || '__default__'; + me.setValues(PVE.Parser.parsePropertyString(vga, 'type')); + } + }); + } +}); +Ext.define('PVE.qemu.KeyboardEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.applyIf(me, { + subject: gettext('Keyboard Layout'), + items: { + xtype: 'VNCKeyboardSelector', + name: 'keyboard', + value: '__default__', + fieldLabel: gettext('Keyboard Layout') + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('PVE.qemu.HardwareView', { + extend: 'Proxmox.grid.PendingObjectGrid', + alias: ['widget.PVE.qemu.HardwareView'], + + onlineHelp: 'qm_virtual_machines_settings', + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = rows[key] || {}; + var iconCls = rowdef.iconCls; + var icon = ''; + var txt = (rowdef.header || key); + + metaData.tdAttr = "valign=middle"; + + if (rowdef.tdCls) { + metaData.tdCls = rowdef.tdCls; + if (rowdef.tdCls == 'pve-itype-icon-storage') { + var value = me.getObjectValue(key, '', false); + if (value === '') { + value = me.getObjectValue(key, '', true); + } + if (value.match(/vm-.*-cloudinit/)) { + metaData.tdCls = 'pve-itype-icon-cloud'; + return rowdef.cloudheader; + } else if (value.match(/media=cdrom/)) { + metaData.tdCls = 'pve-itype-icon-cdrom'; + return rowdef.cdheader; + } + } + } else if (iconCls) { + icon = ""; + metaData.tdCls += " pve-itype-fa"; + } + return icon + txt; + }, + + initComponent : function() { + var me = this; + var i, confid; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + var diskCap = caps.vms['VM.Config.Disk']; + + /*jslint confusion: true */ + var rows = { + memory: { + header: gettext('Memory'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined, + never_delete: true, + defaultValue: '512', + tdCls: 'pve-itype-icon-memory', + group: 2, + multiKey: ['memory', 'balloon', 'shares'], + renderer: function(value, metaData, record, ri, ci, store, pending) { + var res = ''; + + var max = me.getObjectValue('memory', 512, pending); + var balloon = me.getObjectValue('balloon', undefined, pending); + var shares = me.getObjectValue('shares', undefined, pending); + + res = Proxmox.Utils.format_size(max*1024*1024); + + if (balloon !== undefined && balloon > 0) { + res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res; + + if (shares) { + res += ' [shares=' + shares +']'; + } + } else if (balloon === 0) { + res += ' [balloon=0]'; + } + return res; + } + }, + sockets: { + header: gettext('Processors'), + never_delete: true, + editor: (caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']) ? + 'PVE.qemu.ProcessorEdit' : undefined, + tdCls: 'pve-itype-icon-processor', + group: 3, + defaultValue: '1', + multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'], + renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) { + + var sockets = me.getObjectValue('sockets', 1, pending); + var model = me.getObjectValue('cpu', undefined, pending); + var cores = me.getObjectValue('cores', 1, pending); + var numa = me.getObjectValue('numa', undefined, pending); + var vcpus = me.getObjectValue('vcpus', undefined, pending); + var cpulimit = me.getObjectValue('cpulimit', undefined, pending); + var cpuunits = me.getObjectValue('cpuunits', undefined, pending); + + var res = Ext.String.format('{0} ({1} sockets, {2} cores)', + sockets*cores, sockets, cores); + + if (model) { + res += ' [' + model + ']'; + } + + if (numa) { + res += ' [numa=' + numa +']'; + } + + if (vcpus) { + res += ' [vcpus=' + vcpus +']'; + } + + if (cpulimit) { + res += ' [cpulimit=' + cpulimit +']'; + } + + if (cpuunits) { + res += ' [cpuunits=' + cpuunits +']'; + } + + return res; + } + }, + bios: { + header: 'BIOS', + group: 4, + never_delete: true, + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined, + defaultValue: '', + iconCls: 'microchip', + renderer: PVE.Utils.render_qemu_bios + }, + vga: { + header: gettext('Display'), + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined, + never_delete: true, + tdCls: 'pve-itype-icon-display', + group:5, + defaultValue: '', + renderer: PVE.Utils.render_kvm_vga_driver + }, + machine: { + header: gettext('Machine'), + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Machine'), + width: 350, + items: [{ + xtype: 'proxmoxKVComboBox', + name: 'machine', + value: '__default__', + fieldLabel: gettext('Machine'), + comboItems: [ + ['__default__', PVE.Utils.render_qemu_machine('')], + ['q35', 'q35'] + ] + }]} : undefined, + iconCls: 'cogs', + never_delete: true, + group: 6, + defaultValue: '', + renderer: PVE.Utils.render_qemu_machine + }, + scsihw: { + header: gettext('SCSI Controller'), + iconCls: 'database', + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined, + renderer: PVE.Utils.render_scsihw, + group: 7, + never_delete: true, + defaultValue: '' + }, + cores: { + visible: false + }, + cpu: { + visible: false + }, + numa: { + visible: false + }, + balloon: { + visible: false + }, + hotplug: { + visible: false + }, + vcpus: { + visible: false + }, + cpuunits: { + visible: false + }, + cpulimit: { + visible: false + }, + shares: { + visible: false + } + }; + /*jslint confusion: false */ + + PVE.Utils.forEachBus(undefined, function(type, id) { + var confid = type + id; + rows[confid] = { + group: 10, + tdCls: 'pve-itype-icon-storage', + editor: 'PVE.qemu.HDEdit', + never_delete: caps.vms['VM.Config.Disk'] ? false : true, + header: gettext('Hard Disk') + ' (' + confid +')', + cdheader: gettext('CD/DVD Drive') + ' (' + confid +')', + cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')' + }; + }); + for (i = 0; i < 32; i++) { + confid = "net" + i.toString(); + rows[confid] = { + group: 15, + order: i, + tdCls: 'pve-itype-icon-network', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined, + never_delete: caps.vms['VM.Config.Network'] ? false : true, + header: gettext('Network Device') + ' (' + confid +')' + }; + } + rows.efidisk0 = { + group: 20, + tdCls: 'pve-itype-icon-storage', + editor: null, + never_delete: caps.vms['VM.Config.Disk'] ? false : true, + header: gettext('EFI Disk') + }; + for (i = 0; i < 5; i++) { + confid = "usb" + i.toString(); + rows[confid] = { + group: 25, + order: i, + tdCls: 'pve-itype-icon-usb', + editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined, + never_delete: caps.nodes['Sys.Console'] ? false : true, + header: gettext('USB Device') + ' (' + confid + ')' + }; + } + for (i = 0; i < 4; i++) { + confid = "hostpci" + i.toString(); + rows[confid] = { + group: 30, + order: i, + tdCls: 'pve-itype-icon-pci', + never_delete: caps.nodes['Sys.Console'] ? false : true, + editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined, + header: gettext('PCI Device') + ' (' + confid + ')' + }; + } + for (i = 0; i < 4; i++) { + confid = "serial" + i.toString(); + rows[confid] = { + group: 35, + order: i, + tdCls: 'pve-itype-icon-serial', + never_delete: caps.nodes['Sys.Console'] ? false : true, + header: gettext('Serial Port') + ' (' + confid + ')' + }; + } + for (i = 0; i < 256; i++) { + rows["unused" + i.toString()] = { + group: 99, + order: i, + tdCls: 'pve-itype-icon-storage', + editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined, + header: gettext('Unused Disk') + ' ' + i.toString() + }; + } + + var sorterFn = function(rec1, rec2) { + var v1 = rec1.data.key; + var v2 = rec2.data.key; + var g1 = rows[v1].group || 0; + var g2 = rows[v2].group || 0; + var order1 = rows[v1].order || 0; + var order2 = rows[v2].order || 0; + + if ((g1 - g2) !== 0) { + return g1 - g2; + } + + if ((order1 - order2) !== 0) { + return order1 - order2; + } + + if (v1 > v2) { + return 1; + } else if (v1 < v2) { + return -1; + } else { + return 0; + } + }; + + var reload = function() { + me.rstore.load(); + }; + + var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config'; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rowdef = rows[rec.data.key]; + if (!rowdef.editor) { + return; + } + + var editor = rowdef.editor; + if (rowdef.tdCls == 'pve-itype-icon-storage') { + var value = me.getObjectValue(rec.data.key, '', true); + if (value.match(/vm-.*-cloudinit/)) { + return; + } else if (value.match(/media=cdrom/)) { + editor = 'PVE.qemu.CDEdit'; + } else if (!diskCap) { + return; + } + } + + var win; + + if (Ext.isString(editor)) { + win = Ext.create(editor, { + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + baseurl + }); + } else { + var config = Ext.apply({ + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + baseurl + }, rowdef.editor); + win = Ext.createWidget(rowdef.editor.xtype, config); + win.load(); + } + + win.show(); + win.on('destroy', reload); + }; + + var run_resize = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.HDResize', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid + }); + + win.show(); + + win.on('destroy', reload); + }; + + var run_move = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.HDMove', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid + }); + + win.show(); + + win.on('destroy', reload); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + selModel: sm, + disabled: true, + handler: run_editor + }); + + var resize_btn = new Proxmox.button.Button({ + text: gettext('Resize disk'), + selModel: sm, + disabled: true, + handler: run_resize + }); + + var move_btn = new Proxmox.button.Button({ + text: gettext('Move disk'), + selModel: sm, + disabled: true, + handler: run_move + }); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + defaultText: gettext('Remove'), + altText: gettext('Detach'), + selModel: sm, + disabled: true, + dangerous: true, + RESTMethod: 'PUT', + confirmMsg: function(rec) { + var warn = gettext('Are you sure you want to remove entry {0}'); + if (this.text === this.altText) { + warn = gettext('Are you sure you want to detach entry {0}'); + } + + var entry = rec.data.key; + var msg = Ext.String.format(warn, "'" + + me.renderKey(entry, {}, rec) + "'"); + + if (entry.match(/^unused\d+$/)) { + msg += " " + gettext('This will permanently erase all data.'); + } + + return msg; + }, + handler: function(b, e, rec) { + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: b.RESTMethod, + params: { + 'delete': rec.data.key + }, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + success: function(response, options) { + if (b.RESTMethod === 'POST') { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { + upid: upid, + listeners: { + destroy: function () { + me.reload(); + } + } + }); + win.show(); + } + } + }); + }, + listeners: { + render: function(btn) { + // hack: calculate an optimal button width on first display + // to prevent the whole toolbar to move when we switch + // between the "Remove" and "Detach" labels + var def = btn.getSize().width; + + btn.setText(btn.altText); + var alt = btn.getSize().width; + + btn.setText(btn.defaultText); + + var optimal = alt > def ? alt : def; + btn.setSize({ width: optimal }); + } + } + }); + + var revert_btn = new Proxmox.button.Button({ + text: gettext('Revert'), + selModel: sm, + disabled: true, + handler: function(b, e, rec) { + var rowdef = me.rows[rec.data.key] || {}; + var keys = rowdef.multiKey || [ rec.data.key ]; + var revert = keys.join(','); + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'revert': revert + }, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert('Error',response.htmlStatus); + } + }); + } + }); + + var efidisk_menuitem = Ext.create('Ext.menu.Item',{ + text: gettext('EFI Disk'), + iconCls: 'pve-itype-icon-storage', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + + var rstoredata = me.rstore.getData().map; + // check if ovmf is configured + if (rstoredata.bios && rstoredata.bios.data.value === 'ovmf') { + var win = Ext.create('PVE.qemu.EFIDiskEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } else { + Ext.Msg.alert('Error',gettext('Please select OVMF(UEFI) as BIOS first.')); + } + + } + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + // disable button when we have an efidisk already + // disable is ok in this case, because you can instantly + // see that there is already one + efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined); + // en/disable usb add button + var usbcount = 0; + var pcicount = 0; + var hasCloudInit = false; + me.rstore.getData().items.forEach(function(item){ + if (/^usb\d+/.test(item.id)) { + usbcount++; + } else if (/^hostpci\d+/.test(item.id)) { + pcicount++; + } + if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) { + hasCloudInit = true; + } + }); + + // heuristic only for disabling some stuff, the backend has the final word. + var noSysConsolePerm = !caps.nodes['Sys.Console']; + + me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5)); + me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4)); + me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit); + + if (!rec) { + remove_btn.disable(); + edit_btn.disable(); + resize_btn.disable(); + move_btn.disable(); + revert_btn.disable(); + return; + } + var key = rec.data.key; + var value = rec.data.value; + var rowdef = rows[key]; + + var pending = rec.data['delete'] || me.hasPendingChanges(key); + var isCDRom = (value && !!value.toString().match(/media=cdrom/)); + var isUnusedDisk = key.match(/^unused\d+/); + var isUsedDisk = !isUnusedDisk && + rowdef.tdCls == 'pve-itype-icon-storage' && + !isCDRom; + + var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/)); + + var isEfi = (key === 'efidisk0'); + + remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true) || (isUnusedDisk && !diskCap)); + remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText); + remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT'; + + edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit || (!isCDRom && !diskCap)); + + resize_btn.setDisabled(pending || !isUsedDisk || !diskCap); + + move_btn.setDisabled(pending || !isUsedDisk || !diskCap); + + revert_btn.setDisabled(!pending); + + }; + + Ext.apply(me, { + url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending', + interval: 5000, + selModel: sm, + run_editor: run_editor, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Hard Disk'), + iconCls: 'pve-itype-icon-storage', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + var win = Ext.create('PVE.qemu.HDEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('CD/DVD Drive'), + iconCls: 'pve-itype-icon-cdrom', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + var win = Ext.create('PVE.qemu.CDEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('Network Device'), + iconCls: 'pve-itype-icon-network', + disabled: !caps.vms['VM.Config.Network'], + handler: function() { + var win = Ext.create('PVE.qemu.NetworkEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode, + isCreate: true + }); + win.on('destroy', reload); + win.show(); + } + }, + efidisk_menuitem, + { + text: gettext('USB Device'), + itemId: 'addusb', + iconCls: 'pve-itype-icon-usb', + disabled: !caps.nodes['Sys.Console'], + handler: function() { + var win = Ext.create('PVE.qemu.USBEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('PCI Device'), + itemId: 'addpci', + iconCls: 'pve-itype-icon-pci', + disabled: !caps.nodes['Sys.Console'], + handler: function() { + var win = Ext.create('PVE.qemu.PCIEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('Serial Port'), + itemId: 'addserial', + iconCls: 'pve-itype-icon-serial', + disabled: !caps.vms['VM.Config.Options'], + handler: function() { + var win = Ext.create('PVE.qemu.SerialEdit', { + url: '/api2/extjs/' + baseurl + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('CloudInit Drive'), + itemId: 'addci', + iconCls: 'pve-itype-icon-cloud', + disabled: !caps.nodes['Sys.Console'], + handler: function() { + var win = Ext.create('PVE.qemu.CIDriveEdit', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } + } + ] + }) + }, + remove_btn, + edit_btn, + resize_btn, + move_btn, + revert_btn + ], + rows: rows, + sorterFn: sorterFn, + listeners: { + itemdblclick: run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + + me.mon(me.rstore, 'refresh', function() { + set_button_status(); + }); + } +}); +Ext.define('PVE.qemu.ScsiHwEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.applyIf(me, { + subject: gettext('SCSI Controller Type'), + items: { + xtype: 'pveScsiHwSelector', + name: 'scsihw', + value: '__default__', + fieldLabel: gettext('Type') + } + }); + + me.callParent(); + + me.load(); + } +}); +Ext.define('PVE.qemu.BiosEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pveQemuBiosEdit', + + initComponent : function() { + var me = this; + + var EFIHint = Ext.createWidget({ + xtype: 'displayfield', //submitValue is false, so we don't get submitted + userCls: 'pve-hint', + value: 'You need to add an EFI disk for storing the ' + + 'EFI settings. See the online help for details.', + hidden: true + }); + + Ext.applyIf(me, { + subject: 'BIOS', + items: [ { + xtype: 'pveQemuBiosSelector', + onlineHelp: 'qm_bios_and_uefi', + name: 'bios', + value: '__default__', + fieldLabel: 'BIOS', + listeners: { + 'change' : function(field, newValue) { + if (newValue == 'ovmf') { + Proxmox.Utils.API2Request({ + url : me.url, + method : 'GET', + failure : function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success : function(response, opts) { + var vmConfig = response.result.data; + // there can be only one + if (!vmConfig.efidisk0) { + EFIHint.setVisible(true); + } + } + }); + } else { + if (EFIHint.isVisible()) { + EFIHint.setVisible(false); + } + } + } + } + }, + EFIHint + ] }); + + me.callParent(); + + me.load(); + + } +}); +/*jslint confusion: true */ +Ext.define('PVE.qemu.Options', { + extend: 'Proxmox.grid.PendingObjectGrid', + alias: ['widget.PVE.qemu.Options'], + + onlineHelp: 'qm_options', + + initComponent : function() { + var me = this; + var i; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + var rows = { + name: { + required: true, + defaultValue: me.pveSelNode.data.name, + header: gettext('Name'), + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Name'), + items: { + xtype: 'inputpanel', + items:{ + xtype: 'textfield', + name: 'name', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Name'), + allowBlank: true + }, + onGetValues: function(values) { + var params = values; + if (values.name === undefined || + values.name === null || + values.name === '') { + params = { 'delete':'name'}; + } + return params; + } + } + } : undefined + }, + onboot: { + header: gettext('Start at boot'), + defaultValue: '', + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Start at boot'), + items: { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Start at boot') + } + } : undefined + }, + startup: { + header: gettext('Start/Shutdown order'), + defaultValue: '', + renderer: PVE.Utils.render_kvm_startup, + editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ? + { + xtype: 'pveWindowStartupEdit', + onlineHelp: 'qm_startup_and_shutdown' + } : undefined + }, + ostype: { + header: gettext('OS Type'), + editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.OSTypeEdit' : undefined, + renderer: PVE.Utils.render_kvm_ostype, + defaultValue: 'other' + }, + bootdisk: { + visible: false + }, + boot: { + header: gettext('Boot Order'), + defaultValue: 'cdn', + editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.BootOrderEdit' : undefined, + multiKey: ['boot', 'bootdisk'], + renderer: function(order, metaData, record, rowIndex, colIndex, store, pending) { + var i; + var text = ''; + var bootdisk = me.getObjectValue('bootdisk', undefined, pending); + order = order || 'cdn'; + for (i = 0; i < order.length; i++) { + var sel = order.substring(i, i + 1); + if (text) { + text += ', '; + } + if (sel === 'c') { + if (bootdisk) { + text += "Disk '" + bootdisk + "'"; + } else { + text += "Disk"; + } + } else if (sel === 'n') { + text += 'Network'; + } else if (sel === 'a') { + text += 'Floppy'; + } else if (sel === 'd') { + text += 'CD-ROM'; + } else { + text += sel; + } + } + return text; + } + }, + tablet: { + header: gettext('Use tablet for pointer'), + defaultValue: true, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Use tablet for pointer'), + items: { + xtype: 'proxmoxcheckbox', + name: 'tablet', + checked: true, + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + hotplug: { + header: gettext('Hotplug'), + defaultValue: 'disk,network,usb', + renderer: PVE.Utils.render_hotplug_features, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Hotplug'), + items: { + xtype: 'pveHotplugFeatureSelector', + name: 'hotplug', + value: '', + multiSelect: true, + fieldLabel: gettext('Hotplug'), + allowBlank: true + } + } : undefined + }, + acpi: { + header: gettext('ACPI support'), + defaultValue: true, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('ACPI support'), + items: { + xtype: 'proxmoxcheckbox', + name: 'acpi', + checked: true, + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + kvm: { + header: gettext('KVM hardware virtualization'), + defaultValue: true, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.HWType'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('KVM hardware virtualization'), + items: { + xtype: 'proxmoxcheckbox', + name: 'kvm', + checked: true, + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + freeze: { + header: gettext('Freeze CPU at startup'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.PowerMgmt'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Freeze CPU at startup'), + items: { + xtype: 'proxmoxcheckbox', + name: 'freeze', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + labelWidth: 140, + fieldLabel: gettext('Freeze CPU at startup') + } + } : undefined + }, + localtime: { + header: gettext('Use local time for RTC'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Use local time for RTC'), + items: { + xtype: 'proxmoxcheckbox', + name: 'localtime', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + labelWidth: 140, + fieldLabel: gettext('Use local time for RTC') + } + } : undefined + }, + startdate: { + header: gettext('RTC start date'), + defaultValue: 'now', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('RTC start date'), + items: { + xtype: 'proxmoxtextfield', + name: 'startdate', + deleteEmpty: true, + value: 'now', + fieldLabel: gettext('RTC start date'), + vtype: 'QemuStartDate', + allowBlank: true + } + } : undefined + }, + smbios1: { + header: gettext('SMBIOS settings (type1)'), + defaultValue: '', + renderer: Ext.String.htmlEncode, + editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.Smbios1Edit' : undefined + }, + agent: { + header: gettext('Qemu Agent'), + defaultValue: false, + renderer: PVE.Utils.render_qga_features, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Qemu Agent'), + items: { + xtype: 'pveAgentFeatureSelector', + name: 'agent' + } + } : undefined + }, + protection: { + header: gettext('Protection'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Protection'), + items: { + xtype: 'proxmoxcheckbox', + name: 'protection', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + hookscript: { + header: gettext('Hookscript') + } + }; + + var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config'; + + var edit_btn = new Ext.Button({ + text: gettext('Edit'), + disabled: true, + handler: function() { me.run_editor(); } + }); + + var revert_btn = new Proxmox.button.Button({ + text: gettext('Revert'), + disabled: true, + handler: function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rowdef = me.rows[rec.data.key] || {}; + var keys = rowdef.multiKey || [ rec.data.key ]; + var revert = keys.join(','); + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'revert': revert + }, + callback: function() { + me.reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert('Error',response.htmlStatus); + } + }); + } + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + + var key = rec.data.key; + var pending = rec.data['delete'] || me.hasPendingChanges(key); + var rowdef = rows[key]; + + edit_btn.setDisabled(!rowdef.editor); + revert_btn.setDisabled(!pending); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + nodename + "/qemu/" + vmid + "/pending", + interval: 5000, + cwidth1: 250, + tbar: [ edit_btn, revert_btn ], + rows: rows, + editorConfig: { + url: "/api2/extjs/" + baseurl + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + me.rstore.on('datachanged', function() { + set_button_status(); + }); + } +}); + +Ext.define('PVE.window.Snapshot', { + extend: 'Ext.window.Window', + + resizable: false, + + // needed for finding the reference to submitbutton + // because we do not have a controller + referenceHolder: true, + defaultButton: 'submitbutton', + defaultFocus: 'field', + + take_snapshot: function(snapname, descr, vmstate) { + var me = this; + var params = { snapname: snapname, vmstate: vmstate ? 1 : 0 }; + if (descr) { + params.description = descr; + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot", + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + me.close(); + } + }); + }, + + update_snapshot: function(snapname, descr) { + var me = this; + Proxmox.Utils.API2Request({ + params: { description: descr }, + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" + + snapname + '/config', + waitMsgTarget: me, + method: 'PUT', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + me.close(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + var summarystore = Ext.create('Ext.data.Store', { + model: 'KeyValue', + sorters: [ + { + property : 'key', + direction: 'ASC' + } + ] + }); + + var items = [ + { + xtype: me.snapname ? 'displayfield' : 'textfield', + name: 'snapname', + value: me.snapname, + fieldLabel: gettext('Name'), + vtype: 'ConfigId', + allowBlank: false + } + ]; + + if (me.snapname) { + items.push({ + xtype: 'displayfield', + name: 'snaptime', + renderer: PVE.Utils.render_timestamp_human_readable, + fieldLabel: gettext('Timestamp') + }); + } else { + items.push({ + xtype: 'proxmoxcheckbox', + name: 'vmstate', + uncheckedValue: 0, + defaultValue: 0, + checked: 1, + fieldLabel: gettext('Include RAM') + }); + } + + items.push({ + xtype: 'textareafield', + grow: true, + name: 'description', + fieldLabel: gettext('Description') + }); + + if (me.snapname) { + items.push({ + title: gettext('Settings'), + xtype: 'grid', + height: 200, + store: summarystore, + columns: [ + {header: gettext('Key'), width: 150, dataIndex: 'key'}, + {header: gettext('Value'), flex: 1, dataIndex: 'value'} + ] + }); + } + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + if (me.snapname) { + me.title = gettext('Edit') + ': ' + gettext('Snapshot'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Update'), + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.update_snapshot(me.snapname, values.description); + } + } + }); + } else { + me.title ="VM " + me.vmid + ': ' + gettext('Take Snapshot'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Take Snapshot'), + reference: 'submitbutton', + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.take_snapshot(values.snapname, values.description, values.vmstate); + } + } + }); + } + + Ext.apply(me, { + modal: true, + width: 450, + border: false, + layout: 'fit', + buttons: [ submitBtn ], + items: [ me.formPanel ] + }); + + if (me.snapname) { + Ext.apply(me, { + width: 620, + height: 420 + }); + } + + me.callParent(); + + if (!me.snapname) { + return; + } + + // else load data + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + "/snapshot/" + + me.snapname + '/config', + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.close(); + }, + success: function(response, options) { + var data = response.result.data; + var kvarray = []; + Ext.Object.each(data, function(key, value) { + if (key === 'description' || key === 'snaptime') { + return; + } + kvarray.push({ key: key, value: value }); + }); + + summarystore.suspendEvents(); + summarystore.add(kvarray); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh', summarystore); + + form.findField('snaptime').setValue(data.snaptime); + form.findField('description').setValue(data.description); + } + }); + } +}); +Ext.define('PVE.qemu.SnapshotTree', { + extend: 'Ext.tree.Panel', + alias: ['widget.pveQemuSnapshotTree'], + + load_delay: 3000, + + old_digest: 'invalid', + + stateful: true, + stateId: 'grid-qemu-snapshots', + + sorterFn: function(rec1, rec2) { + var v1 = rec1.data.snaptime; + var v2 = rec2.data.snaptime; + + if (rec1.data.name === 'current') { + return 1; + } + if (rec2.data.name === 'current') { + return -1; + } + + return (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)); + }, + + reload: function(repeat) { + var me = this; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot', + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + me.load_task.delay(me.load_delay); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + var digest = 'invalid'; + var idhash = {}; + var root = { name: '__root', expanded: true, children: [] }; + Ext.Array.each(response.result.data, function(item) { + item.leaf = true; + item.children = []; + if (item.name === 'current') { + digest = item.digest + item.running; + if (item.running) { + item.iconCls = 'fa fa-fw fa-desktop x-fa-tree-running'; + } else { + item.iconCls = 'fa fa-fw fa-desktop x-fa-tree'; + } + } else { + item.iconCls = 'fa fa-fw fa-history x-fa-tree'; + } + idhash[item.name] = item; + }); + + if (digest !== me.old_digest) { + me.old_digest = digest; + + Ext.Array.each(response.result.data, function(item) { + if (item.parent && idhash[item.parent]) { + var parent_item = idhash[item.parent]; + parent_item.children.push(item); + parent_item.leaf = false; + parent_item.expanded = true; + parent_item.expandable = false; + } else { + root.children.push(item); + } + }); + + me.setRootNode(root); + } + + me.load_task.delay(me.load_delay); + } + }); + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/feature', + params: { feature: 'snapshot' }, + method: 'GET', + success: function(response, options) { + var res = response.result.data; + if (res.hasFeature) { + var snpBtns = Ext.ComponentQuery.query('#snapshotBtn'); + snpBtns.forEach(function(item){ + item.enable(); + }); + } + } + }); + + + }, + + listeners: { + beforestatesave: function(grid, state, eopts) { + // extjs cannot serialize functions, + // so a the sorter with only the sorterFn will + // not be a valid sorter when restoring the state + delete state.storeState.sorters; + } + }, + + initComponent: function() { + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + me.vmid = me.pveSelNode.data.vmid; + if (!me.vmid) { + throw "no VM ID specified"; + } + + me.load_task = new Ext.util.DelayedTask(me.reload, me); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var valid_snapshot = function(record) { + return record && record.data && record.data.name && + record.data.name !== 'current'; + }; + + var valid_snapshot_rollback = function(record) { + return record && record.data && record.data.name && + record.data.name !== 'current' && !record.data.snapstate; + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (valid_snapshot(rec)) { + var win = Ext.create('PVE.window.Snapshot', { + snapname: rec.data.name, + nodename: me.nodename, + vmid: me.vmid + }); + win.show(); + me.mon(win, 'close', me.reload, me); + } + }; + + var editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: valid_snapshot, + handler: run_editor + }); + + var rollbackBtn = new Proxmox.button.Button({ + text: gettext('Rollback'), + disabled: true, + selModel: sm, + enableFn: valid_snapshot_rollback, + confirmMsg: function(rec) { + return Proxmox.Utils.format_task_description('qmrollback', me.vmid) + + " '" + rec.data.name + "'"; + }, + handler: function(btn, event) { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var snapname = rec.data.name; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname + '/rollback', + method: 'POST', + waitMsgTarget: me, + callback: function() { + me.reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + } + }); + } + }); + + var removeBtn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.name + "'"); + return msg; + }, + enableFn: valid_snapshot, + handler: function(btn, event) { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var snapname = rec.data.name; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/qemu/' + me.vmid + '/snapshot/' + snapname, + method: 'DELETE', + waitMsgTarget: me, + callback: function() { + me.reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + } + }); + } + }); + + var snapshotBtn = Ext.create('Ext.Button', { + itemId: 'snapshotBtn', + text: gettext('Take Snapshot'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.window.Snapshot', { + nodename: me.nodename, + vmid: me.vmid + }); + win.show(); + } + }); + + Ext.apply(me, { + layout: 'fit', + rootVisible: false, + animate: false, + sortableColumns: false, + selModel: sm, + tbar: [ snapshotBtn, rollbackBtn, removeBtn, editBtn ], + fields: [ + 'name', 'description', 'snapstate', 'vmstate', 'running', + { name: 'snaptime', type: 'date', dateFormat: 'timestamp' } + ], + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'name', + width: 200, + renderer: function(value, metaData, record) { + if (value === 'current') { + return "NOW"; + } else { + return value; + } + } + }, + { + text: gettext('RAM'), + align: 'center', + resizable: false, + dataIndex: 'vmstate', + width: 50, + renderer: function(value, metaData, record) { + if (record.data.name !== 'current') { + return Proxmox.Utils.format_boolean(value); + } + } + }, + { + text: gettext('Date') + "/" + gettext("Status"), + dataIndex: 'snaptime', + width: 150, + renderer: function(value, metaData, record) { + if (record.data.snapstate) { + return record.data.snapstate; + } + if (value) { + return Ext.Date.format(value,'Y-m-d H:i:s'); + } + } + }, + { + text: gettext('Description'), + dataIndex: 'description', + flex: 1, + renderer: function(value, metaData, record) { + if (record.data.name === 'current') { + return gettext("You are here!"); + } else { + return Ext.String.htmlEncode(value); + } + } + } + ], + columnLines: true, // will work in 4.1? + listeners: { + activate: me.reload, + destroy: me.load_task.cancel, + itemdblclick: run_editor + } + }); + + me.callParent(); + + me.store.sorters.add(new Ext.util.Sorter({ + sorterFn: me.sorterFn + })); + } +}); + +Ext.define('PVE.qemu.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.qemu.Config', + + onlineHelp: 'chapter_virtual_machines', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var template = !!me.pveSelNode.data.template; + + var running = !!me.pveSelNode.data.uptime; + + var caps = Ext.state.Manager.get('GuiCap'); + + var base_url = '/nodes/' + nodename + "/qemu/" + vmid; + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json' + base_url + '/status/current', + interval: 1000 + }); + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: base_url + '/status/' + cmd, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + var resumeBtn = Ext.create('Ext.Button', { + text: gettext('Resume'), + disabled: !caps.vms['VM.PowerMgmt'], + hidden: true, + handler: function() { + vm_command('resume'); + }, + iconCls: 'fa fa-play' + }); + + var startBtn = Ext.create('Ext.Button', { + text: gettext('Start'), + disabled: !caps.vms['VM.PowerMgmt'] || running, + hidden: template, + handler: function() { + vm_command('start'); + }, + iconCls: 'fa fa-play' + }); + + var migrateBtn = Ext.create('Ext.Button', { + text: gettext('Migrate'), + disabled: !caps.vms['VM.Migrate'], + hidden: PVE.data.ResourceStore.getNodes().length < 2, + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'qemu', + nodename: nodename, + vmid: vmid + }); + win.show(); + }, + iconCls: 'fa fa-send-o' + }); + + var moreBtn = Ext.create('Proxmox.button.Button', { + text: gettext('More'), + menu: { items: [ + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: caps.vms['VM.Clone'] ? false : true, + handler: function() { + PVE.window.Clone.wrap(nodename, vmid, template, 'qemu'); + } + }, + { + text: gettext('Convert to template'), + disabled: template, + xtype: 'pveMenuItem', + iconCls: 'fa fa-fw fa-file-o', + hidden: caps.vms['VM.Allocate'] ? false : true, + confirmMsg: Proxmox.Utils.format_task_description('qmtemplate', vmid), + handler: function() { + Proxmox.Utils.API2Request({ + url: base_url + '/template', + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + } + }, + { + iconCls: 'fa fa-heartbeat ', + hidden: !caps.nodes['Sys.Console'], + text: gettext('Manage HA'), + handler: function() { + var ha = me.pveSelNode.data.hastate; + Ext.create('PVE.ha.VMResourceEdit', { + vmid: vmid, + isCreate: (!ha || ha === 'unmanaged') + }).show(); + } + }, + { + text: gettext('Remove'), + itemId: 'removeBtn', + disabled: !caps.vms['VM.Allocate'], + handler: function() { + Ext.create('PVE.window.SafeDestroy', { + url: base_url, + item: { type: 'VM', id: vmid } + }).show(); + }, + iconCls: 'fa fa-trash-o' + } + ]} + }); + + var shutdownBtn = Ext.create('PVE.button.Split', { + text: gettext('Shutdown'), + disabled: !caps.vms['VM.PowerMgmt'] || !running, + hidden: template, + confirmMsg: Proxmox.Utils.format_task_description('qmshutdown', vmid), + handler: function() { + vm_command('shutdown'); + }, + menu: { + items: [{ + text: gettext('Pause'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('qmpause', vmid), + handler: function() { + vm_command("suspend"); + }, + iconCls: 'fa fa-pause' + },{ + text: gettext('Hibernate'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('qmsuspend', vmid), + tooltip: gettext('Suspend to disk'), + handler: function() { + vm_command("suspend", { todisk: 1 }); + }, + iconCls: 'fa fa-download' + },{ + text: gettext('Stop'), + disabled: !caps.vms['VM.PowerMgmt'], + dangerous: true, + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'), + confirmMsg: Proxmox.Utils.format_task_description('qmstop', vmid), + handler: function() { + vm_command("stop", { timeout: 30 }); + }, + iconCls: 'fa fa-stop' + },{ + text: gettext('Reset'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('qmreset', vmid), + handler: function() { + vm_command("reset"); + }, + iconCls: 'fa fa-bolt' + }] + }, + iconCls: 'fa fa-power-off' + }); + + var vm = me.pveSelNode.data; + + var consoleBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.vms['VM.Console'], + hidden: template, + consoleType: 'kvm', + consoleName: vm.name, + nodename: nodename, + vmid: vmid + }); + + var statusTxt = Ext.create('Ext.toolbar.TextItem', { + data: { + lock: undefined + }, + tpl: [ + '', + ' ({lock})', + '' + ] + }); + + Ext.apply(me, { + title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm.text, nodename), + hstateid: 'kvmtab', + tbarSpacing: false, + tbar: [ statusTxt, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn ], + defaults: { statusStore: me.statusStore }, + items: [ + { + title: gettext('Summary'), + xtype: 'pveQemuSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + } + ] + }); + + if (caps.vms['VM.Console'] && !template) { + me.items.push({ + title: gettext('Console'), + itemId: 'console', + iconCls: 'fa fa-terminal', + xtype: 'pveNoVncConsole', + vmid: vmid, + consoleType: 'kvm', + nodename: nodename + }); + } + + me.items.push( + { + title: gettext('Hardware'), + itemId: 'hardware', + iconCls: 'fa fa-desktop', + xtype: 'PVE.qemu.HardwareView' + }, + { + title: 'Cloud-Init', + itemId: 'cloudinit', + iconCls: 'fa fa-cloud', + xtype: 'pveCiPanel' + }, + { + title: gettext('Options'), + iconCls: 'fa fa-gear', + itemId: 'options', + xtype: 'PVE.qemu.Options' + }, + { + title: gettext('Task History'), + itemId: 'tasks', + xtype: 'proxmoxNodeTasks', + iconCls: 'fa fa-list', + nodename: nodename, + vmidFilter: vmid + } + ); + + if (caps.vms['VM.Monitor'] && !template) { + me.items.push({ + title: gettext('Monitor'), + iconCls: 'fa fa-eye', + itemId: 'monitor', + xtype: 'pveQemuMonitor' + }); + } + + if (caps.vms['VM.Backup']) { + me.items.push({ + title: gettext('Backup'), + iconCls: 'fa fa-floppy-o', + xtype: 'pveBackupView', + itemId: 'backup' + }, + { + title: gettext('Replication'), + iconCls: 'fa fa-retweet', + xtype: 'pveReplicaView', + itemId: 'replication' + }); + } + + if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback']) && !template) { + me.items.push({ + title: gettext('Snapshots'), + iconCls: 'fa fa-history', + xtype: 'pveQemuSnapshotTree', + itemId: 'snapshot' + }); + } + + if (caps.vms['VM.Console']) { + me.items.push( + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + iconCls: 'fa fa-shield', + allow_iface: true, + base_url: base_url + '/firewall/rules', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + groups: ['firewall'], + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_vm_container_configuration', + title: gettext('Options'), + base_url: base_url + '/firewall/options', + fwtype: 'vm', + itemId: 'firewall-options' + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: base_url + '/firewall/aliases', + itemId: 'firewall-aliases' + }, + { + xtype: 'pveIPSet', + title: gettext('IPSet'), + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: base_url + '/firewall/ipset', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall-ipset' + }, + { + title: gettext('Log'), + groups: ['firewall'], + iconCls: 'fa fa-list', + onlineHelp: 'chapter_pve_firewall', + itemId: 'firewall-fwlog', + xtype: 'proxmoxLogView', + url: '/api2/extjs' + base_url + '/firewall/log' + } + ); + } + + if (caps.vms['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: '/vms/' + vmid + }); + } + + me.callParent(); + + me.mon(me.statusStore, 'load', function(s, records, success) { + var status; + var qmpstatus; + var spice = false; + var xtermjs = false; + var lock; + + if (!success) { + status = qmpstatus = 'unknown'; + } else { + var rec = s.data.get('status'); + status = rec ? rec.data.value : 'unknown'; + rec = s.data.get('qmpstatus'); + qmpstatus = rec ? rec.data.value : 'unknown'; + rec = s.data.get('template'); + template = rec.data.value || false; + rec = s.data.get('lock'); + lock = rec ? rec.data.value : undefined; + + spice = s.data.get('spice') ? true : false; + xtermjs = s.data.get('serial') ? true : false; + + } + + if (template) { + return; + } + + var resume = (['prelaunch', 'paused', 'suspended'].indexOf(qmpstatus) !== -1); + + if (resume || lock === 'suspended') { + startBtn.setVisible(false); + resumeBtn.setVisible(true); + } else { + startBtn.setVisible(true); + resumeBtn.setVisible(false); + } + + consoleBtn.setEnableSpice(spice); + consoleBtn.setEnableXtermJS(xtermjs); + + statusTxt.update({ lock: lock }); + + startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template); + shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running'); + me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped'); + consoleBtn.setDisabled(template); + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.qemu.CreateWizard', { + extend: 'PVE.window.Wizard', + alias: 'widget.pveQemuCreateWizard', + mixins: ['Proxmox.Mixin.CBind'], + + viewModel: { + data: { + nodename: '', + current: { + scsihw: '' + } + } + }, + + cbindData: { + nodename: undefined + }, + + subject: gettext('Virtual Machine'), + + items: [ + { + xtype: 'inputpanel', + title: gettext('General'), + onlineHelp: 'qm_general_settings', + column1: [ + { + xtype: 'pveNodeSelector', + name: 'nodename', + cbind: { + selectCurNode: '{!nodename}', + preferredValue: '{nodename}' + }, + bind: { + value: '{nodename}' + }, + fieldLabel: gettext('Node'), + allowBlank: false, + onlineValidator: true + }, + { + xtype: 'pveGuestIDSelector', + name: 'vmid', + guestType: 'qemu', + value: '', + loadNextFreeID: true, + validateExists: false + }, + { + xtype: 'textfield', + name: 'name', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Name'), + allowBlank: true + } + ], + column2: [ + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true + } + ], + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Start at boot') + } + ], + advancedColumn2: [ + { + xtype: 'textfield', + name: 'order', + defaultValue: '', + emptyText: 'any', + labelWidth: 120, + fieldLabel: gettext('Start/Shutdown order') + }, + { + xtype: 'textfield', + name: 'up', + defaultValue: '', + emptyText: 'default', + labelWidth: 120, + fieldLabel: gettext('Startup delay') + }, + { + xtype: 'textfield', + name: 'down', + defaultValue: '', + emptyText: 'default', + labelWidth: 120, + fieldLabel: gettext('Shutdown timeout') + } + ], + onGetValues: function(values) { + + ['name', 'pool', 'onboot', 'agent'].forEach(function(field) { + if (!values[field]) { + delete values[field]; + } + }); + + var res = PVE.Parser.printStartup({ + order: values.order, + up: values.up, + down: values.down + }); + + if (res) { + values.startup = res; + } + + delete values.order; + delete values.up; + delete values.down; + + return values; + } + }, + { + xtype: 'container', + layout: 'hbox', + defaults: { + flex: 1, + padding: '0 10' + }, + title: gettext('OS'), + items: [ + { + xtype: 'pveQemuCDInputPanel', + bind: { + nodename: '{nodename}' + }, + confid: 'ide2', + insideWizard: true + }, + { + xtype: 'pveQemuOSTypePanel', + insideWizard: true + } + ] + }, + { + xtype: 'pveQemuSystemPanel', + title: gettext('System'), + isCreate: true, + insideWizard: true + }, + { + xtype: 'pveQemuHDInputPanel', + bind: { + nodename: '{nodename}' + }, + title: gettext('Hard Disk'), + isCreate: true, + insideWizard: true + }, + { + xtype: 'pveQemuProcessorPanel', + insideWizard: true, + title: gettext('CPU') + }, + { + xtype: 'pveQemuMemoryPanel', + insideWizard: true, + title: gettext('Memory') + }, + { + xtype: 'pveQemuNetworkInputPanel', + bind: { + nodename: '{nodename}' + }, + title: gettext('Network'), + insideWizard: true + }, + { + title: gettext('Confirm'), + layout: 'fit', + items: [ + { + xtype: 'grid', + store: { + model: 'KeyValue', + sorters: [{ + property : 'key', + direction: 'ASC' + }] + }, + columns: [ + {header: 'Key', width: 150, dataIndex: 'key'}, + {header: 'Value', flex: 1, dataIndex: 'value'} + ] + } + ], + dockedItems: [ + { + xtype: 'proxmoxcheckbox', + name: 'start', + dock: 'bottom', + margin: '5 0 0 0', + boxLabel: gettext('Start after created') + } + ], + listeners: { + show: function(panel) { + var kv = this.up('window').getValues(); + var data = []; + Ext.Object.each(kv, function(key, value) { + if (key === 'delete') { // ignore + return; + } + data.push({ key: key, value: value }); + }); + + var summarystore = panel.down('grid').getStore(); + summarystore.suspendEvents(); + summarystore.removeAll(); + summarystore.add(data); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh'); + + } + }, + onSubmit: function() { + var wizard = this.up('window'); + var kv = wizard.getValues(); + delete kv['delete']; + + var nodename = kv.nodename; + delete kv.nodename; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/qemu', + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function(response){ + wizard.close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + } + ] +}); + + + + +Ext.define('PVE.qemu.USBInputPanel', { + extend: 'Proxmox.panel.InputPanel', + mixins: ['Proxmox.Mixin.CBind' ], + + autoComplete: false, + onlineHelp: 'qm_usb_passthrough', + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'field[name=usb]': { + change: function(field, newValue, oldValue) { + var hwidfield = this.lookupReference('hwid'); + var portfield = this.lookupReference('port'); + var usb3field = this.lookupReference('usb3'); + if (field.inputValue === 'hostdevice') { + hwidfield.setDisabled(!newValue); + } else if(field.inputValue === 'port') { + portfield.setDisabled(!newValue); + } else if(field.inputValue === 'spice') { + usb3field.setDisabled(newValue); + } + } + }, + 'pveUSBSelector': { + change: function(field, newValue, oldValue) { + var usbval = field.getUSBValue(); + var usb3field = this.lookupReference('usb3'); + var usb3 = /usb3/.test(usbval); + if(usb3 && !usb3field.isDisabled()) { + usb3field.savedVal = usb3field.getValue(); + usb3field.setValue(true); + usb3field.setDisabled(true); + } else if(!usb3 && usb3field.isDisabled()){ + var val = (usb3field.savedVal === undefined)?usb3field.originalValue:usb3field.savedVal; + usb3field.setValue(val); + usb3field.setDisabled(false); + } + } + } + } + }, + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + }, + + onGetValues: function(values) { + var me = this; + if(!me.confid) { + var i; + for (i = 0; i < 6; i++) { + if (!me.vmconfig['usb' + i.toString()]) { + me.confid = 'usb' + i.toString(); + break; + } + } + } + var val = ""; + var type = me.down('radiofield').getGroupValue(); + switch (type) { + case 'spice': + val = 'spice'; break; + case 'hostdevice': + case 'port': + val = me.down('pveUSBSelector[name=' + type + ']').getUSBValue(); + if (!/usb3/.test(val) && me.down('field[name=usb3]').getValue() === true) { + val += ',usb3=1'; + } + break; + default: + throw "invalid type selected"; + } + + values[me.confid] = val; + return values; + }, + + items: [ + { + xtype: 'fieldcontainer', + defaultType: 'radiofield', + items:[ + { + name: 'usb', + inputValue: 'spice', + boxLabel: gettext('Spice Port'), + submitValue: false, + checked: true + }, + { + name: 'usb', + inputValue: 'hostdevice', + boxLabel: gettext('Use USB Vendor/Device ID'), + submitValue: false + }, + { + xtype: 'pveUSBSelector', + disabled: true, + type: 'device', + name: 'hostdevice', + cbind: { pveSelNode: '{pveSelNode}' }, + editable: true, + reference: 'hwid', + allowBlank: false, + fieldLabel: 'Choose Device', + labelAlign: 'right', + submitValue: false + }, + { + name: 'usb', + inputValue: 'port', + boxLabel: gettext('Use USB Port'), + submitValue: false + }, + { + xtype: 'pveUSBSelector', + disabled: true, + name: 'port', + cbind: { pveSelNode: '{pveSelNode}' }, + editable: true, + type: 'port', + reference: 'port', + allowBlank: false, + fieldLabel: gettext('Choose Port'), + labelAlign: 'right', + submitValue: false + }, + { + xtype: 'checkbox', + name: 'usb3', + submitValue: false, + reference: 'usb3', + fieldLabel: gettext('Use USB3') + } + ] + } + ] +}); + +Ext.define('PVE.qemu.USBEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('USB Device'), + + + initComponent : function() { + var me = this; + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.USBInputPanel', { + confid: me.confid, + pveSelNode: me.pveSelNode + }); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var data = response.result.data[me.confid].split(','); + var port, hostdevice, usb3 = false; + var type = 'spice'; + var i; + for (i = 0; i < data.length; i++) { + if (/^(host=)?(0x)?[a-zA-Z0-9]{4}\:(0x)?[a-zA-Z0-9]{4}$/.test(data[i])) { + hostdevice = data[i]; + hostdevice = hostdevice.replace('host=', '').replace('0x',''); + type = 'hostdevice'; + } else if (/^(host=)?(\d+)\-(\d+(\.\d+)*)$/.test(data[i])) { + port = data[i]; + port = port.replace('host=',''); + type = 'port'; + } + + if (/^usb3=(1|on|true)$/.test(data[i])) { + usb3 = true; + } + } + var values = { + usb : type, + hostdevice: hostdevice, + port: port, + usb3: usb3 + }; + + ipanel.setValues(values); + } + } + }); + } +}); +Ext.define('PVE.qemu.PCIInputPanel', { + extend: 'Proxmox.panel.InputPanel', + + onlineHelp: 'qm_pci_passthrough', + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + + var hostpci = me.vmconfig[me.confid] || ''; + + var values = PVE.Parser.parsePropertyString(hostpci, 'host'); + if (values.host && values.host.length < 6) { // 00:00 format not 00:00.0 + values.host += ".0"; + values.multifunction = true; + } + values['x-vga'] = PVE.Parser.parseBoolean(values['x-vga'], 0); + values.pcie = PVE.Parser.parseBoolean(values.pcie, 0); + values.rombar = PVE.Parser.parseBoolean(values.rombar, 1); + + me.setValues(values); + if (!me.vmconfig.machine || me.vmconfig.machine.indexOf('q35') === -1) { + // machine is not set to some variant of q35, so we disable pcie + var pcie = me.down('field[name=pcie]'); + pcie.setDisabled(true); + pcie.setBoxLabel(gettext('Q35 only')); + } + + if (values.romfile) { + me.down('field[name=romfile]').setVisible(true); + } + }, + + onGetValues: function(values) { + var me = this; + var ret = {}; + if(!me.confid) { + var i; + for (i = 0; i < 5; i++) { + if (!me.vmconfig['hostpci' + i.toString()]) { + me.confid = 'hostpci' + i.toString(); + break; + } + } + } + if (values.multifunction) { + // modify host to skip the '.X' + values.host = values.host.substring(0,5); + delete values.multifunction; + } + + if (values.rombar) { + delete values.rombar; + } else { + values.rombar = 0; + } + + if (!values.romfile) { + delete values.romfile; + } + + ret[me.confid] = PVE.Parser.printPropertyString(values, 'host'); + return ret; + }, + + initComponent: function() { + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + me.column1 = [ + { + xtype: 'pvePCISelector', + fieldLabel: gettext('Device'), + name: 'host', + nodename: me.nodename, + allowBlank: false, + onLoadCallBack: function(store, records, success) { + if (!success || !records.length) { + return; + } + + var first = records[0]; + if (first.data.iommugroup === -1) { + // no iommu groups + var warning = Ext.create('Ext.form.field.Display', { + columnWidth: 1, + padding: '0 0 10 0', + value: 'No IOMMU detected, please activate it.' + + 'See Documentation for further information.', + userCls: 'pve-hint' + }); + me.items.insert(0, warning); + me.updateLayout(); // insert does not trigger that + } + }, + listeners: { + change: function(pcisel, value) { + if (!value) { + return; + } + var pcidev = pcisel.getStore().getById(value); + var mdevfield = me.down('field[name=mdev]'); + mdevfield.setDisabled(!pcidev || !pcidev.data.mdev); + if (!pcidev) { + return; + } + var id = pcidev.data.id.substring(0,5); // 00:00 + var iommu = pcidev.data.iommugroup; + // try to find out if there are more devices + // in that iommu group + if (iommu !== -1) { + var count = 0; + pcisel.getStore().each(function(record) { + if (record.data.iommugroup === iommu && + record.data.id.substring(0,5) !== id) + { + count++; + return false; + } + }); + var warning = me.down('#iommuwarning'); + if (count && !warning) { + warning = Ext.create('Ext.form.field.Display', { + columnWidth: 1, + padding: '0 0 10 0', + itemId: 'iommuwarning', + value: 'The selected Device is not in a seperate' + + 'IOMMU group, make sure this is intended.', + userCls: 'pve-hint' + }); + me.items.insert(0, warning); + me.updateLayout(); // insert does not trigger that + } else if (!count && warning) { + me.remove(warning); + } + } + if (pcidev.data.mdev) { + mdevfield.setPciID(value); + } + } + } + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('All Functions'), + name: 'multifunction' + } + ]; + + me.column2 = [ + { + xtype: 'pveMDevSelector', + name: 'mdev', + disabled: true, + fieldLabel: gettext('MDev Type'), + nodename: me.nodename, + listeners: { + change: function(field, value) { + var mf = me.down('field[name=multifunction]'); + if (!!value) { + mf.setValue(false); + } + mf.setDisabled(!!value); + } + } + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Primary GPU'), + name: 'x-vga' + } + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'ROM-Bar', + name: 'rombar' + }, + { + xtype: 'displayfield', + submitValue: true, + hidden: true, + fieldLabel: 'ROM-File', + name: 'romfile' + } + ]; + + me.advancedColumn2 = [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'PCI-Express', + name: 'pcie' + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.PCIEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('PCI Device'), + + + initComponent : function() { + var me = this; + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.PCIInputPanel', { + confid: me.confid, + pveSelNode: me.pveSelNode + }); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response) { + ipanel.setVMConfig(response.result.data); + } + }); + } +}); +/*jslint confusion: true */ +Ext.define('PVE.qemu.SerialnputPanel', { + extend: 'Proxmox.panel.InputPanel', + + autoComplete: false, + + setVMConfig: function(vmconfig) { + var me = this, i; + me.vmconfig = vmconfig; + + for (i = 0; i < 4; i++) { + var port = 'serial' + i.toString(); + if (!me.vmconfig[port]) { + me.down('field[name=serialid]').setValue(i); + break; + } + } + + }, + + onGetValues: function(values) { + var me = this; + + var id = 'serial' + values.serialid; + delete values.serialid; + values[id] = 'socket'; + return values; + }, + + items: [ + { + xtype: 'proxmoxintegerfield', + name: 'serialid', + fieldLabel: gettext('Serial Port'), + minValue: 0, + maxValue: 3, + allowBlank: false, + validator: function(id) { + if (!this.rendered) { + return true; + } + var me = this.up('panel'); + if (me.vmconfig !== undefined && Ext.isDefined(me.vmconfig['serial' + id])) { + return "This device is already in use."; + } + return true; + } + } + ] +}); + +Ext.define('PVE.qemu.SerialEdit', { + extend: 'Proxmox.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('Serial Port'), + + initComponent : function() { + var me = this; + + // for now create of (socket) serial port only + me.isCreate = true; + + var ipanel = Ext.create('PVE.qemu.SerialnputPanel', {}); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + } + }); + } +}); +Ext.define('PVE.window.IPInfo', { + extend: 'Ext.window.Window', + width: 600, + title: gettext('Guest Agent Network Information'), + height: 300, + layout: { + type: 'fit' + }, + modal: true, + items: [ + { + xtype: 'grid', + emptyText: gettext('No network information'), + columns: [ + { + dataIndex: 'name', + text: gettext('Name'), + flex: 3 + }, + { + dataIndex: 'hardware-address', + text: gettext('MAC address'), + width: 140 + }, + { + dataIndex: 'ip-addresses', + text: gettext('IP address'), + align: 'right', + flex: 4, + renderer: function(val) { + if (!Ext.isArray(val)) { + return ''; + } + var ips = []; + val.forEach(function(ip) { + var addr = ip['ip-address']; + var pref = ip.prefix; + if (addr && pref) { + ips.push(addr + '/' + pref); + } + }); + return ips.join('
'); + } + } + ] + } + ] +}); + +Ext.define('PVE.qemu.AgentIPView', { + extend: 'Ext.container.Container', + xtype: 'pveAgentIPView', + + layout: { + type: 'hbox', + align: 'top' + }, + + nics: [], + + items: [ + { + xtype: 'box', + html: ' IPs' + }, + { + xtype: 'container', + flex: 1, + layout: { + type: 'vbox', + align: 'right', + pack: 'end' + }, + items: [ + { + xtype: 'label', + flex: 1, + itemId: 'ipBox', + style: { + 'text-align': 'right' + } + }, + { + xtype: 'button', + itemId: 'moreBtn', + hidden: true, + ui: 'default-toolbar', + handler: function(btn) { + var me = this.up('pveAgentIPView'); + + var win = Ext.create('PVE.window.IPInfo'); + win.down('grid').getStore().setData(me.nics); + win.show(); + }, + text: gettext('More') + } + ] + } + ], + + getDefaultIps: function(nics) { + var me = this; + var ips = []; + nics.forEach(function(nic) { + if (nic['hardware-address'] && + nic['hardware-address'] != '00:00:00:00:00:00') { + + var nic_ips = nic['ip-addresses'] || []; + nic_ips.forEach(function(ip) { + var p = ip['ip-address']; + // show 2 ips at maximum + if (ips.length < 2) { + ips.push(p); + } + }); + } + }); + + return ips; + }, + + startIPStore: function(store, records, success) { + var me = this; + var agentRec = store.getById('agent'); + /*jslint confusion: true*/ + /* value is number and string */ + me.agent = (agentRec && agentRec.data.value === 1); + me.running = (store.getById('status').data.value === 'running'); + /*jslint confusion: false*/ + + var caps = Ext.state.Manager.get('GuiCap'); + + if (!caps.vms['VM.Monitor']) { + var errorText = gettext("Requires '{0}' Privileges"); + me.updateStatus(false, Ext.String.format(errorText, 'VM.Monitor')); + return; + } + + if (me.agent && me.running && me.ipStore.isStopped) { + me.ipStore.startUpdate(); + } else if (me.ipStore.isStopped) { + me.updateStatus(); + } + }, + + updateStatus: function(unsuccessful, defaulttext) { + var me = this; + var text = defaulttext || gettext('No network information'); + var more = false; + if (unsuccessful) { + text = gettext('Guest Agent not running'); + } else if (me.agent && me.running) { + if (Ext.isArray(me.nics) && me.nics.length) { + more = true; + var ips = me.getDefaultIps(me.nics); + if (ips.length !== 0) { + text = ips.join('
'); + } + } else if (me.nics && me.nics.error) { + var msg = gettext('Cannot get info from Guest Agent
Error: {0}'); + text = Ext.String.format(text, me.nics.error.desc); + } + } else if (me.agent) { + text = gettext('Guest Agent not running'); + } else { + text = gettext('No Guest Agent configured'); + } + + var ipBox = me.down('#ipBox'); + ipBox.update(text); + + var moreBtn = me.down('#moreBtn'); + moreBtn.setVisible(more); + }, + + initComponent: function() { + var me = this; + + if (!me.rstore) { + throw 'rstore not given'; + } + + if (!me.pveSelNode) { + throw 'pveSelNode not given'; + } + + var nodename = me.pveSelNode.data.node; + var vmid = me.pveSelNode.data.vmid; + + me.ipStore = Ext.create('Proxmox.data.UpdateStore', { + interval: 10000, + storeid: 'pve-qemu-agent-' + vmid, + method: 'POST', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + nodename + '/qemu/' + vmid + '/agent/network-get-interfaces' + } + }); + + me.callParent(); + + me.mon(me.ipStore, 'load', function(store, records, success) { + if (records && records.length) { + me.nics = records[0].data.result; + } else { + me.nics = undefined; + } + me.updateStatus(!success); + }); + + me.on('destroy', me.ipStore.stopUpdate); + + // if we already have info about the vm, use it immediately + if (me.rstore.getCount()) { + me.startIPStore(me.rstore, me.rstore.getData(), false); + } + + // check if the guest agent is there on every statusstore load + me.mon(me.rstore, 'load', me.startIPStore, me); + } +}); +Ext.define('PVE.qemu.CloudInit', { + extend: 'Proxmox.grid.PendingObjectGrid', + xtype: 'pveCiPanel', + + onlineHelp: 'qm_cloud_init', + + tbar: [ + { + xtype: 'proxmoxButton', + disabled: true, + dangerous: true, + confirmMsg: function(rec) { + var me = this.up('grid'); + var warn = gettext('Are you sure you want to remove entry {0}'); + + var entry = rec.data.key; + var msg = Ext.String.format(warn, "'" + + me.renderKey(entry, {}, rec) + "'"); + + return msg; + }, + enableFn: function(record) { + var me = this.up('grid'); + var caps = Ext.state.Manager.get('GuiCap'); + if (me.rows[record.data.key].never_delete || + !caps.vms['VM.Config.Network']) { + return false; + } + + if (record.data.key === 'cipassword' && !record.data.value) { + return false; + } + return true; + }, + handler: function() { + var me = this.up('grid'); + var records = me.getSelection(); + if (!records || !records.length) { + return; + } + + var id = records[0].data.key; + var match = id.match(/^net(\d+)$/); + if (match) { + id = 'ipconfig' + match[1]; + } + + var params = {}; + params['delete'] = id; + Proxmox.Utils.API2Request({ + url: me.baseurl + '/config', + waitMsgTarget: me, + method: 'PUT', + params: params, + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }, + callback: function() { + me.reload(); + } + }); + }, + text: gettext('Remove') + }, + { + xtype: 'proxmoxButton', + disabled: true, + handler: function() { + var me = this.up('grid'); + me.run_editor(); + }, + text: gettext('Edit') + }, + '-', + { + xtype: 'button', + itemId: 'savebtn', + text: gettext('Regenerate Image'), + handler: function() { + var me = this.up('grid'); + var eject_params = {}; + var insert_params = {}; + var disk = PVE.Parser.parseQemuDrive(me.ciDriveId, me.ciDrive); + var storage = ''; + var stormatch = disk.file.match(/^([^\:]+)\:/); + if (stormatch) { + storage = stormatch[1]; + } + eject_params[me.ciDriveId] = 'none,media=cdrom'; + insert_params[me.ciDriveId] = storage + ':cloudinit'; + + var failure = function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + }; + + Proxmox.Utils.API2Request({ + url: me.baseurl + '/config', + waitMsgTarget: me, + method: 'PUT', + params: eject_params, + failure: failure, + callback: function() { + Proxmox.Utils.API2Request({ + url: me.baseurl + '/config', + waitMsgTarget: me, + method: 'PUT', + params: insert_params, + failure: failure, + callback: function() { + me.reload(); + } + }); + } + }); + } + } + ], + + border: false, + + set_button_status: function(rstore, records, success) { + if (!success || records.length < 1) { + return; + } + var me = this; + var found; + records.forEach(function(record) { + if (found) { + return; + } + var id = record.data.key; + var value = record.data.value; + var ciregex = new RegExp("vm-" + me.pveSelNode.data.vmid + "-cloudinit"); + if (id.match(/^(ide|scsi|sata)\d+$/) && ciregex.test(value)) { + found = id; + me.ciDriveId = found; + me.ciDrive = value; + } + }); + + me.down('#savebtn').setDisabled(!found); + me.setDisabled(!found); + if (!found) { + me.getView().mask(gettext('No CloudInit Drive found'), ['pve-static-mask']); + } else { + me.getView().unmask(); + } + }, + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = rows[key] || {}; + + var icon = ""; + if (rowdef.iconCls) { + icon = ' '; + } + return icon + (rowdef.header || key); + }, + + listeners: { + activate: function () { + var me = this; + me.rstore.startUpdate(); + }, + itemdblclick: function() { + var me = this; + me.run_editor(); + } + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + var caps = Ext.state.Manager.get('GuiCap'); + me.baseurl = '/api2/extjs/nodes/' + nodename + '/qemu/' + vmid; + me.url = me.baseurl + '/pending'; + me.editorConfig.url = me.baseurl + '/config'; + me.editorConfig.pveSelNode = me.pveSelNode; + + /*jslint confusion: true*/ + /* editor is string and object */ + me.rows = { + ciuser: { + header: gettext('User'), + iconCls: 'fa fa-user', + never_delete: true, + defaultValue: '', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('User'), + items: [ + { + xtype: 'proxmoxtextfield', + deleteEmpty: true, + emptyText: Proxmox.Utils.defaultText, + fieldLabel: gettext('User'), + name: 'ciuser' + } + ] + } : undefined, + renderer: function(value) { + return value || Proxmox.Utils.defaultText; + } + }, + cipassword: { + header: gettext('Password'), + iconCls: 'fa fa-unlock', + defaultValue: '', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Password'), + items: [ + { + xtype: 'proxmoxtextfield', + inputType: 'password', + deleteEmpty: true, + emptyText: Proxmox.Utils.noneText, + fieldLabel: gettext('Password'), + name: 'cipassword' + } + ] + } : undefined, + renderer: function(value) { + return value || Proxmox.Utils.noneText; + } + }, + searchdomain: { + header: gettext('DNS domain'), + iconCls: 'fa fa-globe', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + never_delete: true, + defaultValue: gettext('use host settings') + }, + nameserver: { + header: gettext('DNS servers'), + iconCls: 'fa fa-globe', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + never_delete: true, + defaultValue: gettext('use host settings') + }, + sshkeys: { + header: gettext('SSH public key'), + iconCls: 'fa fa-key', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.SSHKeyEdit' : undefined, + never_delete: true, + renderer: function(value) { + value = decodeURIComponent(value); + var keys = value.split('\n'); + var text = []; + keys.forEach(function(key) { + if (key.length) { + // First erase all quoted strings (eg. command="foo" + var v = key.replace(/"(?:\\.|[^"\\])*"/g, ''); + // Now try to detect the comment: + var res = v.match(/^\s*(\S+\s+)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)\s+\S+\s+(.*?)\s*$/, ''); + if (res) { + key = Ext.String.htmlEncode(res[2]); + if (res[1]) { + key += ' (' + gettext('with options') + ')'; + } + text.push(key); + return; + } + // Most likely invalid at this point, so just stick to + // the old value. + text.push(Ext.String.htmlEncode(key)); + } + }); + if (text.length) { + return text.join('
'); + } else { + return Proxmox.Utils.noneText; + } + }, + defaultValue: '' + } + }; + var i; + var ipconfig_renderer = function(value, md, record, ri, ci, store, pending) { + var id = record.data.key; + var match = id.match(/^net(\d+)$/); + var val = ''; + if (match) { + val = me.getObjectValue('ipconfig'+match[1], '', pending); + } + return val; + }; + for (i = 0; i < 32; i++) { + // we want to show an entry for every network device + // even if it is empty + me.rows['net' + i.toString()] = { + multiKey: ['ipconfig' + i.toString(), 'net' + i.toString()], + header: gettext('IP Config') + ' (net' + i.toString() +')', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.IPConfigEdit' : undefined, + iconCls: 'fa fa-exchange', + renderer: ipconfig_renderer + }; + me.rows['ipconfig' + i.toString()] = { + visible: false + }; + } + /*jslint confusion: false*/ + + PVE.Utils.forEachBus(['ide', 'scsi', 'sata'], function(type, id) { + me.rows[type+id] = { + visible: false + }; + }); + me.callParent(); + me.mon(me.rstore, 'load', me.set_button_status, me); + } +}); +Ext.define('PVE.qemu.CIDriveInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveCIDriveInputPanel', + + insideWizard: false, + + vmconfig: {}, // used to select usused disks + + onGetValues: function(values) { + var me = this; + + var drive = {}; + var params = {}; + drive.file = values.hdstorage + ":cloudinit"; + drive.format = values.diskformat; + params[values.controller + values.deviceid] = PVE.Parser.printQemuDrive(drive); + return params; + }, + + setNodename: function(nodename) { + var me = this; + me.down('#hdstorage').setNodename(nodename); + me.down('#hdimage').setStorage(undefined, nodename); + }, + + setVMConfig: function(config) { + var me = this; + me.down('#drive').setVMConfig(config, 'cdrom'); + }, + + initComponent : function() { + var me = this; + + me.drive = {}; + + me.items = [ + { + xtype: 'pveControllerSelector', + noVirtIO: true, + itemId: 'drive', + fieldLabel: gettext('CloudInit Drive'), + name: 'drive' + }, + { + xtype: 'pveDiskStorageSelector', + itemId: 'storselector', + storageContent: 'images', + nodename: me.nodename, + hideSize: true + } + ]; + me.callParent(); + } +}); + +Ext.define('PVE.qemu.CIDriveEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveCIDriveEdit', + + isCreate: true, + subject: gettext('CloudInit Drive'), + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.items = [{ + xtype: 'pveCIDriveInputPanel', + itemId: 'cipanel', + nodename: nodename + }]; + + me.callParent(); + + me.load({ + success: function(response, opts) { + me.down('#cipanel').setVMConfig(response.result.data); + } + }); + } +}); +Ext.define('PVE.qemu.SSHKeyInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveQemuSSHKeyInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + if (values.sshkeys) { + values.sshkeys.trim(); + } + if (!values.sshkeys.length) { + values = {}; + values['delete'] = 'sshkeys'; + return values; + } else { + values.sshkeys = encodeURIComponent(values.sshkeys); + } + return values; + }, + + items: [ + { + xtype: 'textarea', + itemId: 'sshkeys', + name: 'sshkeys', + height: 250 + }, + { + xtype: 'filebutton', + itemId: 'filebutton', + name: 'file', + text: gettext('Load SSH Key File'), + fieldLabel: 'test', + listeners: { + change: function(btn, e, value) { + var me = this.up('inputpanel'); + e = e.event; + Ext.Array.each(e.target.files, function(file) { + PVE.Utils.loadSSHKeyFromFile(file, function(res) { + var keysField = me.down('#sshkeys'); + var old = keysField.getValue(); + keysField.setValue(old + res); + }); + }); + btn.reset(); + } + } + } + ], + + initComponent: function() { + var me = this; + + me.callParent(); + if (!window.FileReader) { + me.down('#filebutton').setVisible(false); + } + + } +}); + +Ext.define('PVE.qemu.SSHKeyEdit', { + extend: 'Proxmox.window.Edit', + + width: 800, + + initComponent : function() { + var me = this; + + var ipanel = Ext.create('PVE.qemu.SSHKeyInputPanel'); + + Ext.apply(me, { + subject: gettext('SSH Keys'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.create) { + me.load({ + success: function(response, options) { + var data = response.result.data; + if (data.sshkeys) { + data.sshkeys = decodeURIComponent(data.sshkeys); + ipanel.setValues(data); + } + } + }); + } + } +}); +Ext.define('PVE.qemu.IPConfigPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveIPConfigPanel', + + insideWizard: false, + + vmconfig: {}, + + onGetValues: function(values) { + var me = this; + + if (values.ipv4mode !== 'static') { + values.ip = values.ipv4mode; + } + + if (values.ipv6mode !== 'static') { + values.ip6 = values.ipv6mode; + } + + var params = {}; + + var cfg = PVE.Parser.printIPConfig(values); + if (cfg === '') { + params['delete'] = [me.confid]; + } else { + params[me.confid] = cfg; + } + return params; + }, + + setVMConfig: function(config) { + var me = this; + me.vmconfig = config; + }, + + setIPConfig: function(confid, data) { + var me = this; + + me.confid = confid; + + if (data.ip === 'dhcp') { + data.ipv4mode = data.ip; + data.ip = ''; + } else { + data.ipv4mode = 'static'; + } + if (data.ip6 === 'dhcp' || data.ip6 === 'auto') { + data.ipv6mode = data.ip6; + data.ip6 = ''; + } else { + data.ipv6mode = 'static'; + } + + me.ipconfig = data; + me.setValues(me.ipconfig); + }, + + initComponent : function() { + var me = this; + + me.ipconfig = {}; + + me.column1 = [ + { + xtype: 'displayfield', + fieldLabel: gettext('Network Device'), + value: me.netid + }, + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: gettext('IPv4') + ':' + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv4mode', + inputValue: 'static', + checked: false, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip]').setDisabled(!value); + me.down('field[name=gw]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: gettext('DHCP'), + name: 'ipv4mode', + inputValue: 'dhcp', + checked: false, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip', + vtype: 'IPCIDRAddress', + value: '', + disabled: true, + fieldLabel: gettext('IPv4/CIDR') + }, + { + xtype: 'textfield', + name: 'gw', + value: '', + vtype: 'IPAddress', + disabled: true, + fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') +')' + } + ]; + + me.column2 = [ + { + xtype: 'displayfield' + }, + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: gettext('IPv6') + ':' + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv6mode', + inputValue: 'static', + checked: false, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip6]').setDisabled(!value); + me.down('field[name=gw6]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: gettext('DHCP'), + name: 'ipv6mode', + inputValue: 'dhcp', + checked: false, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip6', + value: '', + vtype: 'IP6CIDRAddress', + disabled: true, + fieldLabel: gettext('IPv6/CIDR') + }, + { + xtype: 'textfield', + name: 'gw6', + vtype: 'IP6Address', + value: '', + disabled: true, + fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')' + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.IPConfigEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + // convert confid from netX to ipconfigX + var match = me.confid.match(/^net(\d+)$/); + if (match) { + me.netid = me.confid; + me.confid = 'ipconfig' + match[1]; + } + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.isCreate = me.confid ? false : true; + + var ipanel = Ext.create('PVE.qemu.IPConfigPanel', { + confid: me.confid, + netid: me.netid, + nodename: nodename + }); + + Ext.applyIf(me, { + subject: gettext('Network Config'), + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + me.vmconfig = response.result.data; + var ipconfig = {}; + var value = me.vmconfig[me.confid]; + if (value) { + ipconfig = PVE.Parser.parseIPConfig(me.confid, value); + if (!ipconfig) { + Ext.Msg.alert(gettext('Error'), gettext('Unable to parse network configuration')); + me.close(); + return; + } + } + ipanel.setIPConfig(me.confid, ipconfig); + ipanel.setVMConfig(me.vmconfig); + } + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.qemu.SystemInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveQemuSystemPanel', + + onlineHelp: 'qm_system_settings', + + viewModel: { + data: { + efi: false, + addefi: true + }, + + formulas: { + efidisk: function(get) { + return get('efi') && get('addefi'); + } + } + }, + + onGetValues: function(values) { + if (values.vga && values.vga.substr(0,6) === 'serial') { + values['serial' + values.vga.substr(6,1)] = 'socket'; + } + + var efidrive = {}; + if (values.hdimage) { + efidrive.file = values.hdimage; + } else if (values.hdstorage) { + efidrive.file = values.hdstorage + ":1"; + } + + if (values.diskformat) { + efidrive.format = values.diskformat; + } + + delete values.hdimage; + delete values.hdstorage; + delete values.diskformat; + + if (efidrive.file) { + values.efidisk0 = PVE.Parser.printQemuDrive(efidrive); + } + + return values; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + scsihwChange: function(field, value) { + var me = this; + if (me.getView().insideWizard) { + me.getViewModel().set('current.scsihw', value); + } + }, + + biosChange: function(field, value) { + var me = this; + if (me.getView().insideWizard) { + me.getViewModel().set('efi', value === 'ovmf'); + } + }, + + control: { + 'pveScsiHwSelector': { + change: 'scsihwChange' + }, + 'pveQemuBiosSelector': { + change: 'biosChange' + } + } + }, + + column1: [ + { + xtype: 'proxmoxKVComboBox', + value: '__default__', + deleteEmpty: false, + fieldLabel: gettext('Graphic card'), + name: 'vga', + comboItems: PVE.Utils.kvm_vga_driver_array() + }, + { + xtype: 'proxmoxcheckbox', + name: 'agent', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Qemu Agent') + } + ], + + column2: [ + { + xtype: 'pveScsiHwSelector', + name: 'scsihw', + value: '__default__', + bind: { + value: '{current.scsihw}' + }, + fieldLabel: gettext('SCSI Controller') + } + ], + + advancedColumn1: [ + { + xtype: 'pveQemuBiosSelector', + name: 'bios', + value: '__default__', + fieldLabel: 'BIOS' + }, + { + xtype: 'proxmoxcheckbox', + bind: { + value: '{addefi}', + hidden: '{!efi}', + disabled: '{!efi}' + }, + hidden: true, + submitValue: false, + disabled: true, + fieldLabel: gettext('Add EFI Disk') + }, + { + xtype: 'pveDiskStorageSelector', + name: 'efidisk0', + storageContent: 'images', + bind: { + nodename: '{nodename}', + hidden: '{!efi}', + disabled: '{!efidisk}' + }, + autoSelect: false, + disabled: true, + hidden: true, + hideSize: true + } + ], + + advancedColumn2: [ + { + xtype: 'proxmoxKVComboBox', + name: 'machine', + value: '__default__', + fieldLabel: gettext('Machine'), + comboItems: [ + ['__default__', PVE.Utils.render_qemu_machine('')], + ['q35', 'q35'] + ] + } + ] + +}); +Ext.define('PVE.lxc.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveLxcSummary', + + scrollable: true, + bodyPadding: 5, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + if (!me.workspace) { + throw "no workspace specified"; + } + + if (!me.statusStore) { + throw "no status storage specified"; + } + + var template = !!me.pveSelNode.data.template; + var rstore = me.statusStore; + + var width = template ? 1 : 0.5; + var items = [ + { + xtype: template ? 'pveTemplateStatusView' : 'pveGuestStatusView', + responsiveConfig: { + 'width < 1900': { + columnWidth: width + }, + 'width >= 1900': { + columnWidth: width / 2 + } + }, + itemId: 'gueststatus', + pveSelNode: me.pveSelNode, + rstore: rstore + }, + { + xtype: 'pveNotesView', + maxHeight: 320, + itemId: 'notesview', + pveSelNode: me.pveSelNode, + responsiveConfig: { + 'width < 1900': { + columnWidth: width + }, + 'width >= 1900': { + columnWidth: width / 2 + } + } + } + ]; + + var rrdstore; + if (!template) { + + rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/rrddata", + model: 'pve-rrd-guest' + }); + + items.push( + { + xtype: 'proxmoxRRDChart', + title: gettext('CPU usage'), + pveSelNode: me.pveSelNode, + fields: ['cpu'], + fieldTitles: [gettext('CPU usage')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Memory usage'), + pveSelNode: me.pveSelNode, + fields: ['maxmem', 'mem'], + fieldTitles: [gettext('Total'), gettext('RAM usage')], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Network traffic'), + pveSelNode: me.pveSelNode, + fields: ['netin','netout'], + store: rrdstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Disk IO'), + pveSelNode: me.pveSelNode, + fields: ['diskread','diskwrite'], + store: rrdstore + } + ); + + } + + Ext.apply(me, { + tbar: [ '->', { xtype: 'proxmoxRRDTypeSelector' } ], + items: [ + { + xtype: 'container', + layout: { + type: 'column' + }, + defaults: { + minHeight: 320, + padding: 5, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1 + }, + 'width >= 1900': { + columnWidth: 0.5 + } + } + }, + items: items + } + ] + }); + + me.callParent(); + if (!template) { + rrdstore.startUpdate(); + me.on('destroy', rrdstore.stopUpdate); + } + } +}); +Ext.define('PVE.lxc.NetworkInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcNetworkInputPanel', + + insideWizard: false, + + onlineHelp: 'pct_container_network', + + setNodename: function(nodename) { + var me = this; + + if (!nodename || (me.nodename === nodename)) { + return; + } + + me.nodename = nodename; + + var bridgesel = me.query("[isFormField][name=bridge]")[0]; + bridgesel.setNodename(nodename); + }, + + onGetValues: function(values) { + var me = this; + + var id; + if (me.isCreate) { + id = values.id; + delete values.id; + } else { + id = me.ifname; + } + + if (!id) { + return {}; + } + + var newdata = {}; + + if (values.ipv6mode !== 'static') { + values.ip6 = values.ipv6mode; + } + if (values.ipv4mode !== 'static') { + values.ip = values.ipv4mode; + } + newdata[id] = PVE.Parser.printLxcNetwork(values); + return newdata; + }, + + initComponent : function() { + var me = this; + + var cdata = {}; + + if (me.insideWizard) { + me.ifname = 'net0'; + cdata.name = 'eth0'; + me.dataCache = {}; + } + cdata.firewall = (me.insideWizard || me.isCreate); + + if (!me.dataCache) { + throw "no dataCache specified"; + } + + if (!me.isCreate) { + if (!me.ifname) { + throw "no interface name specified"; + } + if (!me.dataCache[me.ifname]) { + throw "no such interface '" + me.ifname + "'"; + } + + cdata = PVE.Parser.parseLxcNetwork(me.dataCache[me.ifname]); + } + + var i; + for (i = 0; i < 10; i++) { + if (me.isCreate && !me.dataCache['net'+i.toString()]) { + me.ifname = 'net' + i.toString(); + break; + } + } + + var idselector = { + xtype: 'hidden', + name: 'id', + value: me.ifname + }; + + me.column1 = [ + idselector, + { + xtype: 'textfield', + name: 'name', + fieldLabel: gettext('Name'), + emptyText: '(e.g., eth0)', + allowBlank: false, + value: cdata.name, + validator: function(value) { + var result = ''; + Ext.Object.each(me.dataCache, function(key, netstr) { + if (!key.match(/^net\d+/) || key === me.ifname) { + return; // continue + } + var net = PVE.Parser.parseLxcNetwork(netstr); + if (net.name === value) { + result = "interface name already in use"; + return false; + } + }); + if (result !== '') { + return result; + } + // validator can return bool/string + /*jslint confusion:true*/ + return true; + } + }, + { + xtype: 'textfield', + name: 'hwaddr', + fieldLabel: gettext('MAC address'), + vtype: 'MacAddress', + value: cdata.hwaddr, + allowBlank: true, + emptyText: 'auto' + }, + { + xtype: 'PVE.form.BridgeSelector', + name: 'bridge', + nodename: me.nodename, + fieldLabel: gettext('Bridge'), + value: cdata.bridge, + allowBlank: false + }, + { + xtype: 'pveVlanField', + name: 'tag', + value: cdata.tag + }, + { + xtype: 'numberfield', + name: 'rate', + fieldLabel: gettext('Rate limit') + ' (MB/s)', + minValue: 0, + maxValue: 10*1024, + value: cdata.rate, + emptyText: 'unlimited', + allowBlank: true + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Firewall'), + name: 'firewall', + value: cdata.firewall + } + ]; + + var dhcp4 = (cdata.ip === 'dhcp'); + if (dhcp4) { + cdata.ip = ''; + cdata.gw = ''; + } + + var auto6 = (cdata.ip6 === 'auto'); + var dhcp6 = (cdata.ip6 === 'dhcp'); + if (auto6 || dhcp6) { + cdata.ip6 = ''; + cdata.gw6 = ''; + } + + me.column2 = [ + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: 'IPv4:' // do not localize + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv4mode', + inputValue: 'static', + checked: !dhcp4, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip]').setDisabled(!value); + me.down('field[name=gw]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: 'DHCP', // do not localize + name: 'ipv4mode', + inputValue: 'dhcp', + checked: dhcp4, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip', + vtype: 'IPCIDRAddress', + value: cdata.ip, + disabled: dhcp4, + fieldLabel: 'IPv4/CIDR' // do not localize + }, + { + xtype: 'textfield', + name: 'gw', + value: cdata.gw, + vtype: 'IPAddress', + disabled: dhcp4, + fieldLabel: gettext('Gateway') + ' (IPv4)', + margin: '0 0 3 0' // override bottom margin to account for the menuseparator + }, + { + xtype: 'menuseparator', + height: '3', + margin: '0' + }, + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + items: [ + { + xtype: 'label', + text: 'IPv6:' // do not localize + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv6mode', + inputValue: 'static', + checked: !(auto6 || dhcp6), + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip6]').setDisabled(!value); + me.down('field[name=gw6]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: 'DHCP', // do not localize + name: 'ipv6mode', + inputValue: 'dhcp', + checked: dhcp6, + margin: '0 0 0 10' + }, + { + xtype: 'radiofield', + boxLabel: 'SLAAC', // do not localize + name: 'ipv6mode', + inputValue: 'auto', + checked: auto6, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip6', + value: cdata.ip6, + vtype: 'IP6CIDRAddress', + disabled: (dhcp6 || auto6), + fieldLabel: 'IPv6/CIDR' // do not localize + }, + { + xtype: 'textfield', + name: 'gw6', + vtype: 'IP6Address', + value: cdata.gw6, + disabled: (dhcp6 || auto6), + fieldLabel: gettext('Gateway') + ' (IPv6)' + } + ]; + + me.callParent(); + } +}); + + +Ext.define('PVE.lxc.NetworkEdit', { + extend: 'Proxmox.window.Edit', + + isAdd: true, + + initComponent : function() { + var me = this; + + if (!me.dataCache) { + throw "no dataCache specified"; + } + + if (!me.nodename) { + throw "no node name specified"; + } + + var ipanel = Ext.create('PVE.lxc.NetworkInputPanel', { + ifname: me.ifname, + nodename: me.nodename, + dataCache: me.dataCache, + isCreate: me.isCreate + }); + + Ext.apply(me, { + subject: gettext('Network Device') + ' (veth)', + digest: me.dataCache.digest, + items: [ ipanel ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.lxc.NetworkView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveLxcNetworkView', + + onlineHelp: 'pct_container_network', + + dataCache: {}, // used to store result of last load + + stateful: true, + stateId: 'grid-lxc-network', + + load: function() { + var me = this; + + Proxmox.Utils.setErrorMask(me, true); + + Proxmox.Utils.API2Request({ + url: me.url, + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + var result = Ext.decode(response.responseText); + var data = result.data || {}; + me.dataCache = data; + var records = []; + Ext.Object.each(data, function(key, value) { + if (!key.match(/^net\d+/)) { + return; // continue + } + var net = PVE.Parser.parseLxcNetwork(value); + net.id = key; + records.push(net); + }); + me.store.loadData(records); + me.down('button[name=addButton]').setDisabled((records.length >= 10)); + } + }); + }, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + me.url = '/nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var store = new Ext.data.Store({ + model: 'pve-lxc-network', + sorters: [ + { + property : 'id', + direction: 'ASC' + } + ] + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return !!caps.vms['VM.Config.Network']; + }, + confirmMsg: function (rec) { + return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.id + "'"); + }, + handler: function(btn, event, rec) { + Proxmox.Utils.API2Request({ + url: me.url, + waitMsgTarget: me, + method: 'PUT', + params: { 'delete': rec.data.id, digest: me.dataCache.digest }, + callback: function() { + me.load(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + if (!caps.vms['VM.Config.Network']) { + return false; + } + + var win = Ext.create('PVE.lxc.NetworkEdit', { + url: me.url, + nodename: nodename, + dataCache: me.dataCache, + ifname: rec.data.id + }); + win.on('destroy', me.load, me); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + selModel: sm, + disabled: true, + enableFn: function(rec) { + if (!caps.vms['VM.Config.Network']) { + return false; + } + return true; + }, + handler: run_editor + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + name: 'addButton', + disabled: !caps.vms['VM.Config.Network'], + handler: function() { + var win = Ext.create('PVE.lxc.NetworkEdit', { + url: me.url, + nodename: nodename, + isCreate: true, + dataCache: me.dataCache + }); + win.on('destroy', me.load, me); + win.show(); + } + }, + remove_btn, + edit_btn + ], + columns: [ + { + header: 'ID', + width: 50, + dataIndex: 'id' + }, + { + header: gettext('Name'), + width: 80, + dataIndex: 'name' + }, + { + header: gettext('Bridge'), + width: 80, + dataIndex: 'bridge' + }, + { + header: gettext('Firewall'), + width: 80, + dataIndex: 'firewall', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('VLAN Tag'), + width: 80, + dataIndex: 'tag' + }, + { + header: gettext('MAC address'), + width: 110, + dataIndex: 'hwaddr' + }, + { + header: gettext('IP address'), + width: 150, + dataIndex: 'ip', + renderer: function(value, metaData, rec) { + if (rec.data.ip && rec.data.ip6) { + return rec.data.ip + "
" + rec.data.ip6; + } else if (rec.data.ip6) { + return rec.data.ip6; + } else { + return rec.data.ip; + } + } + }, + { + header: gettext('Gateway'), + width: 150, + dataIndex: 'gw', + renderer: function(value, metaData, rec) { + if (rec.data.gw && rec.data.gw6) { + return rec.data.gw + "
" + rec.data.gw6; + } else if (rec.data.gw6) { + return rec.data.gw6; + } else { + return rec.data.gw; + } + } + } + ], + listeners: { + activate: me.load, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-lxc-network', { + extend: "Ext.data.Model", + proxy: { type: 'memory' }, + fields: [ 'id', 'name', 'hwaddr', 'bridge', + 'ip', 'gw', 'ip6', 'gw6', 'tag', 'firewall' ] + }); + +}); + +/*jslint confusion: true */ +Ext.define('PVE.lxc.RessourceView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveLxcRessourceView'], + + onlineHelp: 'pct_configuration', + + renderKey: function(key, metaData, rec, rowIndex, colIndex, store) { + var me = this; + var rowdef = me.rows[key] || {}; + + metaData.tdAttr = "valign=middle"; + if (rowdef.tdCls) { + metaData.tdCls = rowdef.tdCls; + } + return rowdef.header || key; + }, + + initComponent : function() { + var me = this; + var i, confid; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + var diskCap = caps.vms['VM.Config.Disk']; + + var mpeditor = caps.vms['VM.Config.Disk'] ? 'PVE.lxc.MountPointEdit' : undefined; + + var rows = { + memory: { + header: gettext('Memory'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined, + defaultValue: 512, + tdCls: 'pve-itype-icon-memory', + group: 1, + renderer: function(value) { + return Proxmox.Utils.format_size(value*1024*1024); + } + }, + swap: { + header: gettext('Swap'), + editor: caps.vms['VM.Config.Memory'] ? 'PVE.lxc.MemoryEdit' : undefined, + defaultValue: 512, + tdCls: 'pve-itype-icon-swap', + group: 2, + renderer: function(value) { + return Proxmox.Utils.format_size(value*1024*1024); + } + }, + cores: { + header: gettext('Cores'), + editor: caps.vms['VM.Config.CPU'] ? 'PVE.lxc.CPUEdit' : undefined, + defaultValue: '', + tdCls: 'pve-itype-icon-processor', + group: 3, + renderer: function(value) { + var cpulimit = me.getObjectValue('cpulimit'); + var cpuunits = me.getObjectValue('cpuunits'); + var res; + if (value) { + res = value; + } else { + res = gettext('unlimited'); + } + + if (cpulimit) { + res += ' [cpulimit=' + cpulimit + ']'; + } + + if (cpuunits) { + res += ' [cpuunits=' + cpuunits + ']'; + } + return res; + } + }, + rootfs: { + header: gettext('Root Disk'), + defaultValue: Proxmox.Utils.noneText, + editor: mpeditor, + tdCls: 'pve-itype-icon-storage', + group: 4 + }, + cpulimit: { + visible: false + }, + cpuunits: { + visible: false + }, + unprivileged: { + visible: false + } + }; + + PVE.Utils.forEachMP(function(bus, i) { + confid = bus + i; + var group = 5; + var header; + if (bus === 'mp') { + header = gettext('Mount Point') + ' (' + confid + ')'; + } else { + header = gettext('Unused Disk') + ' ' + i; + group += 1; + } + rows[confid] = { + group: group, + order: i, + tdCls: 'pve-itype-icon-storage', + editor: mpeditor, + header: header + }; + }, true); + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + me.selModel = Ext.create('Ext.selection.RowModel', {}); + + var run_resize = function() { + var rec = me.selModel.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.MPResize', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid + }); + + win.show(); + }; + + var run_remove = function(b, e, rec) { + Proxmox.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'delete': rec.data.key + }, + failure: function (response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + var run_move = function(b, e, rec) { + if (!rec) { + return; + } + + var win = Ext.create('PVE.window.HDMove', { + disk: rec.data.key, + nodename: nodename, + vmid: vmid, + type: 'lxc' + }); + + win.show(); + + win.on('destroy', me.reload, me); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + selModel: me.selModel, + disabled: true, + enableFn: function(rec) { + if (!rec) { + return false; + } + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: function() { me.run_editor(); } + }); + + var resize_btn = new Proxmox.button.Button({ + text: gettext('Resize disk'), + selModel: me.selModel, + disabled: true, + handler: run_resize + }); + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + selModel: me.selModel, + disabled: true, + dangerous: true, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + me.renderKey(rec.data.key, {}, rec) + "'"); + if (rec.data.key.match(/^unused\d+$/)) { + msg += " " + gettext('This will permanently erase all data.'); + } + + return msg; + }, + handler: run_remove + }); + + var move_btn = new Proxmox.button.Button({ + text: gettext('Move Volume'), + selModel: me.selModel, + disabled: true, + dangerous: true, + handler: run_move + }); + + var set_button_status = function() { + var rec = me.selModel.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + remove_btn.disable(); + resize_btn.disable(); + return; + } + var key = rec.data.key; + var value = rec.data.value; + var rowdef = rows[key]; + + var isDisk = (rowdef.tdCls == 'pve-itype-icon-storage'); + + var noedit = rec.data['delete'] || !rowdef.editor; + if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) { + var mp = PVE.Parser.parseLxcMountPoint(value); + if (mp.type !== 'volume') { + noedit = true; + } + } + edit_btn.setDisabled(noedit); + + remove_btn.setDisabled(!isDisk || rec.data.key === 'rootfs' || !diskCap); + resize_btn.setDisabled(!isDisk || !diskCap); + move_btn.setDisabled(!isDisk || !diskCap); + + }; + + var sorterFn = function(rec1, rec2) { + var v1 = rec1.data.key; + var v2 = rec2.data.key; + var g1 = rows[v1].group || 0; + var g2 = rows[v2].group || 0; + var order1 = rows[v1].order || 0; + var order2 = rows[v2].order || 0; + + if ((g1 - g2) !== 0) { + return g1 - g2; + } + + if ((order1 - order2) !== 0) { + return order1 - order2; + } + + if (v1 > v2) { + return 1; + } else if (v1 < v2) { + return -1; + } else { + return 0; + } + }; + + Ext.apply(me, { + url: '/api2/json/' + baseurl, + selModel: me.selModel, + interval: 2000, + cwidth1: 170, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Mount Point'), + iconCls: 'pve-itype-icon-storage', + disabled: !caps.vms['VM.Config.Disk'], + handler: function() { + var win = Ext.create('PVE.lxc.MountPointEdit', { + url: '/api2/extjs/' + baseurl, + unprivileged: me.getObjectValue('unprivileged'), + pveSelNode: me.pveSelNode + }); + win.show(); + } + } + ] + }) + }, + edit_btn, + remove_btn, + resize_btn, + move_btn + ], + rows: rows, + sorterFn: sorterFn, + editorConfig: { + pveSelNode: me.pveSelNode, + url: '/api2/extjs/' + baseurl + }, + listeners: { + itemdblclick: me.run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + Ext.apply(me.editorConfig, { unprivileged: me.getObjectValue('unprivileged') }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.lxc.FeaturesInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveLxcFeaturesInputPanel', + + // used to save the mounts fstypes until sending + mounts: [], + + fstypes: ['nfs', 'cifs'], + + viewModel: { + parent: null, + data: { + unprivileged: false + }, + formulas: { + privilegedOnly: function(get) { + return (get('unprivileged') ? gettext('privileged only') : ''); + }, + unprivilegedOnly: function(get) { + return (!get('unprivileged') ? gettext('unprivileged only') : ''); + } + } + }, + + items: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('keyctl'), + name: 'keyctl', + bind: { + disabled: '{!unprivileged}', + boxLabel: '{unprivilegedOnly}' + } + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Nesting'), + name: 'nesting' + }, + { + xtype: 'proxmoxcheckbox', + name: 'nfs', + fieldLabel: 'NFS', + bind: { + disabled: '{unprivileged}', + boxLabel: '{privilegedOnly}' + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'cifs', + fieldLabel: 'CIFS', + bind: { + disabled: '{unprivileged}', + boxLabel: '{privilegedOnly}' + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'fuse', + fieldLabel: 'FUSE' + } + ], + + onGetValues: function(values) { + var me = this; + var mounts = me.mounts; + me.fstypes.forEach(function(fs) { + if (values[fs]) { + mounts.push(fs); + } + delete values[fs]; + }); + + if (mounts.length) { + values.mount = mounts.join(';'); + } + + var featuresstring = PVE.Parser.printPropertyString(values, undefined); + if (featuresstring == '') { + return { 'delete': 'features' }; + } + return { features: featuresstring }; + }, + + setValues: function(values) { + var me = this; + + me.viewModel.set({ unprivileged: values.unprivileged }); + + if (values.features) { + var res = PVE.Parser.parsePropertyString(values.features); + me.mounts = []; + if (res.mount) { + res.mount.split(/[; ]/).forEach(function(item) { + if (me.fstypes.indexOf(item) === -1) { + me.mounts.push(item); + } else { + res[item] = 1; + } + }); + } + this.callParent([res]); + } + } +}); + +Ext.define('PVE.lxc.FeaturesEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveLxcFeaturesEdit', + + subject: gettext('Features'), + + items: [{ + xtype: 'pveLxcFeaturesInputPanel' + }], + + initComponent : function() { + var me = this; + + me.callParent(); + + me.load(); + } +}); +/*jslint confusion: true */ +Ext.define('PVE.lxc.Options', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveLxcOptions'], + + onlineHelp: 'pct_options', + + initComponent : function() { + var me = this; + var i; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + var rows = { + onboot: { + header: gettext('Start at boot'), + defaultValue: '', + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Start at boot'), + items: { + xtype: 'proxmoxcheckbox', + name: 'onboot', + uncheckedValue: 0, + defaultValue: 0, + fieldLabel: gettext('Start at boot') + } + } : undefined + }, + startup: { + header: gettext('Start/Shutdown order'), + defaultValue: '', + renderer: PVE.Utils.render_kvm_startup, + editor: caps.vms['VM.Config.Options'] && caps.nodes['Sys.Modify'] ? + { + xtype: 'pveWindowStartupEdit', + onlineHelp: 'pct_startup_and_shutdown' + } : undefined + }, + ostype: { + header: gettext('OS Type'), + defaultValue: Proxmox.Utils.unknownText + }, + arch: { + header: gettext('Architecture'), + defaultValue: Proxmox.Utils.unknownText + }, + console: { + header: '/dev/console', + defaultValue: 1, + renderer: Proxmox.Utils.format_enabled_toggle, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: '/dev/console', + items: { + xtype: 'proxmoxcheckbox', + name: 'console', + uncheckedValue: 0, + defaultValue: 1, + deleteDefaultValue: true, + checked: true, + fieldLabel: '/dev/console' + } + } : undefined + }, + tty: { + header: gettext('TTY count'), + defaultValue: 2, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('TTY count'), + items: { + xtype: 'proxmoxintegerfield', + name: 'tty', + minValue: 0, + maxValue: 6, + value: 2, + fieldLabel: gettext('TTY count'), + emptyText: gettext('Default'), + deleteEmpty: true + } + } : undefined + }, + cmode: { + header: gettext('Console mode'), + defaultValue: 'tty', + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Console mode'), + items: { + xtype: 'proxmoxKVComboBox', + name: 'cmode', + deleteEmpty: true, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + " (tty)"], + ['tty', "/dev/tty[X]"], + ['console', "/dev/console"], + ['shell', "shell"] + ], + fieldLabel: gettext('Console mode') + } + } : undefined + }, + protection: { + header: gettext('Protection'), + defaultValue: false, + renderer: Proxmox.Utils.format_boolean, + editor: caps.vms['VM.Config.Options'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Protection'), + items: { + xtype: 'proxmoxcheckbox', + name: 'protection', + uncheckedValue: 0, + defaultValue: 0, + deleteDefaultValue: true, + fieldLabel: gettext('Enabled') + } + } : undefined + }, + unprivileged: { + header: gettext('Unprivileged container'), + renderer: Proxmox.Utils.format_boolean, + defaultValue: 0 + }, + features: { + header: gettext('Features'), + defaultValue: Proxmox.Utils.noneText, + editor: Proxmox.UserName === 'root@pam' ? + 'PVE.lxc.FeaturesEdit' : undefined + }, + hookscript: { + header: gettext('Hookscript') + } + }; + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: function() { me.run_editor(); } + }); + + Ext.apply(me, { + url: "/api2/json/" + baseurl, + selModel: sm, + interval: 5000, + tbar: [ edit_btn ], + rows: rows, + editorConfig: { + url: '/api2/extjs/' + baseurl + }, + listeners: { + itemdblclick: me.run_editor + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + + } +}); + +Ext.define('PVE.lxc.DNSInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcDNSInputPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + var deletes = []; + if (!values.searchdomain && !me.insideWizard) { + deletes.push('searchdomain'); + } + + if (values.nameserver) { + var list = values.nameserver.split(/[\ \,\;]+/); + values.nameserver = list.join(' '); + } else if(!me.insideWizard) { + deletes.push('nameserver'); + } + + if (deletes.length) { + values['delete'] = deletes.join(','); + } + + return values; + }, + + initComponent : function() { + var me = this; + + var items = [ + { + xtype: 'proxmoxtextfield', + name: 'searchdomain', + skipEmptyText: true, + fieldLabel: gettext('DNS domain'), + emptyText: gettext('use host settings'), + allowBlank: true + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('DNS servers'), + vtype: 'IP64AddressList', + allowBlank: true, + emptyText: gettext('use host settings'), + name: 'nameserver', + itemId: 'nameserver' + } + ]; + + if (me.insideWizard) { + me.column1 = items; + } else { + me.items = items; + } + + me.callParent(); + } +}); + +Ext.define('PVE.lxc.DNSEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + var ipanel = Ext.create('PVE.lxc.DNSInputPanel'); + + Ext.apply(me, { + subject: gettext('Resources'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + if (values.nameserver) { + values.nameserver.replace(/[,;]/, ' '); + values.nameserver.replace(/^\s+/, ''); + } + + ipanel.setValues(values); + } + }); + } + } +}); + +/*jslint confusion: true */ +Ext.define('PVE.lxc.DNS', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveLxcDNS'], + + onlineHelp: 'pct_container_network', + + initComponent : function() { + var me = this; + var i; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + var rows = { + hostname: { + required: true, + defaultValue: me.pveSelNode.data.name, + header: gettext('Hostname'), + editor: caps.vms['VM.Config.Network'] ? { + xtype: 'proxmoxWindowEdit', + subject: gettext('Hostname'), + items: { + xtype: 'inputpanel', + items:{ + fieldLabel: gettext('Hostname'), + xtype: 'textfield', + name: 'hostname', + vtype: 'DnsName', + allowBlank: true, + emptyText: 'CT' + vmid.toString() + }, + onGetValues: function(values) { + var params = values; + if (values.hostname === undefined || + values.hostname === null || + values.hostname === '') { + params = { hostname: 'CT'+vmid.toString()}; + } + return params; + } + } + } : undefined + }, + searchdomain: { + header: gettext('DNS domain'), + defaultValue: '', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + renderer: function(value) { + return value || gettext('use host settings'); + } + }, + nameserver: { + header: gettext('DNS server'), + defaultValue: '', + editor: caps.vms['VM.Config.Network'] ? 'PVE.lxc.DNSEdit' : undefined, + renderer: function(value) { + return value || gettext('use host settings'); + } + } + }; + + var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config'; + + var reload = function() { + me.rstore.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rowdef = rows[rec.data.key]; + if (!rowdef.editor) { + return; + } + + var win; + if (Ext.isString(rowdef.editor)) { + win = Ext.create(rowdef.editor, { + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + baseurl + }); + } else { + var config = Ext.apply({ + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + baseurl + }, rowdef.editor); + win = Ext.createWidget(rowdef.editor.xtype, config); + win.load(); + } + //win.load(); + win.show(); + win.on('destroy', reload); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + var rowdef = rows[rec.data.key]; + return !!rowdef.editor; + }, + handler: run_editor + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + edit_btn.disable(); + return; + } + var rowdef = rows[rec.data.key]; + edit_btn.setDisabled(!rowdef.editor); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + nodename + "/lxc/" + vmid + "/config", + selModel: sm, + cwidth1: 150, + run_editor: run_editor, + tbar: [ edit_btn ], + rows: rows, + listeners: { + itemdblclick: run_editor, + selectionchange: set_button_status, + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.lxc.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.lxc.Config', + + onlineHelp: 'chapter_pct', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var template = !!me.pveSelNode.data.template; + + var running = !!me.pveSelNode.data.uptime; + + var caps = Ext.state.Manager.get('GuiCap'); + + var base_url = '/nodes/' + nodename + '/lxc/' + vmid; + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json' + base_url + '/status/current', + interval: 1000 + }); + + var vm_command = function(cmd, params) { + Proxmox.Utils.API2Request({ + params: params, + url: base_url + "/status/" + cmd, + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }; + + var startBtn = Ext.create('Ext.Button', { + text: gettext('Start'), + disabled: !caps.vms['VM.PowerMgmt'] || running, + hidden: template, + handler: function() { + vm_command('start'); + }, + iconCls: 'fa fa-play' + }); + + var stopBtn = Ext.create('Ext.menu.Item',{ + text: gettext('Stop'), + disabled: !caps.vms['VM.PowerMgmt'], + confirmMsg: Proxmox.Utils.format_task_description('vzstop', vmid), + tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'), + dangerous: true, + handler: function() { + vm_command("stop"); + }, + iconCls: 'fa fa-stop' + }); + + var shutdownBtn = Ext.create('PVE.button.Split', { + text: gettext('Shutdown'), + disabled: !caps.vms['VM.PowerMgmt'] || !running, + hidden: template, + confirmMsg: Proxmox.Utils.format_task_description('vzshutdown', vmid), + handler: function() { + vm_command('shutdown'); + }, + menu: { + items:[stopBtn] + }, + iconCls: 'fa fa-power-off' + }); + + var migrateBtn = Ext.create('Ext.Button', { + text: gettext('Migrate'), + disabled: !caps.vms['VM.Migrate'], + hidden: PVE.data.ResourceStore.getNodes().length < 2, + handler: function() { + var win = Ext.create('PVE.window.Migrate', { + vmtype: 'lxc', + nodename: nodename, + vmid: vmid + }); + win.show(); + }, + iconCls: 'fa fa-send-o' + }); + + var moreBtn = Ext.create('Proxmox.button.Button', { + text: gettext('More'), + menu: { items: [ + { + text: gettext('Clone'), + iconCls: 'fa fa-fw fa-clone', + hidden: caps.vms['VM.Clone'] ? false : true, + handler: function() { + PVE.window.Clone.wrap(nodename, vmid, template, 'lxc'); + } + }, + { + text: gettext('Convert to template'), + disabled: template, + xtype: 'pveMenuItem', + iconCls: 'fa fa-fw fa-file-o', + hidden: caps.vms['VM.Allocate'] ? false : true, + confirmMsg: Proxmox.Utils.format_task_description('vztemplate', vmid), + handler: function() { + Proxmox.Utils.API2Request({ + url: base_url + '/template', + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + } + }, + { + iconCls: 'fa fa-heartbeat ', + hidden: !caps.nodes['Sys.Console'], + text: gettext('Manage HA'), + handler: function() { + var ha = me.pveSelNode.data.hastate; + Ext.create('PVE.ha.VMResourceEdit', { + vmid: vmid, + guestType: 'ct', + isCreate: (!ha || ha === 'unmanaged') + }).show(); + } + }, + { + text: gettext('Remove'), + disabled: !caps.vms['VM.Allocate'], + itemId: 'removeBtn', + handler: function() { + Ext.create('PVE.window.SafeDestroy', { + url: base_url, + item: { type: 'CT', id: vmid } + }).show(); + }, + iconCls: 'fa fa-trash-o' + } + ]} + }); + + var vm = me.pveSelNode.data; + + var consoleBtn = Ext.create('PVE.button.ConsoleButton', { + disabled: !caps.vms['VM.Console'], + consoleType: 'lxc', + consoleName: vm.name, + hidden: template, + nodename: nodename, + vmid: vmid + }); + + var statusTxt = Ext.create('Ext.toolbar.TextItem', { + data: { + lock: undefined + }, + tpl: [ + '', + ' ({lock})', + '' + ] + }); + + + Ext.apply(me, { + title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm.text, nodename), + hstateid: 'lxctab', + tbarSpacing: false, + tbar: [ statusTxt, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn ], + defaults: { statusStore: me.statusStore }, + items: [ + { + title: gettext('Summary'), + xtype: 'pveLxcSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + } + ] + }); + + if (caps.vms['VM.Console'] && !template) { + me.items.push( + { + title: gettext('Console'), + itemId: 'consolejs', + iconCls: 'fa fa-terminal', + xtype: 'pveNoVncConsole', + vmid: vmid, + consoleType: 'lxc', + xtermjs: true, + nodename: nodename + } + ); + } + + me.items.push( + { + title: gettext('Resources'), + itemId: 'resources', + expandedOnInit: true, + iconCls: 'fa fa-cube', + xtype: 'pveLxcRessourceView' + }, + { + title: gettext('Network'), + iconCls: 'fa fa-exchange', + itemId: 'network', + xtype: 'pveLxcNetworkView' + }, + { + title: gettext('DNS'), + iconCls: 'fa fa-globe', + itemId: 'dns', + xtype: 'pveLxcDNS' + }, + { + title: gettext('Options'), + itemId: 'options', + iconCls: 'fa fa-gear', + xtype: 'pveLxcOptions' + }, + { + title: gettext('Task History'), + itemId: 'tasks', + iconCls: 'fa fa-list', + xtype: 'proxmoxNodeTasks', + nodename: nodename, + vmidFilter: vmid + } + ); + + if (caps.vms['VM.Backup']) { + me.items.push({ + title: gettext('Backup'), + iconCls: 'fa fa-floppy-o', + xtype: 'pveBackupView', + itemId: 'backup' + }, + { + title: gettext('Replication'), + iconCls: 'fa fa-retweet', + xtype: 'pveReplicaView', + itemId: 'replication' + }); + } + + if ((caps.vms['VM.Snapshot'] || caps.vms['VM.Snapshot.Rollback']) && !template) { + me.items.push({ + title: gettext('Snapshots'), + iconCls: 'fa fa-history', + xtype: 'pveLxcSnapshotTree', + itemId: 'snapshot' + }); + } + + if (caps.vms['VM.Console']) { + me.items.push( + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + iconCls: 'fa fa-shield', + allow_iface: true, + base_url: base_url + '/firewall/rules', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + groups: ['firewall'], + iconCls: 'fa fa-gear', + onlineHelp: 'pve_firewall_vm_container_configuration', + title: gettext('Options'), + base_url: base_url + '/firewall/options', + fwtype: 'vm', + itemId: 'firewall-options' + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: base_url + '/firewall/aliases', + itemId: 'firewall-aliases' + }, + { + xtype: 'pveIPSet', + title: gettext('IPSet'), + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: base_url + '/firewall/ipset', + list_refs_url: base_url + '/firewall/refs', + itemId: 'firewall-ipset' + }, + { + title: gettext('Log'), + groups: ['firewall'], + iconCls: 'fa fa-list', + onlineHelp: 'chapter_pve_firewall', + itemId: 'firewall-fwlog', + xtype: 'proxmoxLogView', + url: '/api2/extjs' + base_url + '/firewall/log' + } + ); + } + + if (caps.vms['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + itemId: 'permissions', + iconCls: 'fa fa-unlock', + path: '/vms/' + vmid + }); + } + + me.callParent(); + + me.mon(me.statusStore, 'load', function(s, records, success) { + var status; + var lock; + if (!success) { + status = 'unknown'; + } else { + var rec = s.data.get('status'); + status = rec ? rec.data.value : 'unknown'; + rec = s.data.get('template'); + template = rec.data.value || false; + rec = s.data.get('lock'); + lock = rec ? rec.data.value : undefined; + } + + statusTxt.update({ lock: lock }); + + startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template); + shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running'); + stopBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'stopped'); + me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped'); + consoleBtn.setDisabled(template); + }); + + me.on('afterrender', function() { + me.statusStore.startUpdate(); + }); + + me.on('destroy', function() { + me.statusStore.stopUpdate(); + }); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.lxc.CreateWizard', { + extend: 'PVE.window.Wizard', + mixins: ['Proxmox.Mixin.CBind'], + + viewModel: { + data: { + nodename: '', + storage: '', + unprivileged: true + } + }, + + cbindData: { + nodename: undefined + }, + + subject: gettext('LXC Container'), + + items: [ + { + xtype: 'inputpanel', + title: gettext('General'), + onlineHelp: 'pct_general', + column1: [ + { + xtype: 'pveNodeSelector', + name: 'nodename', + cbind: { + selectCurNode: '{!nodename}', + preferredValue: '{nodename}' + }, + bind: { + value: '{nodename}' + }, + fieldLabel: gettext('Node'), + allowBlank: false, + onlineValidator: true + }, + { + xtype: 'pveGuestIDSelector', + name: 'vmid', // backend only knows vmid + guestType: 'lxc', + value: '', + loadNextFreeID: true, + validateExists: false + }, + { + xtype: 'proxmoxtextfield', + name: 'hostname', + vtype: 'DnsName', + value: '', + fieldLabel: gettext('Hostname'), + skipEmptyText: true, + allowBlank: true + }, + { + xtype: 'proxmoxcheckbox', + name: 'unprivileged', + value: true, + bind: { + value: '{unprivileged}' + }, + fieldLabel: gettext('Unprivileged container') + } + ], + column2: [ + { + xtype: 'pvePoolSelector', + fieldLabel: gettext('Resource Pool'), + name: 'pool', + value: '', + allowBlank: true + }, + { + xtype: 'textfield', + inputType: 'password', + name: 'password', + value: '', + fieldLabel: gettext('Password'), + allowBlank: false, + minLength: 5, + change: function(f, value) { + if (f.rendered) { + f.up().down('field[name=confirmpw]').validate(); + } + } + }, + { + xtype: 'textfield', + inputType: 'password', + name: 'confirmpw', + value: '', + fieldLabel: gettext('Confirm password'), + allowBlank: true, + submitValue: false, + validator: function(value) { + var pw = this.up().down('field[name=password]').getValue(); + if (pw !== value) { + return "Passwords do not match!"; + } + return true; + } + }, + { + xtype: 'proxmoxtextfield', + name: 'ssh-public-keys', + value: '', + fieldLabel: gettext('SSH public key'), + allowBlank: true, + validator: function(value) { + var pwfield = this.up().down('field[name=password]'); + if (value.length) { + var key = PVE.Parser.parseSSHKey(value); + if (!key) { + return "Failed to recognize ssh key"; + } + pwfield.allowBlank = true; + } else { + pwfield.allowBlank = false; + } + pwfield.validate(); + return true; + }, + afterRender: function() { + if (!window.FileReader) { + // No FileReader support in this browser + return; + } + var cancel = function(ev) { + ev = ev.event; + if (ev.preventDefault) { + ev.preventDefault(); + } + }; + var field = this; + field.inputEl.on('dragover', cancel); + field.inputEl.on('dragenter', cancel); + field.inputEl.on('drop', function(ev) { + ev = ev.event; + if (ev.preventDefault) { + ev.preventDefault(); + } + var files = ev.dataTransfer.files; + PVE.Utils.loadSSHKeyFromFile(files[0], function(v) { + field.setValue(v); + }); + }); + } + }, + { + xtype: 'filebutton', + name: 'file', + hidden: !window.FileReader, + text: gettext('Load SSH Key File'), + listeners: { + change: function(btn, e, value) { + e = e.event; + var field = this.up().down('proxmoxtextfield[name=ssh-public-keys]'); + PVE.Utils.loadSSHKeyFromFile(e.target.files[0], function(v) { + field.setValue(v); + }); + btn.reset(); + } + } + } + ] + }, + { + xtype: 'inputpanel', + title: gettext('Template'), + onlineHelp: 'pct_container_images', + column1: [ + { + xtype: 'pveStorageSelector', + name: 'tmplstorage', + fieldLabel: gettext('Storage'), + storageContent: 'vztmpl', + autoSelect: true, + allowBlank: false, + bind: { + value: '{storage}', + nodename: '{nodename}' + } + }, + { + xtype: 'pveFileSelector', + name: 'ostemplate', + storageContent: 'vztmpl', + fieldLabel: gettext('Template'), + bind: { + storage: '{storage}', + nodename: '{nodename}' + }, + allowBlank: false + } + ] + }, + { + xtype: 'pveLxcMountPointInputPanel', + title: gettext('Root Disk'), + insideWizard: true, + isCreate: true, + unused: false, + bind: { + nodename: '{nodename}', + unprivileged: '{unprivileged}' + }, + confid: 'rootfs' + }, + { + xtype: 'pveLxcCPUInputPanel', + title: gettext('CPU'), + insideWizard: true + }, + { + xtype: 'pveLxcMemoryInputPanel', + title: gettext('Memory'), + insideWizard: true + }, + { + xtype: 'pveLxcNetworkInputPanel', + title: gettext('Network'), + insideWizard: true, + bind: { + nodename: '{nodename}' + }, + isCreate: true + }, + { + xtype: 'pveLxcDNSInputPanel', + title: gettext('DNS'), + insideWizard: true + }, + { + title: gettext('Confirm'), + layout: 'fit', + items: [ + { + xtype: 'grid', + store: { + model: 'KeyValue', + sorters: [{ + property : 'key', + direction: 'ASC' + }] + }, + columns: [ + {header: 'Key', width: 150, dataIndex: 'key'}, + {header: 'Value', flex: 1, dataIndex: 'value'} + ] + } + ], + dockedItems: [ + { + xtype: 'proxmoxcheckbox', + name: 'start', + dock: 'bottom', + margin: '5 0 0 0', + boxLabel: gettext('Start after created') + } + ], + listeners: { + show: function(panel) { + var wizard = this.up('window'); + var kv = wizard.getValues(); + var data = []; + Ext.Object.each(kv, function(key, value) { + if (key === 'delete' || key === 'tmplstorage') { // ignore + return; + } + if (key === 'password') { // don't show pw + return; + } + var html = Ext.htmlEncode(Ext.JSON.encode(value)); + data.push({ key: key, value: value }); + }); + + var summarystore = panel.down('grid').getStore(); + summarystore.suspendEvents(); + summarystore.removeAll(); + summarystore.add(data); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh'); + } + }, + onSubmit: function() { + var wizard = this.up('window'); + var kv = wizard.getValues(); + delete kv['delete']; + + var nodename = kv.nodename; + delete kv.nodename; + delete kv.tmplstorage; + + if (!kv.pool.length) { + delete kv.pool; + } + + if (!kv.password.length && kv['ssh-public-keys']) { + delete kv.password; + } + + Proxmox.Utils.API2Request({ + url: '/nodes/' + nodename + '/lxc', + waitMsgTarget: wizard, + method: 'POST', + params: kv, + success: function(response, opts){ + var upid = response.result.data; + + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: upid + }); + win.show(); + wizard.close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + } + ] +}); + + + +Ext.define('PVE.lxc.SnapshotTree', { + extend: 'Ext.tree.Panel', + alias: ['widget.pveLxcSnapshotTree'], + + onlineHelp: 'pct_snapshots', + + load_delay: 3000, + + old_digest: 'invalid', + + stateful: true, + stateId: 'grid-lxc-snapshots', + + sorterFn: function(rec1, rec2) { + var v1 = rec1.data.snaptime; + var v2 = rec2.data.snaptime; + + if (rec1.data.name === 'current') { + return 1; + } + if (rec2.data.name === 'current') { + return -1; + } + + return (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)); + }, + + reload: function(repeat) { + var me = this; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot', + method: 'GET', + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(me, response.htmlStatus); + me.load_task.delay(me.load_delay); + }, + success: function(response, opts) { + Proxmox.Utils.setErrorMask(me, false); + var digest = 'invalid'; + var idhash = {}; + var root = { name: '__root', expanded: true, children: [] }; + Ext.Array.each(response.result.data, function(item) { + item.leaf = true; + item.children = []; + if (item.name === 'current') { + digest = item.digest + item.running; + if (item.running) { + item.iconCls = 'fa fa-fw fa-desktop x-fa-tree-running'; + } else { + item.iconCls = 'fa fa-fw fa-desktop x-fa-tree'; + } + } else { + item.iconCls = 'fa fa-fw fa-history x-fa-tree'; + } + idhash[item.name] = item; + }); + + if (digest !== me.old_digest) { + me.old_digest = digest; + + Ext.Array.each(response.result.data, function(item) { + if (item.parent && idhash[item.parent]) { + var parent_item = idhash[item.parent]; + parent_item.children.push(item); + parent_item.leaf = false; + parent_item.expanded = true; + parent_item.expandable = false; + } else { + root.children.push(item); + } + }); + + me.setRootNode(root); + } + + me.load_task.delay(me.load_delay); + } + }); + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/feature', + params: { feature: 'snapshot' }, + method: 'GET', + success: function(response, options) { + var res = response.result.data; + if (res.hasFeature) { + var snpBtns = Ext.ComponentQuery.query('#snapshotBtn'); + snpBtns.forEach(function(item){ + item.enable(); + }); + } + } + }); + + + }, + + listeners: { + beforestatesave: function(grid, state, eopts) { + // extjs cannot serialize functions, + // so a the sorter with only the sorterFn will + // not be a valid sorter when restoring the state + delete state.storeState.sorters; + } + }, + + initComponent: function() { + var me = this; + + me.nodename = me.pveSelNode.data.node; + if (!me.nodename) { + throw "no node name specified"; + } + + me.vmid = me.pveSelNode.data.vmid; + if (!me.vmid) { + throw "no VM ID specified"; + } + + me.load_task = new Ext.util.DelayedTask(me.reload, me); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var valid_snapshot = function(record) { + return record && record.data && record.data.name && + record.data.name !== 'current'; + }; + + var valid_snapshot_rollback = function(record) { + return record && record.data && record.data.name && + record.data.name !== 'current' && !record.data.snapstate; + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (valid_snapshot(rec)) { + var win = Ext.create('PVE.window.LxcSnapshot', { + snapname: rec.data.name, + nodename: me.nodename, + vmid: me.vmid + }); + win.show(); + me.mon(win, 'close', me.reload, me); + } + }; + + var editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + enableFn: valid_snapshot, + handler: run_editor + }); + + var rollbackBtn = new Proxmox.button.Button({ + text: gettext('Rollback'), + disabled: true, + dangerous: true, + selModel: sm, + enableFn: valid_snapshot_rollback, + confirmMsg: function(rec) { + var taskdescription = Proxmox.Utils.format_task_description('vzrollback', me.vmid); + var snaptime = Ext.Date.format(rec.data.snaptime,'Y-m-d H:i:s'); + var snapname = rec.data.name; + + var msg = Ext.String.format(gettext('{0} to {1} ({2})'), + taskdescription, snapname, snaptime); + msg += '

' + gettext('Note: Rollback stops CT') + '

'; + + return msg; + }, + handler: function(btn, event) { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var snapname = rec.data.name; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot/' + snapname + '/rollback', + method: 'POST', + waitMsgTarget: me, + callback: function() { + me.reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + } + }); + } + }); + + var removeBtn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + rec.data.name + "'"); + return msg; + }, + enableFn: valid_snapshot, + handler: function(btn, event) { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var snapname = rec.data.name; + + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/snapshot/' + snapname, + method: 'DELETE', + waitMsgTarget: me, + callback: function() { + me.reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + } + }); + } + }); + + var snapshotBtn = Ext.create('Ext.Button', { + itemId: 'snapshotBtn', + text: gettext('Take Snapshot'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.window.LxcSnapshot', { + nodename: me.nodename, + vmid: me.vmid + }); + win.show(); + } + }); + + Ext.apply(me, { + layout: 'fit', + rootVisible: false, + animate: false, + sortableColumns: false, + selModel: sm, + tbar: [ snapshotBtn, rollbackBtn, removeBtn, editBtn ], + fields: [ + 'name', 'description', 'snapstate', 'vmstate', 'running', + { name: 'snaptime', type: 'date', dateFormat: 'timestamp' } + ], + columns: [ + { + xtype: 'treecolumn', + text: gettext('Name'), + dataIndex: 'name', + width: 200, + renderer: function(value, metaData, record) { + if (value === 'current') { + return "NOW"; + } else { + return value; + } + } + }, +// { +// text: gettext('RAM'), +// align: 'center', +// resizable: false, +// dataIndex: 'vmstate', +// width: 50, +// renderer: function(value, metaData, record) { +// if (record.data.name !== 'current') { +// return Proxmox.Utils.format_boolean(value); +// } +// } +// }, + { + text: gettext('Date') + "/" + gettext("Status"), + dataIndex: 'snaptime', + resizable: false, + width: 150, + renderer: function(value, metaData, record) { + if (record.data.snapstate) { + return record.data.snapstate; + } + if (value) { + return Ext.Date.format(value,'Y-m-d H:i:s'); + } + } + }, + { + text: gettext('Description'), + dataIndex: 'description', + flex: 1, + renderer: function(value, metaData, record) { + if (record.data.name === 'current') { + return gettext("You are here!"); + } else { + return Ext.String.htmlEncode(value); + } + } + } + ], + columnLines: true, + listeners: { + activate: me.reload, + destroy: me.load_task.cancel, + itemdblclick: run_editor + } + }); + + me.callParent(); + + me.store.sorters.add(new Ext.util.Sorter({ + sorterFn: me.sorterFn + })); + } +}); +Ext.define('PVE.window.LxcSnapshot', { + extend: 'Ext.window.Window', + + resizable: false, + + // needed for finding the reference to submitbutton + // because we do not have a controller + referenceHolder: true, + defaultButton: 'submitbutton', + defaultFocus: 'field', + + take_snapshot: function(snapname, descr, vmstate) { + var me = this; + var params = { snapname: snapname }; + if (descr) { + params.description = descr; + } + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot", + waitMsgTarget: me, + method: 'POST', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid }); + win.show(); + me.close(); + } + }); + }, + + update_snapshot: function(snapname, descr) { + var me = this; + Proxmox.Utils.API2Request({ + params: { description: descr }, + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot/" + + snapname + '/config', + waitMsgTarget: me, + method: 'PUT', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + me.close(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + var summarystore = Ext.create('Ext.data.Store', { + model: 'KeyValue', + sorters: [ + { + property : 'key', + direction: 'ASC' + } + ] + }); + + var items = [ + { + xtype: me.snapname ? 'displayfield' : 'textfield', + name: 'snapname', + value: me.snapname, + fieldLabel: gettext('Name'), + vtype: 'ConfigId', + allowBlank: false + } + ]; + + if (me.snapname) { + items.push({ + xtype: 'displayfield', + name: 'snaptime', + renderer: PVE.Utils.render_timestamp_human_readable, + fieldLabel: gettext('Timestamp') + }); + } + + items.push({ + xtype: 'textareafield', + grow: true, + name: 'description', + fieldLabel: gettext('Description') + }); + + if (me.snapname) { + items.push({ + title: gettext('Settings'), + xtype: 'grid', + height: 200, + store: summarystore, + columns: [ + {header: gettext('Key'), width: 150, dataIndex: 'key'}, + {header: gettext('Value'), flex: 1, dataIndex: 'value'} + ] + }); + } + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + if (me.snapname) { + me.title = gettext('Edit') + ': ' + gettext('Snapshot'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Update'), + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.update_snapshot(me.snapname, values.description); + } + } + }); + } else { + me.title ="VM " + me.vmid + ': ' + gettext('Take Snapshot'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Take Snapshot'), + reference: 'submitbutton', + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.take_snapshot(values.snapname, values.description); + } + } + }); + } + + Ext.apply(me, { + modal: true, + width: 450, + border: false, + layout: 'fit', + buttons: [ submitBtn ], + items: [ me.formPanel ] + }); + + if (me.snapname) { + Ext.apply(me, { + width: 620, + height: 420 + }); + } + + me.callParent(); + + if (!me.snapname) { + return; + } + + // else load data + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + "/snapshot/" + + me.snapname + '/config', + waitMsgTarget: me, + method: 'GET', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.close(); + }, + success: function(response, options) { + var data = response.result.data; + var kvarray = []; + Ext.Object.each(data, function(key, value) { + if (key === 'description' || key === 'snaptime') { + return; + } + kvarray.push({ key: key, value: value }); + }); + + summarystore.suspendEvents(); + summarystore.add(kvarray); + summarystore.sort(); + summarystore.resumeEvents(); + summarystore.fireEvent('refresh', summarystore); + + form.findField('snaptime').setValue(data.snaptime); + form.findField('description').setValue(data.description); + } + }); + } +}); +/*jslint confusion: true */ +var labelWidth = 120; + +Ext.define('PVE.lxc.MemoryEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.apply(me, { + subject: gettext('Memory'), + items: Ext.create('PVE.lxc.MemoryInputPanel') + }); + + me.callParent(); + + me.load(); + } +}); + + +Ext.define('PVE.lxc.CPUEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + Ext.apply(me, { + subject: gettext('CPU'), + items: Ext.create('PVE.lxc.CPUInputPanel') + }); + + me.callParent(); + + me.load(); + } +}); + +Ext.define('PVE.lxc.CPUInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcCPUInputPanel', + + onlineHelp: 'pct_cpu', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + PVE.Utils.delete_if_default(values, 'cores', '', me.insideWizard); + // cpu{limit,unit} aren't in the wizard so create is always false + PVE.Utils.delete_if_default(values, 'cpulimit', '0', 0); + PVE.Utils.delete_if_default(values, 'cpuunits', '1024', 0); + + return values; + }, + + advancedColumn1: [ + { + xtype: 'numberfield', + name: 'cpulimit', + minValue: 0, + value: '', + step: 1, + fieldLabel: gettext('CPU limit'), + allowBlank: true, + emptyText: gettext('unlimited') + } + ], + + advancedColumn2: [ + { + xtype: 'proxmoxintegerfield', + name: 'cpuunits', + fieldLabel: gettext('CPU units'), + value: 1024, + minValue: 8, + maxValue: 500000, + labelWidth: labelWidth, + allowBlank: false + } + ], + + initComponent: function() { + var me = this; + + me.column1 = [ + { + xtype: 'proxmoxintegerfield', + name: 'cores', + minValue: 1, + maxValue: 128, + value: me.insideWizard ? 1 : '', + fieldLabel: gettext('Cores'), + allowBlank: true, + deleteEmpty: true, + emptyText: gettext('unlimited') + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.lxc.MemoryInputPanel', { + extend: 'Proxmox.panel.InputPanel', + alias: 'widget.pveLxcMemoryInputPanel', + + onlineHelp: 'pct_memory', + + insideWizard: false, + + initComponent : function() { + var me = this; + + var items = [ + { + xtype: 'proxmoxintegerfield', + name: 'memory', + minValue: 16, + value: '512', + step: 32, + fieldLabel: gettext('Memory') + ' (MiB)', + labelWidth: labelWidth, + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + name: 'swap', + minValue: 0, + value: '512', + step: 32, + fieldLabel: gettext('Swap') + ' (MiB)', + labelWidth: labelWidth, + allowBlank: false + } + ]; + + if (me.insideWizard) { + me.column1 = items; + } else { + me.items = items; + } + + me.callParent(); + } +}); +Ext.define('PVE.window.MPResize', { + extend: 'Ext.window.Window', + + resizable: false, + + resize_disk: function(disk, size) { + var me = this; + var params = { disk: disk, size: '+' + size + 'G' }; + + Proxmox.Utils.API2Request({ + params: params, + url: '/nodes/' + me.nodename + '/lxc/' + me.vmid + '/resize', + waitMsgTarget: me, + method: 'PUT', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, opts) { + var upid = response.result.data; + var win = Ext.create('Proxmox.window.TaskViewer', { upid: upid }); + win.show(); + me.close(); + } + }); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.vmid) { + throw "no VM ID specified"; + } + + var items = [ + { + xtype: 'displayfield', + name: 'disk', + value: me.disk, + fieldLabel: gettext('Disk'), + vtype: 'StorageId', + allowBlank: false + } + ]; + + me.hdsizesel = Ext.createWidget('numberfield', { + name: 'size', + minValue: 0, + maxValue: 128*1024, + decimalPrecision: 3, + value: '0', + fieldLabel: gettext('Size Increment') + ' (GiB)', + allowBlank: false + }); + + items.push(me.hdsizesel); + + me.formPanel = Ext.create('Ext.form.Panel', { + bodyPadding: 10, + border: false, + fieldDefaults: { + labelWidth: 120, + anchor: '100%' + }, + items: items + }); + + var form = me.formPanel.getForm(); + + var submitBtn; + + me.title = gettext('Resize disk'); + submitBtn = Ext.create('Ext.Button', { + text: gettext('Resize disk'), + handler: function() { + if (form.isValid()) { + var values = form.getValues(); + me.resize_disk(me.disk, values.size); + } + } + }); + + Ext.apply(me, { + modal: true, + border: false, + layout: 'fit', + buttons: [ submitBtn ], + items: [ me.formPanel ] + }); + + + me.callParent(); + + if (!me.disk) { + return; + } + + } +}); +/*jslint confusion: true*/ +/* hidden: boolean and string + * bind: function and object + * disabled: boolean and string + */ +Ext.define('PVE.lxc.MountPointInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pveLxcMountPointInputPanel', + + insideWizard: false, + + onlineHelp: 'pct_container_storage', + + unused: false, // add unused disk imaged + + unprivileged: false, + + vmconfig: {}, // used to select unused disks + + setUnprivileged: function(unprivileged) { + var me = this; + var vm = me.getViewModel(); + me.unprivileged = unprivileged; + vm.set('unpriv', unprivileged); + }, + + onGetValues: function(values) { + var me = this; + + var confid = me.confid || "mp"+values.mpid; + values.file = me.down('field[name=file]').getValue(); + if (values.mountoptions) { + values.mountoptions = values.mountoptions.join(';'); + } + + if (me.unused) { + confid = "mp"+values.mpid; + } else if (me.isCreate) { + values.file = values.hdstorage + ':' + values.disksize; + } + + // delete unnecessary fields + delete values.mpid; + delete values.hdstorage; + delete values.disksize; + delete values.diskformat; + + var res = {}; + res[confid] = PVE.Parser.printLxcMountPoint(values); + return res; + }, + + + setMountPoint: function(mp) { + var me = this; + var vm = this.getViewModel(); + vm.set('mptype', mp.type); + if (mp.mountoptions) { + mp.mountoptions = mp.mountoptions.split(';'); + } + + if (this.confid === 'rootfs') { + var field = me.down('field[name=mountoptions]'); + var forbidden = ['nodev', 'noexec']; + var filtered = field.comboItems.filter(e => !forbidden.includes(e[0])); + field.setComboItems(filtered); + } + + me.setValues(mp); + }, + + setVMConfig: function(vmconfig) { + var me = this; + var vm = me.getViewModel(); + me.vmconfig = vmconfig; + vm.set('unpriv', vmconfig.unprivileged); + + PVE.Utils.forEachMP(function(bus, i) { + var name = "mp" + i.toString(); + if (!Ext.isDefined(vmconfig[name])) { + me.down('field[name=mpid]').setValue(i); + return false; + } + }); + }, + + setNodename: function(nodename) { + var me = this; + var vm = me.getViewModel(); + vm.set('node', nodename); + me.down('#diskstorage').setNodename(nodename); + }, + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'field[name=mpid]': { + change: function(field, value) { + field.validate(); + } + }, + '#hdstorage': { + change: function(field, newValue) { + var me = this; + if (!newValue) { + return; + } + + var rec = field.store.getById(newValue); + if (!rec) { + return; + } + + var vm = me.getViewModel(); + vm.set('type', rec.data.type); + } + } + }, + + init: function(view) { + var me = this; + var vm = this.getViewModel(); + vm.set('confid', view.confid); + vm.set('unused', view.unused); + vm.set('node', view.nodename); + vm.set('unpriv', view.unprivileged); + vm.set('hideStorSelector', view.unused || !view.isCreate); + } + }, + + viewModel: { + data: { + unpriv: false, + unused: false, + showStorageSelector: false, + mptype: '', + type: '', + confid: '', + node: '' + }, + + formulas: { + quota: function(get) { + return !(get('type') === 'zfs' || + get('type') === 'zfspool' || + get('unpriv') || + get('isBind')); + }, + hasMP: function(get) { + return !!get('confid') && !get('unused'); + }, + isRoot: function(get) { + return get('confid') === 'rootfs'; + }, + isBind: function(get) { + return get('mptype') === 'bind'; + }, + isBindOrRoot: function(get) { + return get('isBind') || get('isRoot'); + } + } + }, + + column1: [ + { + xtype: 'proxmoxintegerfield', + name: 'mpid', + fieldLabel: gettext('Mount Point ID'), + minValue: 0, + maxValue: PVE.Utils.mp_counts.mps - 1, + hidden: true, + allowBlank: false, + disabled: true, + bind: { + hidden: '{hasMP}', + disabled: '{hasMP}' + }, + validator: function(value) { + var me = this.up('inputpanel'); + if (!me.rendered) { + return; + } + if (Ext.isDefined(me.vmconfig["mp"+value])) { + return "Mount point is already in use."; + } + /*jslint confusion: true*/ + /* returns a string above */ + return true; + } + }, + { + xtype: 'pveDiskStorageSelector', + itemId: 'diskstorage', + storageContent: 'rootdir', + hidden: true, + autoSelect: true, + selectformat: false, + defaultSize: 8, + bind: { + hidden: '{hideStorSelector}', + disabled: '{hideStorSelector}', + nodename: '{node}' + } + }, + { + xtype: 'textfield', + disabled: true, + submitValue: false, + fieldLabel: gettext('Disk image'), + name: 'file', + bind: { + hidden: '{!hideStorSelector}' + } + } + ], + + column2: [ + { + xtype: 'textfield', + name: 'mp', + value: '', + emptyText: gettext('/some/path'), + allowBlank: false, + disabled: true, + fieldLabel: gettext('Path'), + bind: { + hidden: '{isRoot}', + disabled: '{isRoot}' + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'backup', + fieldLabel: gettext('Backup'), + bind: { + hidden: '{isRoot}', + disabled: '{isBindOrRoot}' + } + } + ], + + advancedColumn1: [ + { + xtype: 'proxmoxcheckbox', + name: 'quota', + defaultValue: 0, + bind: { + disabled: '{!quota}' + }, + fieldLabel: gettext('Enable quota'), + listeners: { + disable: function() { + this.reset(); + } + } + }, + { + xtype: 'proxmoxcheckbox', + name: 'ro', + defaultValue: 0, + bind: { + hidden: '{isRoot}', + disabled: '{isRoot}' + }, + fieldLabel: gettext('Read-only') + }, + { + xtype: 'proxmoxKVComboBox', + name: 'mountoptions', + fieldLabel: gettext('Mount options'), + deleteEmpty: false, + comboItems: [ + ['noatime', 'noatime'], + ['nodev', 'nodev'], + ['noexec', 'noexec'], + ['nosuid', 'nosuid'] + ], + multiSelect: true, + value: [], + allowBlank: true + }, + ], + + advancedColumn2: [ + { + xtype: 'proxmoxKVComboBox', + name: 'acl', + fieldLabel: 'ACLs', + deleteEmpty: false, + comboItems: [ + ['__default__', Proxmox.Utils.defaultText], + ['1', Proxmox.Utils.enabledText], + ['0', Proxmox.Utils.disabledText] + ], + value: '__default__', + bind: { + disabled: '{isBind}' + }, + allowBlank: true + }, + { + xtype: 'proxmoxcheckbox', + inputValue: '0', // reverses the logic + name: 'replicate', + fieldLabel: gettext('Skip replication') + } + ] +}); + +Ext.define('PVE.lxc.MountPointEdit', { + extend: 'Proxmox.window.Edit', + + unprivileged: false, + + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var unused = me.confid && me.confid.match(/^unused\d+$/); + + me.isCreate = me.confid ? unused : true; + + var ipanel = Ext.create('PVE.lxc.MountPointInputPanel', { + confid: me.confid, + nodename: nodename, + unused: unused, + unprivileged: me.unprivileged, + isCreate: me.isCreate + }); + + var subject; + if (unused) { + subject = gettext('Unused Disk'); + } else if (me.isCreate) { + subject = gettext('Mount Point'); + } else { + subject = gettext('Mount Point') + ' (' + me.confid + ')'; + } + + Ext.apply(me, { + subject: subject, + defaultFocus: me.confid !== 'rootfs' ? 'textfield[name=mp]' : 'tool', + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + /*jslint confusion: true*/ + /*data is defined as array above*/ + var value = response.result.data[me.confid]; + /*jslint confusion: false*/ + var mp = PVE.Parser.parseLxcMountPoint(value); + + if (!mp) { + Ext.Msg.alert(gettext('Error'), 'Unable to parse mount point options'); + me.close(); + return; + } + + ipanel.setMountPoint(mp); + me.isValid(); // trigger validation + } + } + }); + } +}); +Ext.define('PVE.pool.StatusView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pvePoolStatusView'], + disabled: true, + + title: gettext('Status'), + cwidth1: 150, + interval: 30000, + //height: 195, + initComponent : function() { + var me = this; + + var pool = me.pveSelNode.data.pool; + if (!pool) { + throw "no pool specified"; + } + + var rows = { + comment: { + header: gettext('Comment'), + renderer: Ext.String.htmlEncode, + required: true + } + }; + + Ext.apply(me, { + url: "/api2/json/pools/" + pool, + rows: rows + }); + + me.callParent(); + } +}); +Ext.define('PVE.pool.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pvePoolSummary', + + initComponent: function() { + var me = this; + + var pool = me.pveSelNode.data.pool; + if (!pool) { + throw "no pool specified"; + } + + var statusview = Ext.create('PVE.pool.StatusView', { + pveSelNode: me.pveSelNode, + style: 'padding-top:0px' + }); + + var rstore = statusview.rstore; + + Ext.apply(me, { + autoScroll: true, + bodyStyle: 'padding:10px', + defaults: { + style: 'padding-top:10px', + width: 800 + }, + items: [ statusview ] + }); + + me.on('activate', rstore.startUpdate); + me.on('destroy', rstore.stopUpdate); + + me.callParent(); + } +}); +Ext.define('PVE.pool.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.pvePoolConfig', + + onlineHelp: 'pveum_pools', + + initComponent: function() { + var me = this; + + var pool = me.pveSelNode.data.pool; + if (!pool) { + throw "no pool specified"; + } + + Ext.apply(me, { + title: Ext.String.format(gettext("Resource Pool") + ': ' + pool), + hstateid: 'pooltab', + items: [ + { + title: gettext('Summary'), + iconCls: 'fa fa-book', + xtype: 'pvePoolSummary', + itemId: 'summary' + }, + { + title: gettext('Members'), + xtype: 'pvePoolMembers', + iconCls: 'fa fa-th', + pool: pool, + itemId: 'members' + }, + { + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: '/pool/' + pool + } + ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.panel.StorageBase', { + extend: 'Proxmox.panel.InputPanel', + controller: 'storageEdit', + + type: '', + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.storage; + } + + values.disable = values.enable ? 0 : 1; + delete values.enable; + + return values; + }, + + initComponent : function() { + var me = this; + + me.column1.unshift({ + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'storage', + value: me.storageId || '', + fieldLabel: 'ID', + vtype: 'StorageId', + allowBlank: false + }); + + me.column2.unshift( + { + xtype: 'pveNodeSelector', + name: 'nodes', + disabled: me.storageId === 'local', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') +')', + multiSelect: true, + autoSelect: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: true, + uncheckedValue: 0, + fieldLabel: gettext('Enable') + } + ); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.BaseEdit', { + extend: 'Proxmox.window.Edit', + + initComponent : function() { + var me = this; + + me.isCreate = !me.storageId; + + if (me.isCreate) { + me.url = '/api2/extjs/storage'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/storage/' + me.storageId; + me.method = 'PUT'; + } + + var ipanel = Ext.create(me.paneltype, { + type: me.type, + isCreate: me.isCreate, + storageId: me.storageId + }); + + Ext.apply(me, { + subject: PVE.Utils.format_storage_type(me.type), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + var ctypes = values.content || ''; + + values.content = ctypes.split(','); + + if (values.nodes) { + values.nodes = values.nodes.split(','); + } + values.enable = values.disable ? 0 : 1; + + ipanel.setValues(values); + } + }); + } + } +}); +Ext.define('PVE.grid.TemplateSelector', { + extend: 'Ext.grid.GridPanel', + + alias: 'widget.pveTemplateSelector', + + stateful: true, + stateId: 'grid-template-selector', + viewConfig: { + trackOver: false + }, + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var baseurl = "/nodes/" + me.nodename + "/aplinfo"; + var store = new Ext.data.Store({ + model: 'pve-aplinfo', + groupField: 'section', + proxy: { + type: 'proxmox', + url: '/api2/json' + baseurl + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var groupingFeature = Ext.create('Ext.grid.feature.Grouping',{ + groupHeaderTpl: '{[ "Section: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})' + }); + + var reload = function() { + store.load(); + }; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + '->', + gettext('Search'), + { + xtype: 'textfield', + width: 200, + enableKeyEvents: true, + listeners: { + buffer: 500, + keyup: function(field) { + var value = field.getValue().toLowerCase(); + store.clearFilter(true); + store.filterBy(function(rec) { + return (rec.data['package'].toLowerCase().indexOf(value) !== -1) + || (rec.data.headline.toLowerCase().indexOf(value) !== -1); + }); + } + } + } + ], + features: [ groupingFeature ], + columns: [ + { + header: gettext('Type'), + width: 80, + dataIndex: 'type' + }, + { + header: gettext('Package'), + flex: 1, + dataIndex: 'package' + }, + { + header: gettext('Version'), + width: 80, + dataIndex: 'version' + }, + { + header: gettext('Description'), + flex: 1.5, + renderer: Ext.String.htmlEncode, + dataIndex: 'headline' + } + ], + listeners: { + afterRender: reload + } + }); + + me.callParent(); + } + +}, function() { + + Ext.define('pve-aplinfo', { + extend: 'Ext.data.Model', + fields: [ + 'template', 'type', 'package', 'version', 'headline', 'infopage', + 'description', 'os', 'section' + ], + idProperty: 'template' + }); + +}); + +Ext.define('PVE.storage.TemplateDownload', { + extend: 'Ext.window.Window', + alias: 'widget.pveTemplateDownload', + + modal: true, + title: gettext('Templates'), + layout: 'fit', + width: 900, + height: 600, + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + var grid = Ext.create('PVE.grid.TemplateSelector', { + border: false, + scrollable: true, + nodename: me.nodename + }); + + var sm = grid.getSelectionModel(); + + var submitBtn = Ext.create('Proxmox.button.Button', { + text: gettext('Download'), + disabled: true, + selModel: sm, + handler: function(button, event, rec) { + Proxmox.Utils.API2Request({ + url: '/nodes/' + me.nodename + '/aplinfo', + params: { + storage: me.storage, + template: rec.data.template + }, + method: 'POST', + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var upid = response.result.data; + + Ext.create('Proxmox.window.TaskViewer', { + upid: upid, + listeners: { + destroy: me.reloadGrid + } + }).show(); + + me.close(); + } + }); + } + }); + + Ext.apply(me, { + items: grid, + buttons: [ submitBtn ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.Upload', { + extend: 'Ext.window.Window', + alias: 'widget.pveStorageUpload', + + resizable: false, + + modal: true, + + initComponent : function() { + /*jslint confusion: true */ + var me = this; + + var xhr; + + if (!me.nodename) { + throw "no node name specified"; + } + + if (!me.storage) { + throw "no storage ID specified"; + } + + var baseurl = "/nodes/" + me.nodename + "/storage/" + me.storage + "/upload"; + + var pbar = Ext.create('Ext.ProgressBar', { + text: 'Ready', + hidden: true + }); + + me.formPanel = Ext.create('Ext.form.Panel', { + method: 'POST', + waitMsgTarget: true, + bodyPadding: 10, + border: false, + width: 300, + fieldDefaults: { + labelWidth: 100, + anchor: '100%' + }, + items: [ + { + xtype: 'pveContentTypeSelector', + cts: me.contents, + fieldLabel: gettext('Content'), + name: 'content', + value: me.contents[0] || '', + allowBlank: false + }, + { + xtype: 'filefield', + name: 'filename', + buttonText: gettext('Select File...'), + allowBlank: false + }, + pbar + ] + }); + + var form = me.formPanel.getForm(); + + var doStandardSubmit = function() { + form.submit({ + url: "/api2/htmljs" + baseurl, + waitMsg: gettext('Uploading file...'), + success: function(f, action) { + me.close(); + }, + failure: function(f, action) { + var msg = PVE.Utils.extractFormActionError(action); + Ext.Msg.alert(gettext('Error'), msg); + } + }); + }; + + var updateProgress = function(per, bytes) { + var text = (per * 100).toFixed(2) + '%'; + if (bytes) { + text += " (" + Proxmox.Utils.format_size(bytes) + ')'; + } + pbar.updateProgress(per, text); + }; + + var abortBtn = Ext.create('Ext.Button', { + text: gettext('Abort'), + disabled: true, + handler: function() { + me.close(); + } + }); + + var submitBtn = Ext.create('Ext.Button', { + text: gettext('Upload'), + disabled: true, + handler: function(button) { + var fd; + try { + fd = new FormData(); + } catch (err) { + doStandardSubmit(); + return; + } + + button.setDisabled(true); + abortBtn.setDisabled(false); + + var field = form.findField('content'); + fd.append("content", field.getValue()); + field.setDisabled(true); + + field = form.findField('filename'); + var file = field.fileInputEl.dom; + fd.append("filename", file.files[0]); + field.setDisabled(true); + + pbar.setVisible(true); + updateProgress(0); + + xhr = new XMLHttpRequest(); + + xhr.addEventListener("load", function(e) { + if (xhr.status == 200) { + me.close(); + } else { + var msg = gettext('Error') + " " + xhr.status.toString() + ": " + Ext.htmlEncode(xhr.statusText); + if (xhr.responseText !== "") { + var result = Ext.decode(xhr.responseText); + result.message = msg; + msg = Proxmox.Utils.extractRequestError(result, true); + } + Ext.Msg.alert(gettext('Error'), msg, function(btn) { + me.close(); + }); + } + }, false); + + xhr.addEventListener("error", function(e) { + var msg = "Error " + e.target.status.toString() + " occurred while receiving the document."; + Ext.Msg.alert(gettext('Error'), msg, function(btn) { + me.close(); + }); + }); + + xhr.upload.addEventListener("progress", function(evt) { + if (evt.lengthComputable) { + var percentComplete = evt.loaded / evt.total; + updateProgress(percentComplete, evt.loaded); + } + }, false); + + xhr.open("POST", "/api2/json" + baseurl, true); + xhr.send(fd); + } + }); + + form.on('validitychange', function(f, valid) { + submitBtn.setDisabled(!valid); + }); + + Ext.apply(me, { + title: gettext('Upload'), + items: me.formPanel, + buttons: [ abortBtn, submitBtn ], + listeners: { + close: function() { + if (xhr) { + xhr.abort(); + } + } + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.ContentView', { + extend: 'Ext.grid.GridPanel', + + alias: 'widget.pveStorageContentView', + + stateful: true, + stateId: 'grid-storage-content', + viewConfig: { + trackOver: false, + loadMask: false + }, + features: [ + { + ftype: 'grouping', + groupHeaderTpl: '{name} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})' + } + ], + initComponent : function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var storage = me.pveSelNode.data.storage; + if (!storage) { + throw "no storage ID specified"; + } + + var baseurl = "/nodes/" + nodename + "/storage/" + storage + "/content"; + var store = Ext.create('Ext.data.Store',{ + model: 'pve-storage-content', + groupField: 'content', + proxy: { + type: 'proxmox', + url: '/api2/json' + baseurl + }, + sorters: { + property: 'volid', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + store.load(); + me.statusStore.load(); + }; + + Proxmox.Utils.monStoreErrors(me, store); + + var templateButton = Ext.create('Proxmox.button.Button',{ + itemId: 'tmpl-btn', + text: gettext('Templates'), + handler: function() { + var win = Ext.create('PVE.storage.TemplateDownload', { + nodename: nodename, + storage: storage, + reloadGrid: reload + }); + win.show(); + } + }); + + var uploadButton = Ext.create('Proxmox.button.Button', { + contents : ['iso','vztmpl'], + text: gettext('Upload'), + handler: function() { + var me = this; + var win = Ext.create('PVE.storage.Upload', { + nodename: nodename, + storage: storage, + contents: me.contents + }); + win.show(); + win.on('destroy', reload); + } + }); + + var imageRemoveButton; + var removeButton = Ext.create('Proxmox.button.StdRemoveButton',{ + selModel: sm, + enableFn: function(rec) { + if (rec && rec.data.content !== 'images') { + imageRemoveButton.setVisible(false); + removeButton.setVisible(true); + return true; + } + return false; + }, + callback: function() { + reload(); + }, + baseurl: baseurl + '/' + }); + + imageRemoveButton = Ext.create('Proxmox.button.Button',{ + selModel: sm, + hidden: true, + text: gettext('Remove'), + enableFn: function(rec) { + if (rec && rec.data.content === 'images') { + removeButton.setVisible(false); + imageRemoveButton.setVisible(true); + return true; + } + return false; + }, + handler: function(btn, event, rec) { + me = this; + + var url = baseurl + '/' + rec.data.volid; + var vmid = rec.data.vmid; + + var store = PVE.data.ResourceStore; + + if (vmid && store.findVMID(vmid)) { + var guest_node = store.guestNode(vmid); + var storage_path = 'storage/' + nodename + '/' + storage; + + // allow to delete local backed images if a VMID exists on another node. + if (store.storageIsShared(storage_path) || guest_node == nodename) { + var msg = Ext.String.format( + gettext("Cannot remove image, a guest with VMID '{0}' exists!"), vmid); + msg += '
' + gettext("You can delete the image from the guest's hardware pane"); + + Ext.Msg.show({ + title: gettext('Cannot remove disk image.'), + icon: Ext.Msg.ERROR, + msg: msg + }); + return; + } + } + var win = Ext.create('PVE.window.SafeDestroy', { + title: Ext.String.format(gettext("Destroy '{0}'"), rec.data.volid), + showProgress: true, + url: url, + item: { type: 'Image', id: vmid } + }).show(); + win.on('destroy', function() { + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status' + }); + reload(); + + }); + } + }); + + me.statusStore = Ext.create('Proxmox.data.ObjectStore', { + url: '/api2/json/nodes/' + nodename + '/storage/' + storage + '/status' + }); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Restore'), + selModel: sm, + disabled: true, + enableFn: function(rec) { + return rec && rec.data.content === 'backup'; + }, + handler: function(b, e, rec) { + var vmtype; + if (rec.data.volid.match(/vzdump-qemu-/)) { + vmtype = 'qemu'; + } else if (rec.data.volid.match(/vzdump-openvz-/) || rec.data.volid.match(/vzdump-lxc-/)) { + vmtype = 'lxc'; + } else { + return; + } + + var win = Ext.create('PVE.window.Restore', { + nodename: nodename, + volid: rec.data.volid, + volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, rec), + vmtype: vmtype + }); + win.show(); + win.on('destroy', reload); + } + }, + removeButton, + imageRemoveButton, + templateButton, + uploadButton, + { + xtype: 'proxmoxButton', + text: gettext('Show Configuration'), + disabled: true, + selModel: sm, + enableFn: function(rec) { + return rec && rec.data.content === 'backup'; + }, + handler: function(b,e,rec) { + var win = Ext.create('PVE.window.BackupConfig', { + volume: rec.data.volid, + pveSelNode: me.pveSelNode + }); + + win.show(); + } + }, + '->', + gettext('Search') + ':', ' ', + { + xtype: 'textfield', + width: 200, + enableKeyEvents: true, + listeners: { + buffer: 500, + keyup: function(field) { + store.clearFilter(true); + store.filter([ + { + property: 'text', + value: field.getValue(), + anyMatch: true, + caseSensitive: false + } + ]); + } + } + } + ], + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + renderer: PVE.Utils.render_storage_content, + dataIndex: 'text' + }, + { + header: gettext('Format'), + width: 100, + dataIndex: 'format' + }, + { + header: gettext('Type'), + width: 100, + dataIndex: 'content', + renderer: PVE.Utils.format_content_types + }, + { + header: gettext('Size'), + width: 100, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size' + } + ], + listeners: { + activate: reload + } + }); + + me.callParent(); + + // disable the buttons/restrict the upload window + // if templates or uploads are not allowed + me.mon(me.statusStore, 'load', function(s, records, success) { + var availcontent = []; + Ext.Array.each(records, function(item){ + if (item.id === 'content') { + availcontent = item.data.value.split(','); + } + }); + var templ = false; + var upload = false; + var cts = []; + + Ext.Array.each(availcontent, function(content) { + if (content === 'vztmpl') { + templ = true; + cts.push('vztmpl'); + } else if (content === 'iso') { + upload = true; + cts.push('iso'); + } + }); + + if (templ !== upload) { + uploadButton.contents = cts; + } + + templateButton.setDisabled(!templ); + uploadButton.setDisabled(!upload && !templ); + }); + } +}, function() { + + Ext.define('pve-storage-content', { + extend: 'Ext.data.Model', + fields: [ + 'volid', 'content', 'format', 'size', 'used', 'vmid', + 'channel', 'id', 'lun', + { + name: 'text', + convert: function(value, record) { + // check for volid, because if you click on a grouping header, + // it calls convert (but with an empty volid) + if (value || record.data.volid === null) { + return value; + } + return PVE.Utils.render_storage_content(value, {}, record); + } + } + ], + idProperty: 'volid' + }); + +}); +Ext.define('PVE.storage.StatusView', { + extend: 'PVE.panel.StatusView', + alias: 'widget.pveStorageStatusView', + + height: 230, + title: gettext('Status'), + + layout: { + type: 'vbox', + align: 'stretch' + }, + + defaults: { + xtype: 'pveInfoWidget', + padding: '0 30 5 30' + }, + items: [ + { + xtype: 'box', + height: 30 + }, + { + itemId: 'enabled', + title: gettext('Enabled'), + printBar: false, + textField: 'disabled', + renderer: Proxmox.Utils.format_neg_boolean + }, + { + itemId: 'active', + title: gettext('Active'), + printBar: false, + textField: 'active', + renderer: Proxmox.Utils.format_boolean + }, + { + itemId: 'content', + title: gettext('Content'), + printBar: false, + textField: 'content', + renderer: PVE.Utils.format_content_types + }, + { + itemId: 'type', + title: gettext('Type'), + printBar: false, + textField: 'type', + renderer: PVE.Utils.format_storage_type + }, + { + xtype: 'box', + height: 10 + }, + { + itemId: 'usage', + title: gettext('Usage'), + valueField: 'used', + maxField: 'total' + } + ], + + updateTitle: function() { + return; + } +}); +Ext.define('PVE.storage.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveStorageSummary', + scrollable: true, + bodyPadding: 5, + tbar: [ + '->', + { + xtype: 'proxmoxRRDTypeSelector' + } + ], + layout: { + type: 'column' + }, + defaults: { + padding: 5, + columnWidth: 1 + }, + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var storage = me.pveSelNode.data.storage; + if (!storage) { + throw "no storage ID specified"; + } + + var rstore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/status", + interval: 1000 + }); + + var rrdstore = Ext.create('Proxmox.data.RRDStore', { + rrdurl: "/api2/json/nodes/" + nodename + "/storage/" + storage + "/rrddata", + model: 'pve-rrd-storage' + }); + + Ext.apply(me, { + items: [ + { + xtype: 'pveStorageStatusView', + pveSelNode: me.pveSelNode, + rstore: rstore + }, + { + xtype: 'proxmoxRRDChart', + title: gettext('Usage'), + fields: ['total','used'], + fieldTitles: ['Total Size', 'Used Size'], + store: rrdstore + } + ], + listeners: { + activate: function() { rstore.startUpdate(); rrdstore.startUpdate(); }, + destroy: function() { rstore.stopUpdate(); rrdstore.stopUpdate(); } + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.storage.Browser', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.storage.Browser', + + onlineHelp: 'chapter_storage', + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var storeid = me.pveSelNode.data.storage; + if (!storeid) { + throw "no storage ID specified"; + } + + + me.items = [ + { + title: gettext('Summary'), + xtype: 'pveStorageSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + } + ]; + + var caps = Ext.state.Manager.get('GuiCap'); + + Ext.apply(me, { + title: Ext.String.format(gettext("Storage {0} on node {1}"), + "'" + storeid + "'", "'" + nodename + "'"), + hstateid: 'storagetab' + }); + + if (caps.storage['Datastore.Allocate'] || + caps.storage['Datastore.AllocateSpace'] || + caps.storage['Datastore.Audit']) { + me.items.push({ + xtype: 'pveStorageContentView', + title: gettext('Content'), + iconCls: 'fa fa-th', + itemId: 'content' + }); + } + + if (caps.storage['Permissions.Modify']) { + me.items.push({ + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + path: '/storage/' + storeid + }); + } + + me.callParent(); + } +}); +Ext.define('PVE.storage.DirInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_directory', + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'path', + value: '', + fieldLabel: gettext('Directory'), + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'shared', + uncheckedValue: 0, + fieldLabel: gettext('Shared') + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + disabled: true, + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.NFSScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveNFSScan', + + queryParam: 'server', + + valueField: 'path', + displayField: 'path', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.nfsServer) { + me.store.removeAll(); + } + + me.allQuery = me.nfsServer; + + me.callParent(); + }, + + setServer: function(server) { + var me = this; + + me.nfsServer = server; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'path', 'options' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/nfs' + } + }); + + store.sort('path', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.NFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_nfs', + + options : [], + + onGetValues: function(values) { + var me = this; + + var i; + var res = []; + for (i = 0; i < me.options.length; i++) { + var item = me.options[i]; + if (!item.match(/^vers=(.*)$/)) { + res.push(item); + } + } + if (values.nfsversion && values.nfsversion !== '__default__') { + res.push('vers=' + values.nfsversion); + } + delete values.nfsversion; + values.options = res.join(','); + if (values.options === '') { + delete values.options; + if (!me.isCreate) { + values["delete"] = "options"; + } + } + + return me.callParent([values]); + }, + + setValues: function(values) { + var me = this; + if (values.options) { + var res = values.options; + me.options = values.options.split(','); + me.options.forEach(function(item) { + var match = item.match(/^vers=(.*)$/); + if (match) { + values.nfsversion = match[1]; + } + }); + } + return me.callParent([values]); + }, + + initComponent : function() { + var me = this; + + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=export]'); + exportField.setServer(value); + exportField.setValue(''); + } + } + } + }, + { + xtype: me.isCreate ? 'pveNFSScan' : 'displayfield', + name: 'export', + value: '', + fieldLabel: 'Export', + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + disabled: true, + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.advancedColumn1 = [ + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('NFS Version'), + name: 'nfsversion', + value: '__default__', + deleteEmpty: false, + comboItems: [ + ['__default__', Proxmox.Utils.defaultText], + ['3', '3'], + ['4', '4'], + ['4.1', '4.1'], + ['4.2', '4.2'] + ] + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.CIFSScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveCIFSScan', + + queryParam: 'server', + + valueField: 'share', + displayField: 'share', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.cifsServer) { + me.store.removeAll(); + } + + var params = {}; + if (me.cifsUsername && me.cifsPassword) { + params.username = me.cifsUsername; + params.password = me.cifsPassword; + } + + if (me.cifsDomain) { + params.domain = me.cifsDomain; + } + + me.store.getProxy().setExtraParams(params); + me.allQuery = me.cifsServer; + + me.callParent(); + }, + + setServer: function(server) { + this.cifsServer = server; + }, + + setUsername: function(username) { + this.cifsUsername = username; + }, + + setPassword: function(password) { + this.cifsPassword = password; + }, + + setDomain: function(domain) { + this.cifsDomain = domain; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: ['description', 'share'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/cifs' + } + }); + store.sort('share', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.CIFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_cifs', + + initComponent : function() { + var me = this; + + var passwordfield = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + inputType: 'password', + name: 'password', + value: me.isCreate ? '' : '********', + fieldLabel: gettext('Password'), + allowBlank: false, + disabled: me.isCreate, + minLength: 1, + listeners: { + change: function(f, value) { + + if (me.isCreate) { + var exportField = me.down('field[name=share]'); + exportField.setPassword(value); + } + } + } + }); + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=share]'); + exportField.setServer(value); + } + } + } + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + value: '', + fieldLabel: gettext('Username'), + emptyText: gettext('Guest user'), + allowBlank: true, + listeners: { + change: function(f, value) { + if (!me.isCreate) { + return; + } + var exportField = me.down('field[name=share]'); + exportField.setUsername(value); + + if (value == "") { + passwordfield.disable(); + } else { + passwordfield.enable(); + } + passwordfield.validate(); + } + } + }, + passwordfield, + { + xtype: me.isCreate ? 'pveCIFSScan' : 'displayfield', + name: 'share', + value: '', + fieldLabel: 'Share', + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'domain', + value: me.isCreate ? '' : undefined, + fieldLabel: gettext('Domain'), + allowBlank: true, + listeners: { + change: function(f, value) { + if (me.isCreate) { + + var exportField = me.down('field[name=share]'); + exportField.setDomain(value); + } + } + } + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.GlusterFsScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveGlusterFsScan', + + queryParam: 'server', + + valueField: 'volname', + displayField: 'volname', + matchFieldWidth: false, + listConfig: { + loadingText: 'Scanning...', + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.glusterServer) { + me.store.removeAll(); + } + + me.allQuery = me.glusterServer; + + me.callParent(); + }, + + setServer: function(server) { + var me = this; + + me.glusterServer = server; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'volname' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/glusterfs' + } + }); + + store.sort('volname', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.GlusterFsInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_glusterfs', + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'server', + value: '', + fieldLabel: gettext('Server'), + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var volumeField = me.down('field[name=volume]'); + volumeField.setServer(value); + volumeField.setValue(''); + } + } + } + }, + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + name: 'server2', + value: '', + fieldLabel: gettext('Second Server'), + allowBlank: true + }, + { + xtype: me.isCreate ? 'pveGlusterFsScan' : 'displayfield', + name: 'volume', + value: '', + fieldLabel: 'Volume name', + allowBlank: false + }, + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'iso', 'backup', 'vztmpl', 'snippets'], + name: 'content', + value: 'images', + multiSelect: true, + fieldLabel: gettext('Content'), + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + disabled: true, + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.IScsiScan', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveIScsiScan', + + queryParam: 'portal', + valueField: 'target', + displayField: 'target', + matchFieldWidth: false, + listConfig: { + loadingText: gettext('Scanning...'), + width: 350 + }, + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.portal) { + me.store.removeAll(); + } + + me.allQuery = me.portal; + + me.callParent(); + }, + + setPortal: function(portal) { + var me = this; + + me.portal = portal; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'target', 'portal' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/iscsi' + } + }); + + store.sort('target', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.IScsiInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_open_iscsi', + + onGetValues: function(values) { + var me = this; + + values.content = values.luns ? 'images' : 'none'; + delete values.luns; + + return me.callParent([values]); + }, + + setValues: function(values) { + values.luns = (values.content.indexOf('images') !== -1) ? true : false; + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'portal', + value: '', + fieldLabel: 'Portal', + allowBlank: false, + listeners: { + change: function(f, value) { + if (me.isCreate) { + var exportField = me.down('field[name=target]'); + exportField.setPortal(value); + exportField.setValue(''); + } + } + } + }, + { + readOnly: !me.isCreate, + xtype: me.isCreate ? 'pveIScsiScan' : 'displayfield', + name: 'target', + value: '', + fieldLabel: 'Target', + allowBlank: false + } + ]; + + me.column2 = [ + { + xtype: 'checkbox', + name: 'luns', + checked: true, + fieldLabel: gettext('Use LUNs directly') + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.VgSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveVgSelector', + valueField: 'vg', + displayField: 'vg', + queryMode: 'local', + editable: false, + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: {}, // true, + fields: [ 'vg', 'size', 'free' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/lvm' + } + }); + + store.sort('vg', 'ASC'); + + Ext.apply(me, { + store: store, + listConfig: { + loadingText: gettext('Scanning...') + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.BaseStorageSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveBaseStorageSelector', + + existingGroupsText: gettext("Existing volume groups"), + queryMode: 'local', + editable: false, + value: '', + valueField: 'storage', + displayField: 'text', + initComponent : function() { + var me = this; + + var store = Ext.create('Ext.data.Store', { + autoLoad: { + addRecords: true, + params: { + type: 'iscsi' + } + }, + fields: [ 'storage', 'type', 'content', + { + name: 'text', + convert: function(value, record) { + if (record.data.storage) { + return record.data.storage + " (iSCSI)"; + } else { + return me.existingGroupsText; + } + } + }], + proxy: { + type: 'proxmox', + url: '/api2/json/storage/' + } + }); + + store.loadData([{ storage: '' }], true); + + store.sort('storage', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.LVMInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_lvm', + + initComponent : function() { + var me = this; + + me.column1 = []; + + var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + name: 'vgname', + hidden: !!me.isCreate, + disabled: !!me.isCreate, + value: '', + fieldLabel: gettext('Volume group'), + allowBlank: false + }); + + if (me.isCreate) { + var vgField = Ext.create('PVE.storage.VgSelector', { + name: 'vgname', + fieldLabel: gettext('Volume group'), + allowBlank: false + }); + + var baseField = Ext.createWidget('pveFileSelector', { + name: 'base', + hidden: true, + disabled: true, + nodename: 'localhost', + storageContent: 'images', + fieldLabel: gettext('Base volume'), + allowBlank: false + }); + + me.column1.push({ + xtype: 'pveBaseStorageSelector', + name: 'basesel', + fieldLabel: gettext('Base storage'), + submitValue: false, + listeners: { + change: function(f, value) { + if (value) { + vgnameField.setVisible(true); + vgnameField.setDisabled(false); + vgField.setVisible(false); + vgField.setDisabled(true); + baseField.setVisible(true); + baseField.setDisabled(false); + } else { + vgnameField.setVisible(false); + vgnameField.setDisabled(true); + vgField.setVisible(true); + vgField.setDisabled(false); + baseField.setVisible(false); + baseField.setDisabled(true); + } + baseField.setStorage(value); + } + } + }); + + me.column1.push(baseField); + + me.column1.push(vgField); + } + + me.column1.push(vgnameField); + + // here value is an array, + // while before it was a string + /*jslint confusion: true*/ + me.column1.push({ + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false + }); + /*jslint confusion: false*/ + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'shared', + uncheckedValue: 0, + fieldLabel: gettext('Shared') + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.TPoolSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveTPSelector', + + queryParam: 'vg', + valueField: 'lv', + displayField: 'lv', + editable: false, + + doRawQuery: function() { + }, + + onTriggerClick: function() { + var me = this; + + if (!me.queryCaching || me.lastQuery !== me.vg) { + me.store.removeAll(); + } + + me.allQuery = me.vg; + + me.callParent(); + }, + + setVG: function(myvg) { + var me = this; + + me.vg = myvg; + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + fields: [ 'lv' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/lvmthin' + } + }); + + store.sort('lv', 'ASC'); + + Ext.apply(me, { + store: store, + listConfig: { + loadingText: gettext('Scanning...') + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.BaseVGSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveBaseVGSelector', + + valueField: 'vg', + displayField: 'vg', + queryMode: 'local', + editable: false, + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: {}, + fields: [ 'vg', 'size', 'free'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/lvm' + } + }); + + Ext.apply(me, { + store: store, + listConfig: { + loadingText: gettext('Scanning...') + } + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.LvmThinInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_lvmthin', + + initComponent : function() { + var me = this; + + me.column1 = []; + + var vgnameField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + name: 'vgname', + hidden: !!me.isCreate, + disabled: !!me.isCreate, + value: '', + fieldLabel: gettext('Volume group'), + allowBlank: false + }); + + var thinpoolField = Ext.createWidget(me.isCreate ? 'textfield' : 'displayfield', { + name: 'thinpool', + hidden: !!me.isCreate, + disabled: !!me.isCreate, + value: '', + fieldLabel: gettext('Thin Pool'), + allowBlank: false + }); + + if (me.isCreate) { + var vgField = Ext.create('PVE.storage.TPoolSelector', { + name: 'thinpool', + fieldLabel: gettext('Thin Pool'), + allowBlank: false + }); + + me.column1.push({ + xtype: 'pveBaseVGSelector', + name: 'vgname', + fieldLabel: gettext('Volume group'), + listeners: { + change: function(f, value) { + if (me.isCreate) { + vgField.setVG(value); + vgField.setValue(''); + } + } + } + }); + + me.column1.push(vgField); + } + + me.column1.push(vgnameField); + + me.column1.push(thinpoolField); + + // here value is an array, + // while before it was a string + /*jslint confusion: true*/ + me.column1.push({ + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false + }); + /*jslint confusion: false*/ + + me.column2 = []; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.CephFSInputPanel', { + extend: 'PVE.panel.StorageBase', + controller: 'cephstorage', + + onlineHelp: 'storage_cephfs', + + viewModel: { + type: 'cephstorage' + }, + + setValues: function(values) { + if (values.monhost) { + this.viewModel.set('pveceph', false); + this.lookupReference('pvecephRef').setValue(false); + this.lookupReference('pvecephRef').resetOriginalValue(); + } + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + me.type = 'cephfs'; + + me.column1 = []; + + me.column1.push( + { + xtype: 'textfield', + name: 'monhost', + vtype: 'HostList', + value: '', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}' + }, + fieldLabel: 'Monitor(s)', + allowBlank: false + }, + { + xtype: 'displayfield', + reference: 'monhost', + bind: { + disabled: '{!pveceph}', + hidden: '{!pveceph}' + }, + value: '', + fieldLabel: 'Monitor(s)' + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + value: 'admin', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}' + }, + fieldLabel: gettext('User name'), + allowBlank: true + } + ); + + me.column2 = [ + { + xtype: 'pveContentTypeSelector', + cts: ['backup', 'iso', 'vztmpl', 'snippets'], + fieldLabel: gettext('Content'), + name: 'content', + value: 'backup', + multiSelect: true, + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + fieldLabel: gettext('Max Backups'), + name: 'maxfiles', + reference: 'maxfiles', + minValue: 0, + maxValue: 365, + value: me.isCreate ? '1' : undefined, + allowBlank: false + } + ]; + + me.columnB = [{ + xtype: 'proxmoxcheckbox', + name: 'pveceph', + reference: 'pvecephRef', + bind : { + disabled: '{!pvecephPossible}', + value: '{pveceph}' + }, + checked: true, + uncheckedValue: 0, + submitValue: false, + hidden: !me.isCreate, + boxLabel: gettext('Use Proxmox VE managed hyper-converged cephFS') + }]; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.Ceph.Model', { + extend: 'Ext.app.ViewModel', + alias: 'viewmodel.cephstorage', + + data: { + pveceph: true, + pvecephPossible: true + } +}); + +Ext.define('PVE.storage.Ceph.Controller', { + extend: 'PVE.controller.StorageEdit', + alias: 'controller.cephstorage', + + control: { + '#': { + afterrender: 'queryMonitors' + }, + 'textfield[name=username]': { + disable: 'resetField' + }, + 'displayfield[name=monhost]': { + enable: 'queryMonitors' + }, + 'textfield[name=monhost]': { + disable: 'resetField', + enable: 'resetField' + } + }, + resetField: function(field) { + field.reset(); + }, + queryMonitors: function(field, newVal, oldVal) { + // we get called with two signatures, the above one for a field + // change event and the afterrender from the view, this check only + // can be true for the field change one and omit the API request if + // pveceph got unchecked - as it's not needed there. + if (field && !newVal && oldVal) { + return; + } + var view = this.getView(); + var vm = this.getViewModel(); + if (!(view.isCreate || vm.get('pveceph'))) { + return; // only query on create or if editing a pveceph store + } + + var monhostField = this.lookupReference('monhost'); + + Proxmox.Utils.API2Request({ + url: '/api2/json/nodes/localhost/ceph/mon', + method: 'GET', + scope: this, + callback: function(options, success, response) { + var data = response.result.data; + if (response.status === 200) { + if (data.length > 0) { + var monhost = Ext.Array.pluck(data, 'name').sort().join(','); + monhostField.setValue(monhost); + monhostField.resetOriginalValue(); + if (view.isCreate) { + vm.set('pvecephPossible', true); + } + } else { + vm.set('pveceph', false); + } + } else { + vm.set('pveceph', false); + vm.set('pvecephPossible', false); + } + } + }); + } +}); + +Ext.define('PVE.storage.RBDInputPanel', { + extend: 'PVE.panel.StorageBase', + controller: 'cephstorage', + + onlineHelp: 'ceph_rados_block_devices', + + viewModel: { + type: 'cephstorage' + }, + + setValues: function(values) { + if (values.monhost) { + this.viewModel.set('pveceph', false); + this.lookupReference('pvecephRef').setValue(false); + this.lookupReference('pvecephRef').resetOriginalValue(); + } + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + me.type = 'rbd'; + + me.column1 = []; + + if (me.isCreate) { + me.column1.push({ + xtype: 'pveCephPoolSelector', + nodename: me.nodename, + name: 'pool', + bind: { + disabled: '{!pveceph}', + submitValue: '{pveceph}', + hidden: '{!pveceph}' + }, + fieldLabel: gettext('Pool'), + allowBlank: false + },{ + xtype: 'textfield', + name: 'pool', + value: 'rbd', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}' + }, + fieldLabel: gettext('Pool'), + allowBlank: false + }); + } else { + me.column1.push({ + xtype: 'displayfield', + nodename: me.nodename, + name: 'pool', + fieldLabel: gettext('Pool'), + allowBlank: false + }); + } + + me.column1.push( + { + xtype: 'textfield', + name: 'monhost', + vtype: 'HostList', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}', + hidden: '{pveceph}' + }, + value: '', + fieldLabel: 'Monitor(s)', + allowBlank: false + }, + { + xtype: 'displayfield', + reference: 'monhost', + bind: { + disabled: '{!pveceph}', + hidden: '{!pveceph}' + }, + value: '', + fieldLabel: 'Monitor(s)' + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'username', + bind: { + disabled: '{pveceph}', + submitValue: '{!pveceph}' + }, + value: 'admin', + fieldLabel: gettext('User name'), + allowBlank: true + } + ); + + me.column2 = [ + { + xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images'], + multiSelect: true, + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'krbd', + uncheckedValue: 0, + fieldLabel: 'KRBD' + } + ]; + + me.columnB = [{ + xtype: 'proxmoxcheckbox', + name: 'pveceph', + reference: 'pvecephRef', + bind : { + disabled: '{!pvecephPossible}', + value: '{pveceph}' + }, + checked: true, + uncheckedValue: 0, + submitValue: false, + hidden: !me.isCreate, + boxLabel: gettext('Use Proxmox VE managed hyper-converged ceph pool') + }]; + + me.callParent(); + } +}); +/*jslint confusion: true*/ +Ext.define('PVE.storage.ZFSInputPanel', { + extend: 'PVE.panel.StorageBase', + + viewModel: { + parent: null, + data: { + isLIO: false, + isComstar: true, + hasWriteCacheOption: true + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'field[name=iscsiprovider]': { + change: 'changeISCSIProvider' + } + }, + changeISCSIProvider: function(f, newVal, oldVal) { + var vm = this.getViewModel(); + vm.set('isLIO', newVal === 'LIO'); + vm.set('isComstar', newVal === 'comstar'); + vm.set('hasWriteCacheOption', newVal === 'comstar' || newVal === 'istgt'); + } + }, + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.content = 'images'; + } + + values.nowritecache = values.writecache ? 0 : 1; + delete values.writecache; + + return me.callParent([values]); + }, + + setValues: function diff(values) { + values.writecache = values.nowritecache ? 0 : 1; + this.callParent([values]); + }, + + initComponent : function() { + var me = this; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'portal', + value: '', + fieldLabel: gettext('Portal'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'pool', + value: '', + fieldLabel: gettext('Pool'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'blocksize', + value: '4k', + fieldLabel: gettext('Block Size'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'target', + value: '', + fieldLabel: gettext('Target'), + allowBlank: false + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'comstar_tg', + value: '', + fieldLabel: gettext('Target group'), + bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, + allowBlank: true + } + ]; + + me.column2 = [ + { + xtype: me.isCreate ? 'pveiScsiProviderSelector' : 'displayfield', + name: 'iscsiprovider', + value: 'comstar', + fieldLabel: gettext('iSCSI Provider'), + allowBlank: false + }, + { + xtype: 'proxmoxcheckbox', + name: 'sparse', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Thin provision') + }, + { + xtype: 'proxmoxcheckbox', + name: 'writecache', + checked: true, + bind: me.isCreate ? { disabled: '{!hasWriteCacheOption}' } : { hidden: '{!hasWriteCacheOption}' }, + uncheckedValue: 0, + fieldLabel: gettext('Write cache') + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'comstar_hg', + value: '', + bind: me.isCreate ? { disabled: '{!isComstar}' } : { hidden: '{!isComstar}' }, + fieldLabel: gettext('Host group'), + allowBlank: true + }, + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'lio_tpg', + value: '', + bind: me.isCreate ? { disabled: '{!isLIO}' } : { hidden: '{!isLIO}' }, + allowBlank: false, + fieldLabel: gettext('Target portal group') + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.storage.ZFSPoolSelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pveZFSPoolSelector', + valueField: 'pool', + displayField: 'pool', + queryMode: 'local', + editable: false, + listConfig: { + loadingText: gettext('Scanning...') + }, + initComponent : function() { + var me = this; + + if (!me.nodename) { + me.nodename = 'localhost'; + } + + var store = Ext.create('Ext.data.Store', { + autoLoad: {}, // true, + fields: [ 'pool', 'size', 'free' ], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodename + '/scan/zfs' + } + }); + + store.sort('pool', 'ASC'); + + Ext.apply(me, { + store: store + }); + + me.callParent(); + } +}); + +Ext.define('PVE.storage.ZFSPoolInputPanel', { + extend: 'PVE.panel.StorageBase', + + onlineHelp: 'storage_zfspool', + + initComponent : function() { + var me = this; + + me.column1 = []; + + if (me.isCreate) { + me.column1.push(Ext.create('PVE.storage.ZFSPoolSelector', { + name: 'pool', + fieldLabel: gettext('ZFS Pool'), + allowBlank: false + })); + } else { + me.column1.push(Ext.createWidget('displayfield', { + name: 'pool', + value: '', + fieldLabel: gettext('ZFS Pool'), + allowBlank: false + })); + } + + // value is an array, + // while before it was a string + /*jslint confusion: true*/ + me.column1.push( + {xtype: 'pveContentTypeSelector', + cts: ['images', 'rootdir'], + fieldLabel: gettext('Content'), + name: 'content', + value: ['images', 'rootdir'], + multiSelect: true, + allowBlank: false + }); + /*jslint confusion: false*/ + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'sparse', + checked: false, + uncheckedValue: 0, + fieldLabel: gettext('Thin provision') + }, + { + xtype: 'textfield', + name: 'blocksize', + emptyText: '8k', + fieldLabel: gettext('Block Size'), + allowBlank: true + } + ]; + + me.callParent(); + } +}); +Ext.define('PVE.ha.StatusView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAStatusView'], + + onlineHelp: 'chapter_ha_manager', + + sortPriority: { + quorum: 1, + master: 2, + lrm: 3, + service: 4 + }, + + initComponent : function() { + var me = this; + + if (!me.rstore) { + throw "no rstore given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + sortAfterUpdate: true, + sorters: [{ + sorterFn: function(rec1, rec2) { + var p1 = me.sortPriority[rec1.data.type]; + var p2 = me.sortPriority[rec2.data.type]; + return (p1 !== p2) ? ((p1 > p2) ? 1 : -1) : 0; + } + }], + filters: { + property: 'type', + value: 'service', + operator: '!=' + } + }); + + Ext.apply(me, { + store: store, + stateful: false, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Type'), + width: 80, + dataIndex: 'type' + }, + { + header: gettext('Status'), + width: 80, + flex: 1, + dataIndex: 'status' + } + ] + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + + } +}, function() { + + Ext.define('pve-ha-status', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'type', 'node', 'status', 'sid', + 'state', 'group', 'comment', + 'max_restart', 'max_relocate', 'type', + 'crm_state', 'request_state' + ], + idProperty: 'id' + }); + +}); +Ext.define('PVE.ha.Status', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveHAStatus', + + onlineHelp: 'chapter_ha_manager', + layout: { + type: 'vbox', + align: 'stretch' + }, + + initComponent: function() { + var me = this; + + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + interval: me.interval, + model: 'pve-ha-status', + storeid: 'pve-store-' + (++Ext.idSeed), + groupField: 'type', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/ha/status/current' + } + }); + + me.items = [{ + xtype: 'pveHAStatusView', + title: gettext('Status'), + rstore: me.rstore, + border: 0, + collapsible: true, + padding: '0 0 20 0' + },{ + xtype: 'pveHAResourcesView', + flex: 1, + collapsible: true, + title: gettext('Resources'), + border: 0, + rstore: me.rstore + }]; + + me.callParent(); + me.on('activate', me.rstore.startUpdate); + } +}); +Ext.define('PVE.ha.GroupSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveHAGroupSelector'], + + value: [], + autoSelect: false, + valueField: 'group', + displayField: 'group', + listConfig: { + columns: [ + { + header: gettext('Group'), + width: 100, + sortable: true, + dataIndex: 'group' + }, + { + header: gettext('Nodes'), + width: 100, + sortable: false, + dataIndex: 'nodes' + }, + { + header: gettext('Comment'), + flex: 1, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode + } + ] + }, + store: { + model: 'pve-ha-groups', + sorters: { + property: 'group', + order: 'DESC' + } + }, + + initComponent: function() { + var me = this; + me.callParent(); + me.getStore().load(); + } + +}, function() { + + Ext.define('pve-ha-groups', { + extend: 'Ext.data.Model', + fields: [ + 'group', 'type', 'digest', 'nodes', 'comment', + { + name : 'restricted', + type: 'boolean' + }, + { + name : 'nofailback', + type: 'boolean' + } + ], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/ha/groups" + }, + idProperty: 'group' + }); +}); +Ext.define('PVE.ha.VMResourceInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'ha_manager_resource_config', + vmid: undefined, + + onGetValues: function(values) { + var me = this; + + if (values.vmid) { + values.sid = values.vmid; + } + delete values.vmid; + + PVE.Utils.delete_if_default(values, 'group', '', me.isCreate); + PVE.Utils.delete_if_default(values, 'max_restart', '1', me.isCreate); + PVE.Utils.delete_if_default(values, 'max_relocate', '1', me.isCreate); + + return values; + }, + + initComponent : function() { + var me = this; + var MIN_QUORUM_VOTES = 3; + + var disabledHint = Ext.createWidget({ + xtype: 'displayfield', // won't get submitted by default + userCls: 'pve-hint', + value: 'Disabling the resource will stop the guest system. ' + + 'See the online help for details.', + hidden: true + }); + + var fewVotesHint = Ext.createWidget({ + itemId: 'fewVotesHint', + xtype: 'displayfield', + userCls: 'pve-hint', + value: 'At least three quorum votes are recommended for reliable HA.', + hidden: true + }); + + Proxmox.Utils.API2Request({ + url: '/cluster/config/nodes', + method: 'GET', + failure: function(response) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response) { + var nodes = response.result.data; + var votes = 0; + Ext.Array.forEach(nodes, function(node) { + var vote = parseInt(node.quorum_votes, 10); // parse as base 10 + votes += vote || 0; // parseInt might return NaN, which is false + }); + + if (votes < MIN_QUORUM_VOTES) { + fewVotesHint.setVisible(true); + } + } + }); + + /*jslint confusion: true */ + var vmidStore = (me.vmid) ? {} : { + model: 'PVEResources', + autoLoad: true, + sorters: 'vmid', + filters: [ + { + property: 'type', + value: /lxc|qemu/ + }, + { + property: 'hastate', + value: /unmanaged/ + } + ] + }; + + // value is a string above, but a number below + me.column1 = [ + { + xtype: me.vmid ? 'displayfield' : 'vmComboSelector', + submitValue: me.isCreate, + name: 'vmid', + fieldLabel: (me.vmid && me.guestType === 'ct') ? 'CT' : 'VM', + value: me.vmid, + store: vmidStore, + validateExists: true + }, + { + xtype: 'proxmoxintegerfield', + name: 'max_restart', + fieldLabel: gettext('Max. Restart'), + value: 1, + minValue: 0, + maxValue: 10, + allowBlank: false + }, + { + xtype: 'proxmoxintegerfield', + name: 'max_relocate', + fieldLabel: gettext('Max. Relocate'), + value: 1, + minValue: 0, + maxValue: 10, + allowBlank: false + } + ]; + /*jslint confusion: false */ + + me.column2 = [ + { + xtype: 'pveHAGroupSelector', + name: 'group', + fieldLabel: gettext('Group') + }, + { + xtype: 'proxmoxKVComboBox', + name: 'state', + value: 'started', + fieldLabel: gettext('Request State'), + comboItems: [ + ['started', 'started'], + ['stopped', 'stopped'], + ['ignored', 'ignored'], + ['disabled', 'disabled'] + ], + listeners: { + 'change': function(field, newValue) { + if (newValue === 'disabled') { + disabledHint.setVisible(true); + } + else { + if (disabledHint.isVisible()) { + disabledHint.setVisible(false); + } + } + } + } + }, + disabledHint + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + }, + fewVotesHint + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.ha.VMResourceEdit', { + extend: 'Proxmox.window.Edit', + + vmid: undefined, + guestType: undefined, + isCreate: undefined, + + initComponent : function() { + var me = this; + + if (me.isCreate === undefined) { + me.isCreate = !me.vmid; + } + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/ha/resources'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/ha/resources/' + me.vmid; + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.ha.VMResourceInputPanel', { + isCreate: me.isCreate, + vmid: me.vmid, + guestType: me.guestType + }); + + Ext.apply(me, { + subject: gettext('Resource') + ': ' + gettext('Container') + + '/' + gettext('Virtual Machine'), + isAdd: true, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + var regex = /^(\S+):(\S+)$/; + var res = regex.exec(values.sid); + + if (res[1] !== 'vm' && res[1] !== 'ct') { + throw "got unexpected resource type"; + } + + values.vmid = res[2]; + + ipanel.setValues(values); + } + }); + } + } +}); +Ext.define('PVE.ha.ResourcesView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAResourcesView'], + + onlineHelp: 'ha_manager_resources', + + stateful: true, + stateId: 'grid-ha-resources', + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + if (!me.rstore) { + throw "no store given"; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + var store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + filters: { + property: 'type', + value: 'service' + } + }); + + var reload = function() { + me.rstore.load(); + }; + + var render_error = function(dataIndex, value, metaData, record) { + var errors = record.data.errors; + if (errors) { + var msg = errors[dataIndex]; + if (msg) { + metaData.tdCls = 'proxmox-invalid-row'; + var html = '

' + Ext.htmlEncode(msg) + '

'; + metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + + html.replace(/\"/g,'"') + '"'; + } + } + return value; + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + var sid = rec.data.sid; + + var regex = /^(\S+):(\S+)$/; + var res = regex.exec(sid); + + if (res[1] !== 'vm' && res[1] !== 'ct') { + return; + } + var guestType = res[1]; + var vmid = res[2]; + + var win = Ext.create('PVE.ha.VMResourceEdit',{ + guestType: guestType, + vmid: vmid + }); + win.on('destroy', reload); + win.show(); + }; + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/ha/resources/', + getUrl: function(rec) { + var me = this; + return me.baseurl + '/' + rec.get('sid'); + }, + callback: function() { + reload(); + } + }); + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + Ext.apply(me, { + store: store, + selModel: sm, + viewConfig: { + trackOver: false + }, + tbar: [ + { + text: gettext('Add'), + disabled: !caps.nodes['Sys.Console'], + handler: function() { + var win = Ext.create('PVE.ha.VMResourceEdit',{}); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn + ], + + columns: [ + { + header: 'ID', + width: 100, + sortable: true, + dataIndex: 'sid' + }, + { + header: gettext('State'), + width: 100, + sortable: true, + dataIndex: 'state' + }, + { + header: gettext('Node'), + width: 100, + sortable: true, + dataIndex: 'node' + }, + { + header: gettext('Request State'), + width: 100, + hidden: true, + sortable: true, + renderer: function(v) { + return v || 'started'; + }, + dataIndex: 'request_state' + }, + { + header: gettext('CRM State'), + width: 100, + hidden: true, + sortable: true, + dataIndex: 'crm_state' + }, + { + header: gettext('Max. Restart'), + width: 100, + sortable: true, + renderer: (v) => v === undefined ? '1' : v, + dataIndex: 'max_restart' + }, + { + header: gettext('Max. Relocate'), + width: 100, + sortable: true, + renderer: (v) => v === undefined ? '1' : v, + dataIndex: 'max_relocate' + }, + { + header: gettext('Group'), + width: 200, + sortable: true, + renderer: function(value, metaData, record) { + return render_error('group', value, metaData, record); + }, + dataIndex: 'group' + }, + { + header: gettext('Description'), + flex: 1, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment' + } + ], + listeners: { + beforeselect: function(grid, record, index, eOpts) { + if (!caps.nodes['Sys.Console']) { + return false; + } + }, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-ha-resources', { + extend: 'Ext.data.Model', + fields: [ + 'sid', 'state', 'digest', 'errors', 'group', 'comment', + 'max_restart', 'max_relocate', 'type', 'status', 'node', + 'crm_state', 'request_state' + ], + idProperty: 'sid' + }); + +}); +Ext.define('PVE.ha.GroupInputPanel', { + extend: 'Proxmox.panel.InputPanel', + onlineHelp: 'ha_manager_groups', + + groupId: undefined, + + onGetValues: function(values) { + var me = this; + + if (me.isCreate) { + values.type = 'group'; + } + + return values; + }, + + initComponent : function() { + var me = this; + + var update_nodefield, update_node_selection; + + var sm = Ext.create('Ext.selection.CheckboxModel', { + mode: 'SIMPLE', + listeners: { + selectionchange: function(model, selected) { + update_nodefield(selected); + } + } + }); + + // use already cached data to avoid an API call + var data = PVE.data.ResourceStore.getNodes(); + + var store = Ext.create('Ext.data.Store', { + fields: [ 'node', 'mem', 'cpu', 'priority' ], + data: data, + proxy: { + type: 'memory', + reader: {type: 'json'} + }, + sorters: [ + { + property : 'node', + direction: 'ASC' + } + ] + }); + + var nodegrid = Ext.createWidget('grid', { + store: store, + border: true, + height: 300, + selModel: sm, + columns: [ + { + header: gettext('Node'), + flex: 1, + dataIndex: 'node' + }, + { + header: gettext('Memory usage') + " %", + renderer: PVE.Utils.render_mem_usage_percent, + sortable: true, + width: 150, + dataIndex: 'mem' + }, + { + header: gettext('CPU usage'), + renderer: PVE.Utils.render_cpu, + sortable: true, + width: 150, + dataIndex: 'cpu' + }, + { + header: 'Priority', + xtype: 'widgetcolumn', + dataIndex: 'priority', + sortable: true, + stopSelection: true, + widget: { + xtype: 'proxmoxintegerfield', + minValue: 0, + maxValue: 1000, + isFormField: false, + listeners: { + change: function(numberfield, value, old_value) { + var record = numberfield.getWidgetRecord(); + record.set('priority', value); + update_nodefield(sm.getSelection()); + } + } + } + } + ] + }); + + var nodefield = Ext.create('Ext.form.field.Hidden', { + name: 'nodes', + value: '', + listeners: { + change: function (nodefield, value) { + update_node_selection(value); + } + }, + isValid: function () { + var value = nodefield.getValue(); + return (value && 0 !== value.length); + } + }); + + update_node_selection = function(string) { + sm.deselectAll(true); + + string.split(',').forEach(function (e, idx, array) { + var res = e.split(':'); + + store.each(function(record) { + var node = record.get('node'); + + if (node == res[0]) { + sm.select(record, true); + record.set('priority', res[1]); + record.commit(); + } + }); + }); + nodegrid.reconfigure(store); + + }; + + update_nodefield = function(selected) { + var nodes = ''; + var first_iteration = true; + Ext.Array.each(selected, function(record) { + if (!first_iteration) { + nodes += ','; + } + first_iteration = false; + + nodes += record.data.node; + if (record.data.priority) { + nodes += ':' + record.data.priority; + } + }); + + // nodefield change listener calls us again, which results in a + // endless recursion, suspend the event temporary to avoid this + nodefield.suspendEvent('change'); + nodefield.setValue(nodes); + nodefield.resumeEvent('change'); + }; + + me.column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'group', + value: me.groupId || '', + fieldLabel: 'ID', + vtype: 'StorageId', + allowBlank: false + }, + nodefield + ]; + + me.column2 = [ + { + xtype: 'proxmoxcheckbox', + name: 'restricted', + uncheckedValue: 0, + fieldLabel: 'restricted' + }, + { + xtype: 'proxmoxcheckbox', + name: 'nofailback', + uncheckedValue: 0, + fieldLabel: 'nofailback' + } + ]; + + me.columnB = [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + }, + nodegrid + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.ha.GroupEdit', { + extend: 'Proxmox.window.Edit', + + groupId: undefined, + + initComponent : function() { + var me = this; + + me.isCreate = !me.groupId; + + if (me.isCreate) { + me.url = '/api2/extjs/cluster/ha/groups'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/cluster/ha/groups/' + me.groupId; + me.method = 'PUT'; + } + + var ipanel = Ext.create('PVE.ha.GroupInputPanel', { + isCreate: me.isCreate, + groupId: me.groupId + }); + + Ext.apply(me, { + subject: gettext('HA Group'), + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var values = response.result.data; + + ipanel.setValues(values); + } + }); + } + } +}); +Ext.define('PVE.ha.GroupsView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveHAGroupsView'], + + onlineHelp: 'ha_manager_groups', + + stateful: true, + stateId: 'grid-ha-groups', + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + var store = new Ext.data.Store({ + model: 'pve-ha-groups', + sorters: { + property: 'group', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + + var win = Ext.create('PVE.ha.GroupEdit',{ + groupId: rec.data.group + }); + win.on('destroy', reload); + win.show(); + }; + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/ha/groups/', + callback: function() { + reload(); + } + }); + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + Ext.apply(me, { + store: store, + selModel: sm, + viewConfig: { + trackOver: false + }, + tbar: [ + { + text: gettext('Create'), + disabled: !caps.nodes['Sys.Console'], + handler: function() { + var win = Ext.create('PVE.ha.GroupEdit',{}); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn + ], + columns: [ + { + header: gettext('Group'), + width: 150, + sortable: true, + dataIndex: 'group' + }, + { + header: 'restricted', + width: 100, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'restricted' + }, + { + header: 'nofailback', + width: 100, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'nofailback' + }, + { + header: gettext('Nodes'), + flex: 1, + sortable: false, + dataIndex: 'nodes' + }, + { + header: gettext('Comment'), + flex: 1, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment' + } + ], + listeners: { + activate: reload, + beforeselect: function(grid, record, index, eOpts) { + if (!caps.nodes['Sys.Console']) { + return false; + } + }, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.ha.FencingView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveFencingView'], + + onlineHelp: 'ha_manager_fencing', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-ha-fencing', + data: [] + }); + + Ext.apply(me, { + store: store, + stateful: false, + viewConfig: { + trackOver: false, + deferEmptyText: false, + emptyText: 'Use watchdog based fencing.' + }, + columns: [ + { + header: 'Node', + width: 100, + sortable: true, + dataIndex: 'node' + }, + { + header: gettext('Command'), + flex: 1, + dataIndex: 'command' + } + ] + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-ha-fencing', { + extend: 'Ext.data.Model', + fields: [ + 'node', 'command', 'digest' + ] + }); + +}); +Ext.define('PVE.dc.Summary', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcSummary', + + scrollable: true, + + bodyPadding: 5, + + layout: 'column', + + defaults: { + padding: 5, + plugins: 'responsive', + responsiveConfig: { + 'width < 1900': { + columnWidth: 1 + }, + 'width >= 1900': { + columnWidth: 0.5 + } + } + }, + + items: [ + { + itemId: 'dcHealth', + xtype: 'pveDcHealth' + }, + { + itemId: 'dcGuests', + xtype: 'pveDcGuests' + }, + { + title: gettext('Resources'), + xtype: 'panel', + minHeight: 250, + bodyPadding: 5, + layout: 'hbox', + defaults: { + xtype: 'proxmoxGauge', + flex: 1 + }, + items:[ + { + title: gettext('CPU'), + itemId: 'cpu' + }, + { + title: gettext('Memory'), + itemId: 'memory' + }, + { + title: gettext('Storage'), + itemId: 'storage' + } + ] + }, + { + itemId: 'nodeview', + xtype: 'pveDcNodeView', + height: 250 + }, + { + title: gettext('Subscriptions'), + height: 220, + items: [ + { + itemId: 'subscriptions', + xtype: 'pveHealthWidget', + userCls: 'pointer', + listeners: { + element: 'el', + click: function() { + if (this.component.userCls === 'pointer') { + window.open('https://www.proxmox.com/en/proxmox-ve/pricing', '_blank'); + } + } + } + } + ] + } + ], + + initComponent: function() { + var me = this; + + var rstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: 'pve-cluster-status', + model: 'pve-dc-nodes', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/status" + } + }); + + var gridstore = Ext.create('Proxmox.data.DiffStore', { + rstore: rstore, + filters: { + property: 'type', + value: 'node' + }, + sorters: { + property: 'id', + direction: 'ASC' + } + }); + + me.callParent(); + + me.getComponent('nodeview').setStore(gridstore); + + var gueststatus = me.getComponent('dcGuests'); + + var cpustat = me.down('#cpu'); + var memorystat = me.down('#memory'); + var storagestat = me.down('#storage'); + var sp = Ext.state.Manager.getProvider(); + + me.mon(PVE.data.ResourceStore, 'load', function(curstore, results) { + me.suspendLayout = true; + + var cpu = 0; + var maxcpu = 0; + + var nodes = 0; + + var memory = 0; + var maxmem = 0; + + var countedStorages = {}; + var used = 0; + var total = 0; + var usableStorages = {}; + var storages = sp.get('dash-storages') || ''; + storages.split(',').forEach(function(storage){ + if (storage !== '') { + usableStorages[storage] = true; + } + }); + + var qemu = { + running: 0, + paused: 0, + stopped: 0, + template: 0 + }; + var lxc = { + running: 0, + paused: 0, + stopped: 0, + template: 0 + }; + var error = 0; + + var i; + + for (i = 0; i < results.length; i++) { + var item = results[i]; + switch(item.data.type) { + case 'node': + cpu += (item.data.cpu * item.data.maxcpu); + maxcpu += item.data.maxcpu || 0; + memory += item.data.mem || 0; + maxmem += item.data.maxmem || 0; + nodes++; + + // update grid also + var griditem = gridstore.getById(item.data.id); + if (griditem) { + griditem.set('cpuusage', item.data.cpu); + var max = item.data.maxmem || 1; + var val = item.data.mem || 0; + griditem.set('memoryusage', val/max); + griditem.set('uptime', item.data.uptime); + griditem.commit(); //else it marks the fields as dirty + } + break; + case 'storage': + if (!Ext.Object.isEmpty(usableStorages)) { + if (usableStorages[item.data.id] === true) { + used += item.data.disk; + total += item.data.maxdisk; + } + break; + } + if (!countedStorages[item.data.storage] || + (item.data.storage === 'local' && + !countedStorages[item.data.id])) { + used += item.data.disk; + total += item.data.maxdisk; + + countedStorages[item.data.storage === 'local'?item.data.id:item.data.storage] = true; + } + break; + case 'qemu': + qemu[item.data.template ? 'template' : item.data.status]++; + if (item.data.hastate === 'error') { + error++; + } + break; + case 'lxc': + lxc[item.data.template ? 'template' : item.data.status]++; + if (item.data.hastate === 'error') { + error++; + } + break; + default: break; + } + } + + var text = Ext.String.format(gettext('of {0} CPU(s)'), maxcpu); + cpustat.updateValue((cpu/maxcpu), text); + + text = Ext.String.format(gettext('{0} of {1}'), PVE.Utils.render_size(memory), PVE.Utils.render_size(maxmem)); + memorystat.updateValue((memory/maxmem), text); + + text = Ext.String.format(gettext('{0} of {1}'), PVE.Utils.render_size(used), PVE.Utils.render_size(total)); + storagestat.updateValue((used/total), text); + + gueststatus.updateValues(qemu,lxc,error); + + me.suspendLayout = false; + me.updateLayout(true); + }); + + var dcHealth = me.getComponent('dcHealth'); + me.mon(rstore, 'load', dcHealth.updateStatus, dcHealth); + + var subs = me.down('#subscriptions'); + me.mon(rstore, 'load', function(store, records, success) { + var i; + var level; + var mixed = false; + for (i = 0; i < records.length; i++) { + if (records[i].get('type') !== 'node') { + continue; + } + var node = records[i]; + if (node.get('status') === 'offline') { + continue; + } + + var curlevel = node.get('level'); + + if (curlevel === '') { // no subscription trumps all, set and break + level = ''; + break; + } + + if (level === undefined) { // save level + level = curlevel; + } else if (level !== curlevel) { // detect different levels + mixed = true; + } + } + + var data = { + title: Proxmox.Utils.unknownText, + text: Proxmox.Utils.unknownText, + iconCls: PVE.Utils.get_health_icon(undefined, true) + }; + if (level === '') { + data = { + title: gettext('No Subscription'), + iconCls: PVE.Utils.get_health_icon('critical', true), + text: gettext('You have at least one node without subscription.') + }; + subs.setUserCls('pointer'); + } else if (mixed) { + data = { + title: gettext('Mixed Subscriptions'), + iconCls: PVE.Utils.get_health_icon('warning', true), + text: gettext('Warning: Your subscription levels are not the same.') + }; + subs.setUserCls('pointer'); + } else if (level) { + data = { + title: PVE.Utils.render_support_level(level), + iconCls: PVE.Utils.get_health_icon('good', true), + text: gettext('Your subscription status is valid.') + }; + subs.setUserCls(''); + } + + subs.setData(data); + }); + + me.on('destroy', function(){ + rstore.stopUpdate(); + }); + + rstore.startUpdate(); + } + +}); +Ext.define('PVE.window.ReplicaEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveReplicaEdit', + + subject: gettext('Replication Job'), + + + url: '/cluster/replication', + method: 'POST', + + initComponent: function() { + var me = this; + + var vmid = me.pveSelNode.data.vmid; + var nodename = me.pveSelNode.data.node; + + var items = []; + + items.push({ + xtype: (me.isCreate && !vmid)?'pveGuestIDSelector':'displayfield', + name: 'guest', + fieldLabel: 'CT/VM ID', + value: vmid || '' + }); + + items.push( + { + xtype: me.isCreate ? 'pveNodeSelector':'displayfield', + name: 'target', + disallowedNodes: [nodename], + allowBlank: false, + onlineValidator: true, + fieldLabel: gettext("Target") + }, + { + xtype: 'pveCalendarEvent', + fieldLabel: gettext('Schedule'), + emptyText: '*/15 - ' + Ext.String.format(gettext('Every {0} minutes'), 15), + name: 'schedule' + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Rate limit') + ' (MB/s)', + step: 1, + minValue: 1, + emptyText: gettext('unlimited'), + name: 'rate' + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment' + }, + { + xtype: 'proxmoxcheckbox', + name: 'enabled', + defaultValue: 'on', + checked: true, + fieldLabel: gettext('Enabled') + } + ); + + me.items = [ + { + xtype: 'inputpanel', + itemId: 'ipanel', + onlineHelp: 'pvesr_schedule_time_format', + + onGetValues: function(values) { + var me = this.up('window'); + + values.disable = values.enabled ? 0 : 1; + delete values.enabled; + + PVE.Utils.delete_if_default(values, 'rate', '', me.isCreate); + PVE.Utils.delete_if_default(values, 'disable', 0, me.isCreate); + PVE.Utils.delete_if_default(values, 'schedule', '*/15', me.isCreate); + PVE.Utils.delete_if_default(values, 'comment', '', me.isCreate); + + if (me.isCreate) { + values.type = 'local'; + var vm = vmid || values.guest; + var id = -1; + if (me.highestids[vm] !== undefined) { + id = me.highestids[vm]; + } + id++; + values.id = vm + '-' + id.toString(); + delete values.guest; + } + return values; + }, + items: items + } + ]; + + me.callParent(); + + if (me.isCreate) { + me.load({ + success: function(response) { + var jobs = response.result.data; + var highestids = {}; + Ext.Array.forEach(jobs, function(job) { + var match = /^([0-9]+)\-([0-9]+)$/.exec(job.id); + if (match) { + var vmid = parseInt(match[1],10); + var id = parseInt(match[2],10); + if (highestids[vmid] < id || + highestids[vmid] === undefined) { + highestids[vmid] = id; + } + } + }); + + me.highestids = highestids; + } + }); + + } else { + me.load({ + success: function(response, options) { + response.result.data.enabled = !response.result.data.disable; + me.setValues(response.result.data); + me.digest = response.result.data.digest; + } + }); + } + } +}); + +/*jslint confusion: true */ +/* callback is a function and string */ +Ext.define('PVE.grid.ReplicaView', { + extend: 'Ext.grid.Panel', + xtype: 'pveReplicaView', + + onlineHelp: 'chapter_pvesr', + + stateful: true, + stateId: 'grid-pve-replication-status', + + controller: { + xclass: 'Ext.app.ViewController', + + addJob: function(button,event,rec) { + var me = this.getView(); + var controller = this; + var win = Ext.create('PVE.window.ReplicaEdit', { + isCreate: true, + method: 'POST', + pveSelNode: me.pveSelNode + }); + win.on('destroy', function() { controller.reload(); }); + win.show(); + }, + + editJob: function(button,event,rec) { + var me = this.getView(); + var controller = this; + var data = rec.data; + var win = Ext.create('PVE.window.ReplicaEdit', { + url: '/cluster/replication/' + data.id, + method: 'PUT', + pveSelNode: me.pveSelNode + }); + win.on('destroy', function() { controller.reload(); }); + win.show(); + }, + + scheduleJobNow: function(button,event,rec) { + var me = this.getView(); + var controller = this; + + Proxmox.Utils.API2Request({ + url: "/api2/extjs/nodes/" + me.nodename + "/replication/" + rec.data.id + "/schedule_now", + method: 'POST', + waitMsgTarget: me, + callback: function() { controller.reload(); }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + showLog: function(button, event, rec) { + var me = this.getView(); + var controller = this; + var logView = Ext.create('Proxmox.panel.LogView', { + border: false, + url: "/api2/extjs/nodes/" + me.nodename + "/replication/" + rec.data.id + "/log" + }); + var win = Ext.create('Ext.window.Window', { + items: [ logView ], + layout: 'fit', + width: 800, + height: 400, + modal: true, + title: gettext("Replication Log") + }); + var task = { + run: function() { + logView.requestUpdate(); + }, + interval: 1000 + }; + Ext.TaskManager.start(task); + win.on('destroy', function() { + Ext.TaskManager.stop(task); + controller.reload(); + }); + win.show(); + }, + + reload: function() { + var me = this.getView(); + me.rstore.load(); + }, + + dblClick: function(grid, record, item) { + var me = this; + me.editJob(undefined, undefined, record); + }, + + // check for cluster + // currently replication is for cluster only, so we disable the whole + // component + checkPrerequisites: function() { + var me = this.getView(); + if (PVE.data.ResourceStore.getNodes().length < 2) { + me.mask(gettext("Replication needs at least two nodes"), ['pve-static-mask']); + } + }, + + control: { + '#': { + itemdblclick: 'dblClick', + afterlayout: 'checkPrerequisites' + } + } + }, + + tbar: [ + { + text: gettext('Add'), + itemId: 'addButton', + handler: 'addJob' + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + itemId: 'editButton', + handler: 'editJob', + disabled: true + }, + { + xtype: 'proxmoxStdRemoveButton', + itemId: 'removeButton', + baseurl: '/api2/extjs/cluster/replication/', + dangerous: true, + callback: 'reload' + }, + { + xtype: 'proxmoxButton', + text: gettext('Log'), + itemId: 'logButton', + handler: 'showLog', + disabled: true + }, + { + xtype: 'proxmoxButton', + text: gettext('Schedule now'), + itemId: 'scheduleNowButton', + handler: 'scheduleJobNow', + disabled: true + } + ], + + initComponent: function() { + var me = this; + var mode = ''; + var url = '/cluster/replication'; + + me.nodename = me.pveSelNode.data.node; + me.vmid = me.pveSelNode.data.vmid; + + me.columns = [ + { + text: gettext('Enabled'), + dataIndex: 'enabled', + xtype: 'checkcolumn', + sortable: true, + disabled: true + }, + { + text: 'ID', + dataIndex: 'id', + width: 60, + hidden: true + }, + { + text: gettext('Guest'), + dataIndex: 'guest', + width: 75 + }, + { + text: gettext('Job'), + dataIndex: 'jobnum', + width: 60 + }, + { + text: gettext('Target'), + dataIndex: 'target' + } + ]; + + if (!me.nodename) { + mode = 'dc'; + me.stateId = 'grid-pve-replication-dc'; + } else if (!me.vmid) { + mode = 'node'; + url = '/nodes/' + me.nodename + '/replication'; + } else { + mode = 'vm'; + url = '/nodes/' + me.nodename + '/replication' + '?guest=' + me.vmid; + } + + if (mode !== 'dc') { + me.columns.push( + { + text: gettext('Status'), + dataIndex: 'state', + minWidth: 160, + flex: 1, + renderer: function(value, metadata, record) { + + if (record.data.pid) { + metadata.tdCls = 'x-grid-row-loading'; + return ''; + } + + var icons = []; + var states = []; + + if (record.data.remove_job) { + icons.push(''); + states.push(gettext("Removal Scheduled")); + } + + if (record.data.error) { + icons.push(''); + states.push(record.data.error); + } + + if (icons.length == 0) { + icons.push(''); + states.push(gettext('OK')); + } + + return icons.join(',') + ' ' + states.join(','); + } + }, + { + text: gettext('Last Sync'), + dataIndex: 'last_sync', + width: 150, + renderer: function(value, metadata, record) { + if (!value) { + return '-'; + } + + if (record.data.pid) { + return gettext('syncing'); + } + + return Proxmox.Utils.render_timestamp(value); + } + }, + { + text: gettext('Duration'), + dataIndex: 'duration', + width: 60, + renderer: PVE.Utils.render_duration + }, + { + text: gettext('Next Sync'), + dataIndex: 'next_sync', + width: 150, + renderer: function(value) { + if (!value) { + return '-'; + } + + var now = new Date(); + var next = new Date(value*1000); + + if (next < now) { + return gettext('pending'); + } + + return Proxmox.Utils.render_timestamp(value); + } + } + ); + } + + me.columns.push( + { + text: gettext('Schedule'), + width: 75, + dataIndex: 'schedule' + }, + { + text: gettext('Rate limit'), + dataIndex: 'rate', + renderer: function(value) { + if (!value) { + return gettext('unlimited'); + } + + return value.toString() + ' MB/s'; + }, + hidden: true + }, + { + text: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.htmlEncode + } + ); + + me.rstore = Ext.create('Proxmox.data.UpdateStore', { + storeid: 'pve-replica-' + me.nodename + me.vmid, + model: (mode === 'dc')? 'pve-replication' : 'pve-replication-state', + interval: 3000, + proxy: { + type: 'proxmox', + url: "/api2/json" + url + } + }); + + me.store = Ext.create('Proxmox.data.DiffStore', { + rstore: me.rstore, + sorters: [ + { + property: 'guest' + }, + { + property: 'jobnum' + } + ] + }); + + me.callParent(); + + // we cannot access the log and scheduleNow button + // in the datacenter, because + // we do not know where/if the jobs runs + if (mode === 'dc') { + me.down('#logButton').setHidden(true); + me.down('#scheduleNowButton').setHidden(true); + } + + // if we set the warning mask, we do not want to load + // or set the mask on store errors + if (PVE.data.ResourceStore.getNodes().length < 2) { + return; + } + + Proxmox.Utils.monStoreErrors(me, me.rstore); + + me.on('destroy', me.rstore.stopUpdate); + me.rstore.startUpdate(); + } +}, function() { + + Ext.define('pve-replication', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'target', 'comment', 'rate', 'type', + { name: 'guest', type: 'integer' }, + { name: 'jobnum', type: 'integer' }, + { name: 'schedule', defaultValue: '*/15' }, + { name: 'disable', defaultValue: '' }, + { name: 'enabled', calculate: function(data) { return !data.disable; } } + ] + }); + + Ext.define('pve-replication-state', { + extend: 'pve-replication', + fields: [ + 'last_sync', 'next_sync', 'error', 'duration', 'state', + 'fail_count', 'remove_job', 'pid' + ] + }); + +}); +Ext.define('PVE.dc.Health', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcHealth', + + title: gettext('Health'), + + bodyPadding: 10, + height: 220, + layout: { + type: 'hbox', + align: 'stretch' + }, + + defaults: { + flex: 1, + xtype: 'box', + style: { + 'text-align':'center' + } + }, + + nodeList: [], + nodeIndex: 0, + + updateStatus: function(store, records, success) { + var me = this; + if (!success) { + return; + } + + var cluster = { + iconCls: PVE.Utils.get_health_icon('good', true), + text: gettext("Standalone node - no cluster defined") + }; + + var nodes = { + online: 0, + offline: 0 + }; + + // by default we have one node + var numNodes = 1; + var i; + + for (i = 0; i < records.length; i++) { + var item = records[i]; + if (item.data.type === 'node') { + nodes[item.data.online === 1 ? 'online':'offline']++; + } else if(item.data.type === 'cluster') { + cluster.text = gettext("Cluster") + ": "; + cluster.text += item.data.name + ", "; + cluster.text += gettext("Quorate") + ": "; + cluster.text += Proxmox.Utils.format_boolean(item.data.quorate); + if (item.data.quorate != 1) { + cluster.iconCls = PVE.Utils.get_health_icon('critical', true); + } + + numNodes = item.data.nodes; + } + } + + if (numNodes !== (nodes.online + nodes.offline)) { + nodes.offline = numNodes - nodes.online; + } + + me.getComponent('clusterstatus').updateHealth(cluster); + me.getComponent('nodestatus').update(nodes); + }, + + updateCeph: function(store, records, success) { + var me = this; + var cephstatus = me.getComponent('ceph'); + if (!success || records.length < 1) { + + // if ceph status is already visible + // don't stop to update + if (cephstatus.isVisible()) { + return; + } + + // try all nodes until we either get a successful api call, + // or we tried all nodes + if (++me.nodeIndex >= me.nodeList.length) { + me.cephstore.stopUpdate(); + } else { + store.getProxy().setUrl('/api2/json/nodes/' + me.nodeList[me.nodeIndex].node + '/ceph/status'); + } + + return; + } + + var state = PVE.Utils.render_ceph_health(records[0].data.health || {}); + cephstatus.updateHealth(state); + cephstatus.setVisible(true); + }, + + listeners: { + destroy: function() { + var me = this; + me.cephstore.stopUpdate(); + } + }, + + items: [ + { + itemId: 'clusterstatus', + xtype: 'pveHealthWidget', + title: gettext('Status') + }, + { + itemId: 'nodestatus', + data: { + online: 0, + offline: 0 + }, + tpl: [ + '

' + gettext('Nodes') + '


', + '
', + '
', + ' ', + gettext('Online'), + '
', + '
{online}
', + '

', + '
', + ' ', + gettext('Offline'), + '
', + '
{offline}
', + '
' + ] + }, + { + itemId: 'ceph', + width: 250, + columnWidth: undefined, + userCls: 'pointer', + title: 'Ceph', + xtype: 'pveHealthWidget', + hidden: true, + listeners: { + element: 'el', + click: function() { + var sp = Ext.state.Manager.getProvider(); + sp.set('dctab', {value:'ceph'}, true); + } + } + } + ], + + initComponent: function() { + var me = this; + + me.nodeList = PVE.data.ResourceStore.getNodes(); + me.nodeIndex = 0; + me.cephstore = Ext.create('Proxmox.data.UpdateStore', { + interval: 3000, + storeid: 'pve-cluster-ceph', + proxy: { + type: 'proxmox', + url: '/api2/json/nodes/' + me.nodeList[me.nodeIndex].node + '/ceph/status' + } + }); + me.callParent(); + me.mon(me.cephstore, 'load', me.updateCeph, me); + me.cephstore.startUpdate(); + } +}); +Ext.define('PVE.dc.Guests', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcGuests', + + + title: gettext('Guests'), + height: 220, + layout: { + type: 'table', + columns: 2, + tableAttrs: { + style: { + width: '100%' + } + } + }, + bodyPadding: '0 20 20 20', + + defaults: { + xtype: 'box', + padding: '0 50 0 50', + style: { + 'text-align':'center', + 'line-height':'1.2' + } + }, + items: [{ + itemId: 'qemu', + data: { + running: 0, + paused: 0, + stopped: 0, + template: 0 + }, + tpl: [ + '

' + gettext("Virtual Machines") + '

', + '
', + ' ', + gettext('Running'), + '
', + '
{running}
' + '
', + '', + '
', + ' ', + gettext('Paused'), + '
', + '
{paused}
' + '
', + '
', + '
', + ' ', + gettext('Stopped'), + '
', + '
{stopped}
' + '
', + '', + '
', + ' ', + gettext('Templates'), + '
', + '
{template}
', + '
' + ] + },{ + itemId: 'lxc', + data: { + running: 0, + paused: 0, + stopped: 0, + template: 0 + }, + tpl: [ + '

' + gettext("LXC Container") + '

', + '
', + ' ', + gettext('Running'), + '
', + '
{running}
' + '
', + '', + '
', + ' ', + gettext('Paused'), + '
', + '
{paused}
' + '
', + '
', + '
', + ' ', + gettext('Stopped'), + '
', + '
{stopped}
' + '
', + '', + '
', + ' ', + gettext('Templates'), + '
', + '
{template}
', + '
' + ] + },{ + itemId: 'error', + colspan: 2, + data: { + num: 0 + }, + columnWidth: 1, + padding: '10 250 0 250', + tpl: [ + '', + '
', + ' ', + gettext('Error'), + '
', + '
{num}
', + '
' + ] + }], + + updateValues: function(qemu, lxc, error) { + var me = this; + me.getComponent('qemu').update(qemu); + me.getComponent('lxc').update(lxc); + me.getComponent('error').update({num: error}); + } +}); + /*jslint confusion: true*/ +Ext.define('PVE.dc.OptionView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.pveDcOptionView'], + + onlineHelp: 'datacenter_configuration_file', + + monStoreErrors: true, + + add_inputpanel_row: function(name, text, opts) { + var me = this; + + opts = opts || {}; + me.rows = me.rows || {}; + + var canEdit = (opts.caps === undefined || opts.caps); + me.rows[name] = { + required: true, + defaultValue: opts.defaultValue, + header: text, + renderer: opts.renderer, + editor: canEdit ? { + xtype: 'proxmoxWindowEdit', + width: 350, + subject: text, + fieldDefaults: { + labelWidth: opts.labelWidth || 100 + }, + setValues: function(values) { + // FIXME: run through parsePropertyString if not an object? + var edit_value = values[name]; + Ext.Array.each(this.query('inputpanel'), function(panel) { + panel.setValues(edit_value); + }); + }, + url: opts.url, + items: [{ + xtype: 'inputpanel', + onGetValues: function(values) { + if (values === undefined || Object.keys(values).length === 0) { + return { 'delete': name }; + } + var ret_val = {}; + ret_val[name] = PVE.Parser.printPropertyString(values); + return ret_val; + }, + items: opts.items + }] + } : undefined + }; + }, + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + me.add_combobox_row('keyboard', gettext('Keyboard Layout'), { + renderer: PVE.Utils.render_kvm_language, + comboItems: PVE.Utils.kvm_keymap_array(), + defaultValue: '__default__', + deleteEmpty: true + }); + me.add_text_row('http_proxy', gettext('HTTP proxy'), { + defaultValue: Proxmox.Utils.noneText, + vtype: 'HttpProxy', + deleteEmpty: true + }); + me.add_combobox_row('console', gettext('Console Viewer'), { + renderer: PVE.Utils.render_console_viewer, + comboItems: PVE.Utils.console_viewer_array(), + defaultValue: '__default__', + deleteEmpty: true + }); + me.add_text_row('email_from', gettext('Email from address'), { + deleteEmpty: true, + vtype: 'proxmoxMail', + defaultValue: 'root@$hostname' + }); + me.add_text_row('mac_prefix', gettext('MAC address prefix'), { + deleteEmpty: true, + vtype: 'MacPrefix', + defaultValue: Proxmox.Utils.noneText + }); + me.add_inputpanel_row('migration', gettext('Migration Settings'), { + renderer: PVE.Utils.render_dc_ha_opts, + caps: caps.vms['Sys.Modify'], + labelWidth: 120, + url: "/api2/extjs/cluster/options", + defaultKey: 'type', + items: [{ + xtype: 'displayfield', + name: 'type', + fieldLabel: gettext('Type'), + value: 'secure', + submitValue: true, + }, { + xtype: 'proxmoxNetworkSelector', + name: 'network', + fieldLabel: gettext('Network'), + value: null, + emptyText: Proxmox.Utils.defaultText, + autoSelect: false, + skipEmptyText: true + }] + }); + me.add_inputpanel_row('ha', gettext('HA Settings'), { + renderer: PVE.Utils.render_dc_ha_opts, + caps: caps.vms['Sys.Modify'], + labelWidth: 120, + url: "/api2/extjs/cluster/options", + items: [{ + xtype: 'proxmoxKVComboBox', + name: 'shutdown_policy', + fieldLabel: gettext('Shutdown Policy'), + deleteEmpty: false, + value: '__default__', + comboItems: [ + ['__default__', Proxmox.Utils.defaultText + ' (conditional)' ], + ['freeze', 'freeze'], + ['failover', 'failover'], + ['conditional', 'conditional'] + ], + defaultValue: '__default__' + }] + }); + + // TODO: bwlimits, u2f? + + me.selModel = Ext.create('Ext.selection.RowModel', {}); + + Ext.apply(me, { + tbar: [{ + text: gettext('Edit'), + xtype: 'proxmoxButton', + disabled: true, + handler: function() { me.run_editor(); }, + selModel: me.selModel + }], + url: "/api2/json/cluster/options", + editorConfig: { + url: "/api2/extjs/cluster/options" + }, + interval: 5000, + cwidth1: 200, + listeners: { + itemdblclick: me.run_editor + } + }); + + me.callParent(); + + // set the new value for the default console + me.mon(me.rstore, 'load', function(store, records, success) { + if (!success) { + return; + } + + var rec = store.getById('console'); + PVE.VersionInfo.console = rec.data.value; + if (rec.data.value === '__default__') { + delete PVE.VersionInfo.console; + } + }); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + me.on('deactivate', me.rstore.stopUpdate); + } +}); +Ext.define('PVE.dc.StorageView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveStorageView'], + + onlineHelp: 'chapter_storage', + + stateful: true, + stateId: 'grid-dc-storage', + + createStorageEditWindow: function(type, sid) { + var schema = PVE.Utils.storageSchema[type]; + if (!schema || !schema.ipanel) { + throw "no editor registered for storage type: " + type; + } + + Ext.create('PVE.storage.BaseEdit', { + paneltype: 'PVE.storage.' + schema.ipanel, + type: type, + storageId: sid, + autoShow: true, + listeners: { + destroy: this.reloadStore + } + }); + }, + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-storage', + proxy: { + type: 'proxmox', + url: "/api2/json/storage" + }, + sorters: { + property: 'storage', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var type = rec.data.type, + sid = rec.data.storage; + + me.createStorageEditWindow(type, sid); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/storage/', + callback: reload + }); + + // else we cannot dynamically generate the add menu handlers + var addHandleGenerator = function(type) { + return function() { me.createStorageEditWindow(type); }; + }; + var addMenuItems = [], type; + /*jslint forin: true */ + for (type in PVE.Utils.storageSchema) { + var storage = PVE.Utils.storageSchema[type]; + if (storage.hideAdd) { + continue; + } + addMenuItems.push({ + text: PVE.Utils.format_storage_type(type), + iconCls: 'fa fa-fw fa-' + storage.faIcon, + handler: addHandleGenerator(type) + }); + } + + Ext.apply(me, { + store: store, + reloadStore: reload, + selModel: sm, + viewConfig: { + trackOver: false + }, + tbar: [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: addMenuItems + }) + }, + remove_btn, + edit_btn + ], + columns: [ + { + header: 'ID', + flex: 2, + sortable: true, + dataIndex: 'storage' + }, + { + header: gettext('Type'), + flex: 1, + sortable: true, + dataIndex: 'type', + renderer: PVE.Utils.format_storage_type + }, + { + header: gettext('Content'), + flex: 3, + sortable: true, + dataIndex: 'content', + renderer: PVE.Utils.format_content_types + }, + { + header: gettext('Path') + '/' + gettext('Target'), + flex: 2, + sortable: true, + dataIndex: 'path', + renderer: function(value, metaData, record) { + if (record.data.target) { + return record.data.target; + } + return value; + } + }, + { + header: gettext('Shared'), + flex: 1, + sortable: true, + dataIndex: 'shared', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('Enabled'), + flex: 1, + sortable: true, + dataIndex: 'disable', + renderer: Proxmox.Utils.format_neg_boolean + }, + { + header: gettext('Bandwidth Limit'), + flex: 2, + sortable: true, + dataIndex: 'bwlimit' + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-storage', { + extend: 'Ext.data.Model', + fields: [ + 'path', 'type', 'content', 'server', 'portal', 'target', 'export', 'storage', + { name: 'shared', type: 'boolean'}, + { name: 'disable', type: 'boolean'} + ], + idProperty: 'storage' + }); + +}); +/*global u2f,QRCode,Uint8Array*/ +/*jslint confusion: true*/ +Ext.define('PVE.window.TFAEdit', { + extend: 'Ext.window.Window', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'pveum_tfa_auth', // fake to ensure this gets a link target + + modal: true, + resizable: false, + title: gettext('Two Factor Authentication'), + subject: 'TFA', + url: '/api2/extjs/access/tfa', + width: 512, + + layout: { + type: 'vbox', + align: 'stretch' + }, + + updateQrCode: function() { + var me = this; + var values = me.lookup('totp_form').getValues(); + var algorithm = values.algorithm; + if (!algorithm) { + algorithm = 'SHA1'; + } + + me.qrcode.makeCode( + 'otpauth://totp/' + encodeURIComponent(me.userid) + + '?secret=' + values.secret + + '&period=' + values.step + + '&digits=' + values.digits + + '&algorithm=' + algorithm + + '&issuer=' + encodeURIComponent(values.issuer) + ); + + me.lookup('challenge').setVisible(true); + me.down('#qrbox').setVisible(true); + }, + + showError: function(error) { + Ext.Msg.alert( + gettext('Error'), + PVE.Utils.render_u2f_error(error) + ); + }, + + doU2FChallenge: function(response) { + var me = this; + + var data = response.result.data; + me.lookup('password').setDisabled(true); + var msg = Ext.Msg.show({ + title: 'U2F: '+gettext('Setup'), + message: gettext('Please press the button on your U2F Device'), + buttons: [] + }); + Ext.Function.defer(function() { + u2f.register(data.appId, [data], [], function(data) { + msg.close(); + if (data.errorCode) { + me.showError(data.errorCode); + } else { + me.respondToU2FChallenge(data); + } + }); + }, 500, me); + }, + + respondToU2FChallenge: function(data) { + var me = this; + var params = { + userid: me.userid, + action: 'confirm', + response: JSON.stringify(data) + }; + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + success: function() { + me.close(); + Ext.Msg.show({ + title: gettext('Success'), + message: gettext('U2F Device successfully connected.'), + buttons: Ext.Msg.OK + }); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + viewModel: { + data: { + in_totp_tab: true, + tfa_required: false, + tfa_type: null, // dependencies of formulas should not be undefined + valid: false, + u2f_available: true + }, + formulas: { + canDeleteTFA: function(get) { + return (get('tfa_type') !== null && !get('tfa_required')); + }, + canSetupTOTP: function(get) { + var tfa = get('tfa_type'); + return (tfa === null || tfa === 'totp' || tfa === 1); + }, + canSetupU2F: function(get) { + var tfa = get('tfa_type'); + return (get('u2f_available') && (tfa === null || tfa === 'u2f' || tfa === 1)); + } + } + }, + + afterLoading: function(realm_tfa_type, user_tfa_type) { + var me = this; + var viewmodel = me.getViewModel(); + if (user_tfa_type === 'oath') { + user_tfa_type = 'totp'; + } + viewmodel.set('tfa_type', user_tfa_type || null); + if (!realm_tfa_type) { + // There's no TFA enforced by the realm, everything works. + viewmodel.set('u2f_available', true); + viewmodel.set('tfa_required', false); + } else if (realm_tfa_type === 'oath') { + // The realm explicitly requires TOTP + if (user_tfa_type !== 'totp' && user_tfa_type !== null) { + // user had a different tfa method, so + // we have to change back to the totp tab and + // generate a secret + viewmodel.set('tfa_type', null); + me.lookup('tfatabs').setActiveTab(me.lookup('totp_panel')); + me.getController().randomizeSecret(); + } + viewmodel.set('tfa_required', true); + viewmodel.set('u2f_available', false); + } else { + // The realm enforces some other TFA type (yubico) + me.close(); + Ext.Msg.alert( + gettext('Error'), + Ext.String.format( + gettext("Custom 2nd factor configuration is not supported on realms with '{0}' TFA."), + realm_tfa_type + ) + ); + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'field[qrupdate=true]': { + change: function() { + var me = this.getView(); + me.updateQrCode(); + } + }, + 'field': { + validitychange: function(field, valid) { + var me = this; + var viewModel = me.getViewModel(); + var form = me.lookup('totp_form'); + var challenge = me.lookup('challenge'); + var password = me.lookup('password'); + viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid()); + } + }, + '#': { + show: function() { + var me = this.getView(); + var viewmodel = this.getViewModel(); + + var loadMaskContainer = me.down('#tfatabs'); + Proxmox.Utils.API2Request({ + url: '/access/users/' + encodeURIComponent(me.userid) + '/tfa', + waitMsgTarget: loadMaskContainer, + method: 'GET', + success: function(response, opts) { + var data = response.result.data; + me.afterLoading(data.realm, data.user); + }, + failure: function(response, opts) { + Proxmox.Utils.setErrorMask(loadMaskContainer, response.htmlStatus); + } + }); + + me.qrdiv = document.createElement('center'); + me.qrcode = new QRCode(me.qrdiv, { + width: 256, + height: 256, + correctLevel: QRCode.CorrectLevel.M + }); + me.down('#qrbox').getEl().appendChild(me.qrdiv); + + viewmodel.set('tfa_type', me.tfa_type || null); + if (!me.tfa_type) { + this.randomizeSecret(); + } else { + me.down('#qrbox').setVisible(false); + me.lookup('challenge').setVisible(false); + if (me.tfa_type === 'u2f') { + var u2f_panel = me.lookup('u2f_panel'); + me.lookup('tfatabs').setActiveTab(u2f_panel); + } + } + + if (Proxmox.UserName === 'root@pam') { + me.lookup('password').setVisible(false); + me.lookup('password').setDisabled(true); + } + } + }, + '#tfatabs': { + tabchange: function(panel, newcard) { + var viewmodel = this.getViewModel(); + viewmodel.set('in_totp_tab', newcard.itemId === 'totp-panel'); + } + } + }, + + applySettings: function() { + var me = this; + var values = me.lookup('totp_form').getValues(); + var params = { + userid: me.getView().userid, + action: 'new', + key: values.secret, + config: PVE.Parser.printPropertyString({ + type: 'oath', + digits: values.digits, + step: values.step + }), + // this is used to verify that the client generates the correct codes: + response: me.lookup('challenge').value + }; + + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + waitMsgTarget: me.getView(), + success: function(response, opts) { + me.getView().close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + deleteTFA: function() { + var me = this; + var values = me.lookup('totp_form').getValues(); + var params = { + userid: me.getView().userid, + action: 'delete' + }; + + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + waitMsgTarget: me.getView(), + success: function(response, opts) { + me.getView().close(); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }, + + randomizeSecret: function() { + var me = this; + var rnd = new Uint8Array(16); + window.crypto.getRandomValues(rnd); + var data = ''; + rnd.forEach(function(b) { + // secret must be base32, so just use the first 5 bits + b = b & 0x1f; + if (b < 26) { + // A..Z + data += String.fromCharCode(b + 0x41); + } else { + // 2..7 + data += String.fromCharCode(b-26 + 0x32); + } + }); + me.lookup('tfa_secret').setValue(data); + }, + + startU2FRegistration: function() { + var me = this; + + var params = { + userid: me.getView().userid, + action: 'new' + }; + + if (Proxmox.UserName !== 'root@pam') { + params.password = me.lookup('password').value; + } + + Proxmox.Utils.API2Request({ + url: '/api2/extjs/access/tfa', + params: params, + method: 'PUT', + waitMsgTarget: me.getView(), + success: function(response) { + me.getView().doU2FChallenge(response); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }, + + items: [ + { + xtype: 'tabpanel', + itemId: 'tfatabs', + reference: 'tfatabs', + border: false, + items: [ + { + xtype: 'panel', + title: 'TOTP', + itemId: 'totp-panel', + reference: 'totp_panel', + tfa_type: 'totp', + border: false, + bind: { + disabled: '{!canSetupTOTP}' + }, + layout: { + type: 'vbox', + align: 'stretch' + }, + items: [ + { + xtype: 'form', + layout: 'anchor', + border: false, + reference: 'totp_form', + fieldDefaults: { + anchor: '100%', + padding: '0 5' + }, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('User name'), + cbind: { + value: '{userid}' + } + }, + { + layout: 'hbox', + border: false, + padding: '0 0 5 0', + items: [{ + xtype: 'textfield', + fieldLabel: gettext('Secret'), + emptyText: gettext('Unchanged'), + name: 'secret', + reference: 'tfa_secret', + regex: /^[A-Z2-7=]+$/, + regexText: 'Must be base32 [A-Z2-7=]', + maskRe: /[A-Z2-7=]/, + qrupdate: true, + flex: 4 + }, + { + xtype: 'button', + text: gettext('Randomize'), + reference: 'randomize_button', + handler: 'randomizeSecret', + flex: 1 + }] + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Time period'), + name: 'step', + // Google Authenticator ignores this and generates bogus data + hidden: true, + value: 30, + minValue: 10, + qrupdate: true + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Digits'), + name: 'digits', + value: 6, + // Google Authenticator ignores this and generates bogus data + hidden: true, + minValue: 6, + maxValue: 8, + qrupdate: true + }, + { + xtype: 'textfield', + fieldLabel: gettext('Issuer Name'), + name: 'issuer', + value: 'Proxmox Web UI', + qrupdate: true + } + ] + }, + { + xtype: 'box', + itemId: 'qrbox', + visible: false, // will be enabled when generating a qr code + style: { + 'background-color': 'white', + padding: '5px', + width: '266px', + height: '266px' + } + }, + { + xtype: 'textfield', + fieldLabel: gettext('Verification Code'), + allowBlank: false, + reference: 'challenge', + padding: '0 5', + emptyText: gettext('Scan QR code and enter TOTP auth. code to verify') + } + ] + }, + { + title: 'U2F', + itemId: 'u2f-panel', + reference: 'u2f_panel', + tfa_type: 'u2f', + border: false, + padding: '5 5', + layout: { + type: 'vbox', + align: 'middle' + }, + bind: { + disabled: '{!canSetupU2F}' + }, + items: [ + { + xtype: 'label', + width: 500, + text: gettext('To register a U2F device, connect the device, then click the button and follow the instructions.') + } + ] + } + ] + }, + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Password'), + minLength: 5, + reference: 'password', + allowBlank: false, + validateBlank: true, + padding: '0 0 5 5', + emptyText: gettext('verify current password') + } + ], + + buttons: [ + { + xtype: 'proxmoxHelpButton' + }, + '->', + { + text: gettext('Apply'), + handler: 'applySettings', + bind: { + hidden: '{!in_totp_tab}', + disabled: '{!valid}' + } + }, + { + xtype: 'button', + text: gettext('Register U2F Device'), + handler: 'startU2FRegistration', + bind: { + hidden: '{in_totp_tab}', + disabled: '{tfa_type}' + } + }, + { + text: gettext('Delete'), + reference: 'delete_button', + disabled: true, + handler: 'deleteTFA', + bind: { + disabled: '{!canDeleteTFA}' + } + } + ], + + initComponent: function() { + var me = this; + + if (!me.userid) { + throw "no userid given"; + } + + me.callParent(); + + Ext.GlobalEvents.fireEvent('proxmoxShowHelp', 'pveum_tfa_auth'); + } +}); +Ext.define('PVE.dc.UserEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcUserEdit'], + + isAdd: true, + + initComponent : function() { + var me = this; + + me.isCreate = !me.userid; + + var url; + var method; + var realm; + + if (me.isCreate) { + url = '/api2/extjs/access/users'; + method = 'POST'; + } else { + url = '/api2/extjs/access/users/' + encodeURIComponent(me.userid); + method = 'PUT'; + } + + var verifypw; + var pwfield; + + var validate_pw = function() { + if (verifypw.getValue() !== pwfield.getValue()) { + return gettext("Passwords do not match"); + } + return true; + }; + + verifypw = Ext.createWidget('textfield', { + inputType: 'password', + fieldLabel: gettext('Confirm password'), + name: 'verifypassword', + submitValue: false, + disabled: true, + hidden: true, + validator: validate_pw + }); + + pwfield = Ext.createWidget('textfield', { + inputType: 'password', + fieldLabel: gettext('Password'), + minLength: 5, + name: 'password', + disabled: true, + hidden: true, + validator: validate_pw + }); + + var update_passwd_field = function(realm) { + if (realm === 'pve') { + pwfield.setVisible(true); + pwfield.setDisabled(false); + verifypw.setVisible(true); + verifypw.setDisabled(false); + } else { + pwfield.setVisible(false); + pwfield.setDisabled(true); + verifypw.setVisible(false); + verifypw.setDisabled(true); + } + + }; + + var column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'userid', + fieldLabel: gettext('User name'), + value: me.userid, + allowBlank: false, + submitValue: me.isCreate ? true : false + }, + pwfield, verifypw, + { + xtype: 'pveGroupSelector', + name: 'groups', + multiSelect: true, + allowBlank: true, + fieldLabel: gettext('Group') + }, + { + xtype: 'datefield', + name: 'expire', + emptyText: 'never', + format: 'Y-m-d', + submitFormat: 'U', + fieldLabel: gettext('Expire') + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enabled'), + name: 'enable', + uncheckedValue: 0, + defaultValue: 1, + checked: true + } + ]; + + var column2 = [ + { + xtype: 'textfield', + name: 'firstname', + fieldLabel: gettext('First Name') + }, + { + xtype: 'textfield', + name: 'lastname', + fieldLabel: gettext('Last Name') + }, + { + xtype: 'textfield', + name: 'email', + fieldLabel: gettext('E-Mail'), + vtype: 'proxmoxMail' + } + ]; + + if (me.isCreate) { + column1.splice(1,0,{ + xtype: 'pveRealmComboBox', + name: 'realm', + fieldLabel: gettext('Realm'), + allowBlank: false, + matchFieldWidth: false, + listConfig: { width: 300 }, + listeners: { + change: function(combo, newValue){ + realm = newValue; + update_passwd_field(realm); + } + }, + submitValue: false + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + column1: column1, + column2: column2, + columnB: [ + { + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + } + ], + advancedItems: [ + { + xtype: 'textfield', + name: 'keys', + fieldLabel: gettext('Key IDs') + } + ], + onGetValues: function(values) { + // hack: ExtJS datefield does not submit 0, so we need to set that + if (!values.expire) { + values.expire = 0; + } + + if (realm) { + values.userid = values.userid + '@' + realm; + } + + if (!values.password) { + delete values.password; + } + + return values; + } + }); + + Ext.applyIf(me, { + subject: gettext('User'), + url: url, + method: method, + fieldDefaults: { + labelWidth: 110 // for spanish translation + }, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var data = response.result.data; + if (Ext.isDefined(data.expire)) { + if (data.expire) { + data.expire = new Date(data.expire * 1000); + } else { + // display 'never' instead of '1970-01-01' + data.expire = null; + } + } + me.setValues(data); + } + }); + } + } +}); +/*jslint confusion: true */ +Ext.define('PVE.dc.UserView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveUserView'], + + onlineHelp: 'pveum_users', + + stateful: true, + stateId: 'grid-users', + + initComponent : function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + var store = new Ext.data.Store({ + id: "users", + model: 'pve-users', + sorters: { + property: 'userid', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/access/users/', + enableFn: function(rec) { + if (!caps.access['User.Modify']) { + return false; + } + return rec.data.userid !== 'root@pam'; + }, + callback: function() { + reload(); + } + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec || !caps.access['User.Modify']) { + return; + } + + var win = Ext.create('PVE.dc.UserEdit',{ + userid: rec.data.userid + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + enableFn: function(rec) { + return !!caps.access['User.Modify']; + }, + selModel: sm, + handler: run_editor + }); + + var pwchange_btn = new Proxmox.button.Button({ + text: gettext('Password'), + disabled: true, + selModel: sm, + handler: function(btn, event, rec) { + var win = Ext.create('Proxmox.window.PasswordEdit', { + userid: rec.data.userid + }); + win.on('destroy', reload); + win.show(); + } + }); + + var tfachange_btn = new Proxmox.button.Button({ + text: 'TFA', + disabled: true, + selModel: sm, + handler: function(btn, event, rec) { + var d = rec.data; + var tfa_type = PVE.Parser.parseTfaType(d.keys); + var win = Ext.create('PVE.window.TFAEdit',{ + tfa_type: tfa_type, + userid: d.userid + }); + win.on('destroy', reload); + win.show(); + } + }); + + var tbar = [ + { + text: gettext('Add'), + disabled: !caps.access['User.Modify'], + handler: function() { + var win = Ext.create('PVE.dc.UserEdit',{ + }); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn, pwchange_btn, tfachange_btn + ]; + + var render_username = function(userid) { + return userid.match(/^(.+)(@[^@]+)$/)[1]; + }; + + var render_realm = function(userid) { + return userid.match(/@([^@]+)$/)[1]; + }; + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('User name'), + width: 200, + sortable: true, + renderer: render_username, + dataIndex: 'userid' + }, + { + header: gettext('Realm'), + width: 100, + sortable: true, + renderer: render_realm, + dataIndex: 'userid' + }, + { + header: gettext('Enabled'), + width: 80, + sortable: true, + renderer: Proxmox.Utils.format_boolean, + dataIndex: 'enable' + }, + { + header: gettext('Expire'), + width: 80, + sortable: true, + renderer: Proxmox.Utils.format_expire, + dataIndex: 'expire' + }, + { + header: gettext('Name'), + width: 150, + sortable: true, + renderer: PVE.Utils.render_full_name, + dataIndex: 'firstname' + }, + { + header: 'TFA', + width: 50, + sortable: true, + renderer: function(v) { + var tfa_type = PVE.Parser.parseTfaType(v); + if (tfa_type === undefined) { + return Proxmox.Utils.noText; + } else if (tfa_type === 1) { + return Proxmox.Utils.yesText; + } else { + return tfa_type; + } + }, + dataIndex: 'keys' + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.PoolView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pvePoolView'], + + onlineHelp: 'pveum_pools', + + stateful: true, + stateId: 'grid-pools', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-pools', + sorters: { + property: 'poolid', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/pools/', + callback: function () { + reload(); + } + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.PoolEdit',{ + poolid: rec.data.poolid + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var tbar = [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.PoolEdit', {}); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn + ]; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Name'), + width: 200, + sortable: true, + dataIndex: 'poolid' + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.PoolEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcPoolEdit'], + + initComponent : function() { + var me = this; + + me.isCreate = !me.poolid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/pools'; + method = 'POST'; + } else { + url = '/api2/extjs/pools/' + me.poolid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Pool'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + fieldLabel: gettext('Name'), + name: 'poolid', + value: me.poolid, + allowBlank: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment', + allowBlank: true + } + ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load(); + } + } +}); +Ext.define('PVE.dc.GroupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveGroupView'], + + onlineHelp: 'pveum_groups', + + stateful: true, + stateId: 'grid-groups', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-groups', + sorters: { + property: 'groupid', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + callback: function() { + reload(); + }, + baseurl: '/access/groups/' + }); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.GroupEdit',{ + groupid: rec.data.groupid + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var tbar = [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.GroupEdit', {}); + win.on('destroy', reload); + win.show(); + } + }, + edit_btn, remove_btn + ]; + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Name'), + width: 200, + sortable: true, + dataIndex: 'groupid' + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.GroupEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcGroupEdit'], + + initComponent : function() { + var me = this; + + me.isCreate = !me.groupid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/access/groups'; + method = 'POST'; + } else { + url = '/api2/extjs/access/groups/' + me.groupid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Group'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + fieldLabel: gettext('Name'), + name: 'groupid', + value: me.groupid, + allowBlank: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment', + allowBlank: true + } + ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load(); + } + } +}); +Ext.define('PVE.dc.RoleView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveRoleView'], + + onlineHelp: 'pveum_roles', + + stateful: true, + stateId: 'grid-roles', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-roles', + sorters: { + property: 'roleid', + order: 'DESC' + } + }); + + var render_privs = function(value, metaData) { + + if (!value) { + return '-'; + } + + // allow word wrap + metaData.style = 'white-space:normal;'; + + return value.replace(/\,/g, ' '); + }; + + Proxmox.Utils.monStoreErrors(me, store); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + store.load(); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + if (rec.data.special === "1") { + return; + } + + var win = Ext.create('PVE.dc.RoleEdit',{ + roleid: rec.data.roleid, + privs: rec.data.privs + }); + win.on('destroy', reload); + win.show(); + }; + + Ext.apply(me, { + store: store, + selModel: sm, + + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Built-In'), + width: 65, + sortable: true, + dataIndex: 'special', + renderer: Proxmox.Utils.format_boolean + }, + { + header: gettext('Name'), + width: 150, + sortable: true, + dataIndex: 'roleid' + }, + { + itemid: 'privs', + header: gettext('Privileges'), + sortable: false, + renderer: render_privs, + dataIndex: 'privs', + flex: 1 + } + ], + listeners: { + activate: function() { + store.load(); + }, + itemdblclick: run_editor + }, + tbar: [ + { + text: gettext('Create'), + handler: function() { + var win = Ext.create('PVE.dc.RoleEdit', {}); + win.on('destroy', reload); + win.show(); + } + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor, + enableFn: function(record) { + return record.data.special !== '1'; + } + }, + { + xtype: 'proxmoxStdRemoveButton', + selModel: sm, + callback: function() { + reload(); + }, + baseurl: '/access/roles/', + enableFn: function(record) { + return record.data.special !== '1'; + } + } + ] + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.RoleEdit', { + extend: 'Proxmox.window.Edit', + xtype: 'pveDcRoleEdit', + + width: 400, + + initComponent : function() { + var me = this; + + me.isCreate = !me.roleid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/access/roles'; + method = 'POST'; + } else { + url = '/api2/extjs/access/roles/' + me.roleid; + method = 'PUT'; + } + + Ext.applyIf(me, { + subject: gettext('Role'), + url: url, + method: method, + items: [ + { + xtype: me.isCreate ? 'proxmoxtextfield' : 'displayfield', + name: 'roleid', + value: me.roleid, + allowBlank: false, + fieldLabel: gettext('Name') + }, + { + xtype: 'pvePrivilegesSelector', + name: 'privs', + value: me.privs, + allowBlank: false, + fieldLabel: gettext('Privileges') + } + ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response) { + var data = response.result.data; + var keys = Ext.Object.getKeys(data); + + me.setValues({ + privs: keys, + roleid: me.roleid + }); + } + }); + } + } +}); +Ext.define('PVE.dc.ACLAdd', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveACLAdd'], + url: '/access/acl', + method: 'PUT', + isAdd: true, + initComponent : function() { + + var me = this; + + me.isCreate = true; + + var items = [ + { + xtype: me.path ? 'hiddenfield' : 'pvePermPathSelector', + name: 'path', + value: me.path, + allowBlank: false, + fieldLabel: gettext('Path') + } + ]; + + if (me.aclType === 'group') { + me.subject = gettext("Group Permission"); + items.push({ + xtype: 'pveGroupSelector', + name: 'groups', + fieldLabel: gettext('Group') + }); + } else if (me.aclType === 'user') { + me.subject = gettext("User Permission"); + items.push({ + xtype: 'pveUserSelector', + name: 'users', + fieldLabel: gettext('User') + }); + } else { + throw "unknown ACL type"; + } + + items.push({ + xtype: 'pveRoleSelector', + name: 'roles', + value: 'NoAccess', + fieldLabel: gettext('Role') + }); + + if (!me.path) { + items.push({ + xtype: 'proxmoxcheckbox', + name: 'propagate', + checked: true, + uncheckedValue: 0, + fieldLabel: gettext('Propagate') + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + items: items, + onlineHelp: 'pveum_permission_management' + }); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.dc.ACLView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveACLView'], + + onlineHelp: 'chapter_user_management', + + stateful: true, + stateId: 'grid-acls', + + // use fixed path + path: undefined, + + initComponent : function() { + var me = this; + + var store = Ext.create('Ext.data.Store',{ + model: 'pve-acl', + proxy: { + type: 'proxmox', + url: "/api2/json/access/acl" + }, + sorters: { + property: 'path', + order: 'DESC' + } + }); + + if (me.path) { + store.addFilter(Ext.create('Ext.util.Filter',{ + filterFn: function(item) { + if (item.data.path === me.path) { + return true; + } + } + })); + } + + var render_ugid = function(ugid, metaData, record) { + if (record.data.type == 'group') { + return '@' + ugid; + } + + return ugid; + }; + + var columns = [ + { + header: gettext('User') + '/' + gettext('Group'), + flex: 1, + sortable: true, + renderer: render_ugid, + dataIndex: 'ugid' + }, + { + header: gettext('Role'), + flex: 1, + sortable: true, + dataIndex: 'roleid' + } + ]; + + if (!me.path) { + columns.unshift({ + header: gettext('Path'), + flex: 1, + sortable: true, + dataIndex: 'path' + }); + columns.push({ + header: gettext('Propagate'), + width: 80, + sortable: true, + dataIndex: 'propagate' + }); + } + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + store.load(); + }; + + var remove_btn = new Proxmox.button.Button({ + text: gettext('Remove'), + disabled: true, + selModel: sm, + confirmMsg: gettext('Are you sure you want to remove this entry'), + handler: function(btn, event, rec) { + var params = { + 'delete': 1, + path: rec.data.path, + roles: rec.data.roleid + }; + if (rec.data.type === 'group') { + params.groups = rec.data.ugid; + } else if (rec.data.type === 'user') { + params.users = rec.data.ugid; + } else { + throw 'unknown data type'; + } + + Proxmox.Utils.API2Request({ + url: '/access/acl', + params: params, + method: 'PUT', + waitMsgTarget: me, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + } + }); + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: [ + { + text: gettext('Add'), + menu: { + xtype: 'menu', + items: [ + { + text: gettext('Group Permission'), + iconCls: 'fa fa-fw fa-group', + handler: function() { + var win = Ext.create('PVE.dc.ACLAdd',{ + aclType: 'group', + path: me.path + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('User Permission'), + iconCls: 'fa fa-fw fa-user', + handler: function() { + var win = Ext.create('PVE.dc.ACLAdd',{ + aclType: 'user', + path: me.path + }); + win.on('destroy', reload); + win.show(); + } + } + ] + } + }, + remove_btn + ], + viewConfig: { + trackOver: false + }, + columns: columns, + listeners: { + activate: reload + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-acl', { + extend: 'Ext.data.Model', + fields: [ + 'path', 'type', 'ugid', 'roleid', + { + name: 'propagate', + type: 'boolean' + } + ] + }); + +}); +Ext.define('PVE.dc.AuthView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveAuthView'], + + onlineHelp: 'pveum_authentication_realms', + + stateful: true, + stateId: 'grid-authrealms', + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-domains', + sorters: { + property: 'realm', + order: 'DESC' + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.AuthEdit',{ + realm: rec.data.realm, + authType: rec.data.type + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + baseurl: '/access/domains/', + selModel: sm, + enableFn: function(rec) { + return !(rec.data.type === 'pve' || rec.data.type === 'pam'); + }, + callback: function() { + reload(); + } + }); + + var tbar = [ + { + text: gettext('Add'), + menu: new Ext.menu.Menu({ + items: [ + { + text: gettext('Active Directory Server'), + handler: function() { + var win = Ext.create('PVE.dc.AuthEdit', { + authType: 'ad' + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: gettext('LDAP Server'), + handler: function() { + var win = Ext.create('PVE.dc.AuthEdit',{ + authType: 'ldap' + }); + win.on('destroy', reload); + win.show(); + } + } + ] + }) + }, + edit_btn, remove_btn + ]; + + Ext.apply(me, { + store: store, + selModel: sm, + tbar: tbar, + viewConfig: { + trackOver: false + }, + columns: [ + { + header: gettext('Realm'), + width: 100, + sortable: true, + dataIndex: 'realm' + }, + { + header: gettext('Type'), + width: 100, + sortable: true, + dataIndex: 'type' + }, + { + header: gettext('TFA'), + width: 100, + sortable: true, + dataIndex: 'tfa' + }, + { + header: gettext('Comment'), + sortable: false, + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 1 + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}); +Ext.define('PVE.dc.AuthEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcAuthEdit'], + + isAdd: true, + + initComponent : function() { + var me = this; + + me.isCreate = !me.realm; + + var url; + var method; + var serverlist; + + if (me.isCreate) { + url = '/api2/extjs/access/domains'; + method = 'POST'; + } else { + url = '/api2/extjs/access/domains/' + me.realm; + method = 'PUT'; + } + + var column1 = [ + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'realm', + fieldLabel: gettext('Realm'), + value: me.realm, + allowBlank: false + } + ]; + + if (me.authType === 'ad') { + + me.subject = gettext('Active Directory Server'); + + column1.push({ + xtype: 'textfield', + name: 'domain', + fieldLabel: gettext('Domain'), + emptyText: 'company.net', + allowBlank: false + }); + + } else if (me.authType === 'ldap') { + + me.subject = gettext('LDAP Server'); + + column1.push({ + xtype: 'textfield', + name: 'base_dn', + fieldLabel: gettext('Base Domain Name'), + emptyText: 'CN=Users,DC=Company,DC=net', + allowBlank: false + }); + + column1.push({ + xtype: 'textfield', + name: 'user_attr', + emptyText: 'uid / sAMAccountName', + fieldLabel: gettext('User Attribute Name'), + allowBlank: false + }); + } else if (me.authType === 'pve') { + + if (me.isCreate) { + throw 'unknown auth type'; + } + + me.subject = 'Proxmox VE authentication server'; + + } else if (me.authType === 'pam') { + + if (me.isCreate) { + throw 'unknown auth type'; + } + + me.subject = 'linux PAM'; + + } else { + throw 'unknown auth type '; + } + + column1.push({ + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Default'), + name: 'default', + uncheckedValue: 0 + }); + + var column2 = []; + + if (me.authType === 'ldap' || me.authType === 'ad') { + column2.push( + { + xtype: 'textfield', + fieldLabel: gettext('Server'), + name: 'server1', + allowBlank: false + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Fallback Server'), + deleteEmpty: !me.isCreate, + name: 'server2' + }, + { + xtype: 'proxmoxintegerfield', + name: 'port', + fieldLabel: gettext('Port'), + minValue: 1, + maxValue: 65535, + emptyText: gettext('Default'), + submitEmptyText: false + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: 'SSL', + name: 'secure', + uncheckedValue: 0 + } + ); + } + + // Two Factor Auth settings + + column2.push({ + xtype: 'proxmoxKVComboBox', + name: 'tfa', + deleteEmpty: !me.isCreate, + value: '', + fieldLabel: gettext('TFA'), + comboItems: [ ['__default__', Proxmox.Utils.noneText], ['oath', 'OATH'], ['yubico', 'Yubico']], + listeners: { + change: function(f, value) { + if (!me.rendered) { + return; + } + me.down('field[name=oath_step]').setVisible(value === 'oath'); + me.down('field[name=oath_digits]').setVisible(value === 'oath'); + me.down('field[name=yubico_api_id]').setVisible(value === 'yubico'); + me.down('field[name=yubico_api_key]').setVisible(value === 'yubico'); + me.down('field[name=yubico_url]').setVisible(value === 'yubico'); + } + } + }); + + column2.push({ + xtype: 'proxmoxintegerfield', + name: 'oath_step', + value: '', + minValue: 10, + emptyText: Proxmox.Utils.defaultText + ' (30)', + submitEmptyText: false, + hidden: true, + fieldLabel: 'OATH time step' + }); + + column2.push({ + xtype: 'proxmoxintegerfield', + name: 'oath_digits', + value: '', + minValue: 6, + maxValue: 8, + emptyText: Proxmox.Utils.defaultText + ' (6)', + submitEmptyText: false, + hidden: true, + fieldLabel: 'OATH password length' + }); + + column2.push({ + xtype: 'textfield', + name: 'yubico_api_id', + hidden: true, + fieldLabel: 'Yubico API Id' + }); + + column2.push({ + xtype: 'textfield', + name: 'yubico_api_key', + hidden: true, + fieldLabel: 'Yubico API Key' + }); + + column2.push({ + xtype: 'textfield', + name: 'yubico_url', + hidden: true, + fieldLabel: 'Yubico URL' + }); + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + column1: column1, + column2: column2, + columnB: [{ + xtype: 'textfield', + name: 'comment', + fieldLabel: gettext('Comment') + }], + onGetValues: function(values) { + if (!values.port) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'port' }); + } + delete values.port; + } + + if (me.isCreate) { + values.type = me.authType; + } + + if (values.tfa === 'oath') { + values.tfa = "type=oath"; + if (values.oath_step) { + values.tfa += ",step=" + values.oath_step; + } + if (values.oath_digits) { + values.tfa += ",digits=" + values.oath_digits; + } + } else if (values.tfa === 'yubico') { + values.tfa = "type=yubico"; + values.tfa += ",id=" + values.yubico_api_id; + values.tfa += ",key=" + values.yubico_api_key; + if (values.yubico_url) { + values.tfa += ",url=" + values.yubico_url; + } + } else { + delete values.tfa; + } + + delete values.oath_step; + delete values.oath_digits; + delete values.yubico_api_id; + delete values.yubico_api_key; + delete values.yubico_url; + + return values; + } + }); + + Ext.applyIf(me, { + url: url, + method: method, + fieldDefaults: { + labelWidth: 120 + }, + items: [ ipanel ] + }); + + me.callParent(); + + if (!me.isCreate) { + me.load({ + success: function(response, options) { + var data = response.result.data || {}; + // just to be sure (should not happen) + if (data.type !== me.authType) { + me.close(); + throw "got wrong auth type"; + } + + if (data.tfa) { + var tfacfg = PVE.Parser.parseTfaConfig(data.tfa); + data.tfa = tfacfg.type; + if (tfacfg.type === 'yubico') { + data.yubico_api_key = tfacfg.key; + data.yubico_api_id = tfacfg.id; + data.yubico_url = tfacfg.url; + } else if (tfacfg.type === 'oath') { + // step is a number before + /*jslint confusion: true*/ + data.oath_step = tfacfg.step; + data.oath_digits = tfacfg.digits; + /*jslint confusion: false*/ + } + } + + me.setValues(data); + } + }); + } + } +}); +Ext.define('PVE.dc.BackupEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.pveDcBackupEdit'], + + defaultFocus: undefined, + + initComponent : function() { + var me = this; + + me.isCreate = !me.jobid; + + var url; + var method; + + if (me.isCreate) { + url = '/api2/extjs/cluster/backup'; + method = 'POST'; + } else { + url = '/api2/extjs/cluster/backup/' + me.jobid; + method = 'PUT'; + } + + var vmidField = Ext.create('Ext.form.field.Hidden', { + name: 'vmid' + }); + + /*jslint confusion: true*/ + // 'value' can be assigned a string or an array + var selModeField = Ext.create('Proxmox.form.KVComboBox', { + xtype: 'proxmoxKVComboBox', + comboItems: [ + ['include', gettext('Include selected VMs')], + ['all', gettext('All')], + ['exclude', gettext('Exclude selected VMs')], + ['pool', gettext('Pool based')] + ], + fieldLabel: gettext('Selection mode'), + name: 'selMode', + value: '' + }); + + var sm = Ext.create('Ext.selection.CheckboxModel', { + mode: 'SIMPLE', + listeners: { + selectionchange: function(model, selected) { + var sel = []; + Ext.Array.each(selected, function(record) { + sel.push(record.data.vmid); + }); + + // to avoid endless recursion suspend the vmidField change + // event temporary as it calls us again + vmidField.suspendEvent('change'); + vmidField.setValue(sel); + vmidField.resumeEvent('change'); + } + } + }); + + var storagesel = Ext.create('PVE.form.StorageSelector', { + fieldLabel: gettext('Storage'), + nodename: 'localhost', + storageContent: 'backup', + allowBlank: false, + name: 'storage' + }); + + var store = new Ext.data.Store({ + model: 'PVEResources', + sorters: { + property: 'vmid', + order: 'ASC' + } + }); + + var vmgrid = Ext.createWidget('grid', { + store: store, + border: true, + height: 300, + selModel: sm, + disabled: true, + columns: [ + { + header: 'ID', + dataIndex: 'vmid', + width: 60 + }, + { + header: gettext('Node'), + dataIndex: 'node' + }, + { + header: gettext('Status'), + dataIndex: 'uptime', + renderer: function(value) { + if (value) { + return Proxmox.Utils.runningText; + } else { + return Proxmox.Utils.stoppedText; + } + } + }, + { + header: gettext('Name'), + dataIndex: 'name', + flex: 1 + }, + { + header: gettext('Type'), + dataIndex: 'type' + } + ] + }); + + var selectPoolMembers = function(poolid) { + if (!poolid) { + return; + } + sm.deselectAll(true); + store.filter([ + { + id: 'poolFilter', + property: 'pool', + value: poolid + } + ]); + sm.selectAll(true); + }; + + var selPool = Ext.create('PVE.form.PoolSelector', { + fieldLabel: gettext('Pool to backup'), + hidden: true, + allowBlank: true, + name: 'pool', + listeners: { + change: function( selpool, newValue, oldValue) { + selectPoolMembers(newValue); + } + } + }); + + var nodesel = Ext.create('PVE.form.NodeSelector', { + name: 'node', + fieldLabel: gettext('Node'), + allowBlank: true, + editable: true, + autoSelect: false, + emptyText: '-- ' + gettext('All') + ' --', + listeners: { + change: function(f, value) { + storagesel.setNodename(value || 'localhost'); + var mode = selModeField.getValue(); + store.clearFilter(); + store.filterBy(function(rec) { + return (!value || rec.get('node') === value); + }); + if (mode === 'all') { + sm.selectAll(true); + } + + if (mode === 'pool') { + selectPoolMembers(selPool.value); + } + } + } + }); + + var column1 = [ + nodesel, + storagesel, + { + xtype: 'pveDayOfWeekSelector', + name: 'dow', + fieldLabel: gettext('Day of week'), + multiSelect: true, + value: ['sat'], + allowBlank: false + }, + { + xtype: 'timefield', + fieldLabel: gettext('Start Time'), + name: 'starttime', + format: 'H:i', + formatText: 'HH:MM', + value: '00:00', + allowBlank: false + }, + selModeField, + selPool + ]; + + var column2 = [ + { + xtype: 'textfield', + fieldLabel: gettext('Send email to'), + name: 'mailto' + }, + { + xtype: 'pveEmailNotificationSelector', + fieldLabel: gettext('Email notification'), + name: 'mailnotification', + deleteEmpty: me.isCreate ? false : true, + value: me.isCreate ? 'always' : '' + }, + { + xtype: 'pveCompressionSelector', + fieldLabel: gettext('Compression'), + name: 'compress', + deleteEmpty: me.isCreate ? false : true, + value: 'lzo' + }, + { + xtype: 'pveBackupModeSelector', + fieldLabel: gettext('Mode'), + value: 'snapshot', + name: 'mode' + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enable'), + name: 'enabled', + uncheckedValue: 0, + defaultValue: 1, + checked: true + }, + vmidField + ]; + /*jslint confusion: false*/ + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + onlineHelp: 'chapter_vzdump', + column1: column1, + column2: column2, + onGetValues: function(values) { + if (!values.node) { + if (!me.isCreate) { + Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' }); + } + delete values.node; + } + + var selMode = values.selMode; + delete values.selMode; + + if (selMode === 'all') { + values.all = 1; + values.exclude = ''; + delete values.vmid; + } else if (selMode === 'exclude') { + values.all = 1; + values.exclude = values.vmid; + delete values.vmid; + } else if (selMode === 'pool') { + delete values.vmid; + } + + if (selMode !== 'pool') { + delete values.pool; + } + return values; + } + }); + + var update_vmid_selection = function(list, mode) { + if (mode !== 'all' && mode !== 'pool') { + sm.deselectAll(true); + if (list) { + Ext.Array.each(list.split(','), function(vmid) { + var rec = store.findRecord('vmid', vmid); + if (rec) { + sm.select(rec, true); + } + }); + } + } + }; + + vmidField.on('change', function(f, value) { + var mode = selModeField.getValue(); + update_vmid_selection(value, mode); + }); + + selModeField.on('change', function(f, value, oldValue) { + if (oldValue === 'pool') { + store.removeFilter('poolFilter'); + } + + if (oldValue === 'all') { + sm.deselectAll(true); + vmidField.setValue(''); + } + + if (value === 'all') { + sm.selectAll(true); + vmgrid.setDisabled(true); + } else { + vmgrid.setDisabled(false); + } + + if (value === 'pool') { + vmgrid.setDisabled(true); + vmidField.setValue(''); + selPool.setVisible(true); + selPool.allowBlank = false; + selectPoolMembers(selPool.value); + + } else { + selPool.setVisible(false); + selPool.allowBlank = true; + } + var list = vmidField.getValue(); + update_vmid_selection(list, value); + }); + + var reload = function() { + store.load({ + params: { type: 'vm' }, + callback: function() { + var node = nodesel.getValue(); + store.clearFilter(); + store.filterBy(function(rec) { + return (!node || node.length === 0 || rec.get('node') === node); + }); + var list = vmidField.getValue(); + var mode = selModeField.getValue(); + if (mode === 'all') { + sm.selectAll(true); + } else if (mode === 'pool'){ + selectPoolMembers(selPool.value); + } else { + update_vmid_selection(list, mode); + } + } + }); + }; + + Ext.applyIf(me, { + subject: gettext("Backup Job"), + url: url, + method: method, + items: [ ipanel, vmgrid ] + }); + + me.callParent(); + + if (me.isCreate) { + selModeField.setValue('include'); + } else { + me.load({ + success: function(response, options) { + var data = response.result.data; + + data.dow = data.dow.split(','); + + if (data.all || data.exclude) { + if (data.exclude) { + data.vmid = data.exclude; + data.selMode = 'exclude'; + } else { + data.vmid = ''; + data.selMode = 'all'; + } + } else if (data.pool) { + data.selMode = 'pool'; + data.selPool = data.pool; + } else { + data.selMode = 'include'; + } + + me.setValues(data); + } + }); + } + + reload(); + } +}); + + +Ext.define('PVE.dc.BackupView', { + extend: 'Ext.grid.GridPanel', + + alias: ['widget.pveDcBackupView'], + + onlineHelp: 'chapter_vzdump', + + allText: '-- ' + gettext('All') + ' --', + allExceptText: gettext('All except {0}'), + + initComponent : function() { + var me = this; + + var store = new Ext.data.Store({ + model: 'pve-cluster-backup', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/backup" + } + }); + + var reload = function() { + store.load(); + }; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var win = Ext.create('PVE.dc.BackupEdit',{ + jobid: rec.data.id + }); + win.on('destroy', reload); + win.show(); + }; + + var edit_btn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: '/cluster/backup', + callback: function() { + reload(); + } + }); + + Proxmox.Utils.monStoreErrors(me, store); + + Ext.apply(me, { + store: store, + selModel: sm, + stateful: true, + stateId: 'grid-dc-backup', + viewConfig: { + trackOver: false + }, + tbar: [ + { + text: gettext('Add'), + handler: function() { + var win = Ext.create('PVE.dc.BackupEdit',{}); + win.on('destroy', reload); + win.show(); + } + }, + remove_btn, + edit_btn + ], + columns: [ + { + header: gettext('Enabled'), + width: 80, + dataIndex: 'enabled', + xtype: 'checkcolumn', + sortable: true, + disabled: true, + disabledCls: 'x-item-enabled', + stopSelection: false + }, + { + header: gettext('Node'), + width: 100, + sortable: true, + dataIndex: 'node', + renderer: function(value) { + if (value) { + return value; + } + return me.allText; + } + }, + { + header: gettext('Day of week'), + width: 200, + sortable: false, + dataIndex: 'dow', + renderer: function(val) { + var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + var selected = []; + var cur = -1; + val.split(',').forEach(function(day){ + cur++; + var dow = (dows.indexOf(day)+6)%7; + if (cur === dow) { + if (selected.length === 0 || selected[selected.length-1] === 0) { + selected.push(1); + } else { + selected[selected.length-1]++; + } + } else { + while (cur < dow) { + cur++; + selected.push(0); + } + selected.push(1); + } + }); + + cur = -1; + var days = []; + selected.forEach(function(item) { + cur++; + if (item > 2) { + days.push(Ext.Date.dayNames[(cur+1)] + '-' + Ext.Date.dayNames[(cur+item)%7]); + cur += item-1; + } else if (item == 2) { + days.push(Ext.Date.dayNames[cur+1]); + days.push(Ext.Date.dayNames[(cur+2)%7]); + cur++; + } else if (item == 1) { + days.push(Ext.Date.dayNames[(cur+1)%7]); + } + }); + return days.join(', '); + } + }, + { + header: gettext('Start Time'), + width: 60, + sortable: true, + dataIndex: 'starttime' + }, + { + header: gettext('Storage'), + width: 100, + sortable: true, + dataIndex: 'storage' + }, + { + header: gettext('Selection'), + flex: 1, + sortable: false, + dataIndex: 'vmid', + renderer: function(value, metaData, record) { + /*jslint confusion: true */ + if (record.data.all) { + if (record.data.exclude) { + return Ext.String.format(me.allExceptText, record.data.exclude); + } + return me.allText; + } + if (record.data.vmid) { + return record.data.vmid; + } + + if (record.data.pool) { + return "Pool '"+ record.data.pool + "'"; + } + + return "-"; + } + } + ], + listeners: { + activate: reload, + itemdblclick: run_editor + } + }); + + me.callParent(); + } +}, function() { + + Ext.define('pve-cluster-backup', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'starttime', 'dow', + 'storage', 'node', 'vmid', 'exclude', + 'mailto', 'pool', + { name: 'enabled', type: 'boolean' }, + { name: 'all', type: 'boolean' }, + { name: 'snapshot', type: 'boolean' }, + { name: 'stop', type: 'boolean' }, + { name: 'suspend', type: 'boolean' }, + { name: 'compress', type: 'boolean' } + ] + }); +}); +Ext.define('PVE.dc.Support', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveDcSupport', + pveGuidePath: '/pve-docs/index.html', + onlineHelp: 'getting_help', + + invalidHtml: '

No valid subscription

' + PVE.Utils.noSubKeyHtml, + + communityHtml: 'Please use the public community forum for any questions.', + + activeHtml: 'Please use our support portal for any questions. You can also use the public community forum to get additional information.', + + bugzillaHtml: '

Bug Tracking

Our bug tracking system is available here.', + + docuHtml: function() { + var me = this; + var guideUrl = window.location.origin + me.pveGuidePath; + var text = Ext.String.format('

Documentation

' + + 'The official Proxmox VE Administration Guide' + + ' is included with this installation and can be browsed at ' + + '{0}', guideUrl); + return text; + }, + + updateActive: function(data) { + var me = this; + + var html = '

' + data.productname + '

' + me.activeHtml; + html += '

' + me.docuHtml(); + html += '

' + me.bugzillaHtml; + + me.update(html); + }, + + updateCommunity: function(data) { + var me = this; + + var html = '

' + data.productname + '

' + me.communityHtml; + html += '

' + me.docuHtml(); + html += '

' + me.bugzillaHtml; + + me.update(html); + }, + + updateInactive: function(data) { + var me = this; + me.update(me.invalidHtml); + }, + + initComponent: function() { + var me = this; + + var reload = function() { + Proxmox.Utils.API2Request({ + url: '/nodes/localhost/subscription', + method: 'GET', + waitMsgTarget: me, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + me.update('Unable to load subscription status' + ": " + response.htmlStatus); + }, + success: function(response, opts) { + var data = response.result.data; + + if (data.status === 'Active') { + if (data.level === 'c') { + me.updateCommunity(data); + } else { + me.updateActive(data); + } + } else { + me.updateInactive(data); + } + } + }); + }; + + Ext.apply(me, { + autoScroll: true, + bodyStyle: 'padding:10px', + listeners: { + activate: reload + } + }); + + me.callParent(); + } +}); +Ext.define('pve-security-groups', { + extend: 'Ext.data.Model', + + fields: [ 'group', 'comment', 'digest' ], + idProperty: 'group' +}); + +Ext.define('PVE.SecurityGroupEdit', { + extend: 'Proxmox.window.Edit', + + base_url: "/cluster/firewall/groups", + + allow_iface: false, + + initComponent : function() { + var me = this; + + me.isCreate = (me.group_name === undefined); + + var subject; + + me.url = '/api2/extjs' + me.base_url; + me.method = 'POST'; + + var items = [ + { + xtype: 'textfield', + name: 'group', + value: me.group_name || '', + fieldLabel: gettext('Name'), + allowBlank: false + }, + { + xtype: 'textfield', + name: 'comment', + value: me.group_comment || '', + fieldLabel: gettext('Comment') + } + ]; + + if (me.isCreate) { + subject = gettext('Security Group'); + } else { + subject = gettext('Security Group') + " '" + me.group_name + "'"; + items.push({ + xtype: 'hiddenfield', + name: 'rename', + value: me.group_name + }); + } + + var ipanel = Ext.create('Proxmox.panel.InputPanel', { + // InputPanel does not have a 'create' property, does it need a 'isCreate' + isCreate: me.isCreate, + items: items + }); + + + Ext.apply(me, { + subject: subject, + items: [ ipanel ] + }); + + me.callParent(); + } +}); + +Ext.define('PVE.SecurityGroupList', { + extend: 'Ext.grid.Panel', + alias: 'widget.pveSecurityGroupList', + + stateful: true, + stateId: 'grid-securitygroups', + + rule_panel: undefined, + + addBtn: undefined, + removeBtn: undefined, + editBtn: undefined, + + base_url: "/cluster/firewall/groups", + + initComponent: function() { + /*jslint confusion: true */ + var me = this; + + if (me.rule_panel == undefined) { + throw "no rule panel specified"; + } + + if (me.base_url == undefined) { + throw "no base_url specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-security-groups', + proxy: { + type: 'proxmox', + url: '/api2/json' + me.base_url + }, + sorters: { + property: 'group', + order: 'DESC' + } + }); + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var reload = function() { + var oldrec = sm.getSelection()[0]; + store.load(function(records, operation, success) { + if (oldrec) { + var rec = store.findRecord('group', oldrec.data.group); + if (rec) { + sm.select(rec); + } + } + }); + }; + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + var win = Ext.create('PVE.SecurityGroupEdit', { + digest: rec.data.digest, + group_name: rec.data.group, + group_comment: rec.data.comment + }); + win.show(); + win.on('destroy', reload); + }; + + me.editBtn = new Proxmox.button.Button({ + text: gettext('Edit'), + disabled: true, + selModel: sm, + handler: run_editor + }); + + me.addBtn = new Proxmox.button.Button({ + text: gettext('Create'), + handler: function() { + sm.deselectAll(); + var win = Ext.create('PVE.SecurityGroupEdit', {}); + win.show(); + win.on('destroy', reload); + } + }); + + me.removeBtn = Ext.create('Proxmox.button.StdRemoveButton', { + selModel: sm, + baseurl: me.base_url + '/', + enableFn: function(rec) { + return (rec && me.base_url); + }, + callback: function() { + reload(); + } + }); + + Ext.apply(me, { + store: store, + tbar: [ '' + gettext('Group') + ':', me.addBtn, me.removeBtn, me.editBtn ], + selModel: sm, + columns: [ + { header: gettext('Group'), dataIndex: 'group', width: '100' }, + { header: gettext('Comment'), dataIndex: 'comment', renderer: Ext.String.htmlEncode, flex: 1 } + ], + listeners: { + itemdblclick: run_editor, + select: function(sm, rec) { + var url = '/cluster/firewall/groups/' + rec.data.group; + me.rule_panel.setBaseUrl(url); + }, + deselect: function() { + me.rule_panel.setBaseUrl(undefined); + }, + show: reload + } + }); + + me.callParent(); + + store.load(); + } +}); + +Ext.define('PVE.SecurityGroups', { + extend: 'Ext.panel.Panel', + alias: 'widget.pveSecurityGroups', + + title: 'Security Groups', + + initComponent: function() { + var me = this; + + var rule_panel = Ext.createWidget('pveFirewallRules', { + region: 'center', + allow_groups: false, + list_refs_url: '/cluster/firewall/refs', + tbar_prefix: '' + gettext('Rules') + ':', + border: false + }); + + var sglist = Ext.createWidget('pveSecurityGroupList', { + region: 'west', + rule_panel: rule_panel, + width: '25%', + border: false, + split: true + }); + + + Ext.apply(me, { + layout: 'border', + items: [ sglist, rule_panel ], + listeners: { + show: function() { + sglist.fireEvent('show', sglist); + } + } + }); + + me.callParent(); + } +}); +/* + * Datacenter config panel, located in the center of the ViewPort after the Datacenter view is selected + */ + +Ext.define('PVE.dc.Config', { + extend: 'PVE.panel.Config', + alias: 'widget.PVE.dc.Config', + + onlineHelp: 'pve_admin_guide', + + initComponent: function() { + var me = this; + + var caps = Ext.state.Manager.get('GuiCap'); + + me.items = []; + + Ext.apply(me, { + title: gettext("Datacenter"), + hstateid: 'dctab' + }); + + if (caps.dc['Sys.Audit']) { + me.items.push({ + title: gettext('Summary'), + xtype: 'pveDcSummary', + iconCls: 'fa fa-book', + itemId: 'summary' + }, + { + title: gettext('Cluster'), + xtype: 'pveClusterAdministration', + iconCls: 'fa fa-server', + itemId: 'cluster' + }, + { + title: 'Ceph', + itemId: 'ceph', + iconCls: 'fa fa-ceph', + xtype: 'pveNodeCephStatus' + }, + { + xtype: 'pveDcOptionView', + title: gettext('Options'), + iconCls: 'fa fa-gear', + itemId: 'options' + }); + } + + if (caps.storage['Datastore.Allocate'] || caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveStorageView', + title: gettext('Storage'), + iconCls: 'fa fa-database', + itemId: 'storage' + }); + } + + if (caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveDcBackupView', + iconCls: 'fa fa-floppy-o', + title: gettext('Backup'), + itemId: 'backup' + }, + { + xtype: 'pveReplicaView', + iconCls: 'fa fa-retweet', + title: gettext('Replication'), + itemId: 'replication' + }, + { + xtype: 'pveACLView', + title: gettext('Permissions'), + iconCls: 'fa fa-unlock', + itemId: 'permissions', + expandedOnInit: true + }); + } + + me.items.push({ + xtype: 'pveUserView', + groups: ['permissions'], + iconCls: 'fa fa-user', + title: gettext('Users'), + itemId: 'users' + }); + + if (caps.dc['Sys.Audit']) { + me.items.push({ + xtype: 'pveGroupView', + title: gettext('Groups'), + iconCls: 'fa fa-users', + groups: ['permissions'], + itemId: 'groups' + }, + { + xtype: 'pvePoolView', + title: gettext('Pools'), + iconCls: 'fa fa-tags', + groups: ['permissions'], + itemId: 'pools' + }, + { + xtype: 'pveRoleView', + title: gettext('Roles'), + iconCls: 'fa fa-male', + groups: ['permissions'], + itemId: 'roles' + }, + { + xtype: 'pveAuthView', + title: gettext('Authentication'), + groups: ['permissions'], + iconCls: 'fa fa-key', + itemId: 'domains' + }, + { + xtype: 'pveHAStatus', + title: 'HA', + iconCls: 'fa fa-heartbeat', + itemId: 'ha' + }, + { + title: gettext('Groups'), + groups: ['ha'], + xtype: 'pveHAGroupsView', + iconCls: 'fa fa-object-group', + itemId: 'ha-groups' + }, + { + title: gettext('Fencing'), + groups: ['ha'], + iconCls: 'fa fa-bolt', + xtype: 'pveFencingView', + itemId: 'ha-fencing' + }, + { + xtype: 'pveFirewallRules', + title: gettext('Firewall'), + allow_iface: true, + base_url: '/cluster/firewall/rules', + list_refs_url: '/cluster/firewall/refs', + iconCls: 'fa fa-shield', + itemId: 'firewall' + }, + { + xtype: 'pveFirewallOptions', + title: gettext('Options'), + groups: ['firewall'], + iconCls: 'fa fa-gear', + base_url: '/cluster/firewall/options', + onlineHelp: 'pve_firewall_cluster_wide_setup', + fwtype: 'dc', + itemId: 'firewall-options' + }, + { + xtype: 'pveSecurityGroups', + title: gettext('Security Group'), + groups: ['firewall'], + iconCls: 'fa fa-group', + itemId: 'firewall-sg' + }, + { + xtype: 'pveFirewallAliases', + title: gettext('Alias'), + groups: ['firewall'], + iconCls: 'fa fa-external-link', + base_url: '/cluster/firewall/aliases', + itemId: 'firewall-aliases' + }, + { + xtype: 'pveIPSet', + title: 'IPSet', + groups: ['firewall'], + iconCls: 'fa fa-list-ol', + base_url: '/cluster/firewall/ipset', + list_refs_url: '/cluster/firewall/refs', + itemId: 'firewall-ipset' + }, + { + xtype: 'pveDcSupport', + title: gettext('Support'), + itemId: 'support', + iconCls: 'fa fa-comments-o' + }); + } + + me.callParent(); + } +}); +Ext.define('PVE.dc.NodeView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pveDcNodeView', + + title: gettext('Nodes'), + disableSelection: true, + scrollable: true, + + columns: [ + { + header: gettext('Name'), + flex: 1, + sortable: true, + dataIndex: 'name' + }, + { + header: 'ID', + width: 40, + sortable: true, + dataIndex: 'nodeid' + }, + { + header: gettext('Online'), + width: 60, + sortable: true, + dataIndex: 'online', + renderer: function(value) { + var cls = (value)?'good':'critical'; + return ''; + } + }, + { + header: gettext('Support'), + width: 100, + sortable: true, + dataIndex: 'level', + renderer: PVE.Utils.render_support_level + }, + { + header: gettext('Server Address'), + width: 115, + sortable: true, + dataIndex: 'ip' + }, + { + header: gettext('CPU usage'), + sortable: true, + width: 110, + dataIndex: 'cpuusage', + tdCls: 'x-progressbar-default-cell', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Memory usage'), + width: 110, + sortable: true, + tdCls: 'x-progressbar-default-cell', + dataIndex: 'memoryusage', + xtype: 'widgetcolumn', + widget: { + xtype: 'pveProgressBar' + } + }, + { + header: gettext('Uptime'), + sortable: true, + dataIndex: 'uptime', + align: 'right', + renderer: Proxmox.Utils.render_uptime + } + ], + + stateful: true, + stateId: 'grid-cluster-nodes', + tools: [ + { + type: 'up', + handler: function(){ + var me = this.up('grid'); + var height = Math.max(me.getHeight()-50, 250); + me.setHeight(height); + } + }, + { + type: 'down', + handler: function(){ + var me = this.up('grid'); + var height = me.getHeight()+50; + me.setHeight(height); + } + } + ] +}, function() { + + Ext.define('pve-dc-nodes', { + extend: 'Ext.data.Model', + fields: [ 'id', 'type', 'name', 'nodeid', 'ip', 'level', 'local', 'online'], + idProperty: 'id' + }); + +}); + +Ext.define('PVE.widget.ProgressBar',{ + extend: 'Ext.Progress', + alias: 'widget.pveProgressBar', + + animate: true, + textTpl: [ + '{percent}%' + ], + + setValue: function(value){ + var me = this; + me.callParent([value]); + + me.removeCls(['warning', 'critical']); + + if (value > 0.89) { + me.addCls('critical'); + } else if (value > 0.59) { + me.addCls('warning'); + } + } +}); +/*jslint confusion: true*/ +Ext.define('pve-cluster-nodes', { + extend: 'Ext.data.Model', + fields: [ + 'node', { type: 'integer', name: 'nodeid' }, 'ring0_addr', 'ring1_addr', + { type: 'integer', name: 'quorum_votes' } + ], + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/config/nodes" + }, + idProperty: 'nodeid' +}); + +Ext.define('pve-cluster-info', { + extend: 'Ext.data.Model', + proxy: { + type: 'proxmox', + url: "/api2/json/cluster/config/join" + } +}); + +Ext.define('PVE.ClusterAdministration', { + extend: 'Ext.panel.Panel', + xtype: 'pveClusterAdministration', + + title: gettext('Cluster Administration'), + onlineHelp: 'chapter_pvecm', + + border: false, + defaults: { border: false }, + + viewModel: { + parent: null, + data: { + totem: {}, + nodelist: [], + preferred_node: { + name: '', + fp: '', + addr: '' + }, + isInCluster: false, + nodecount: 0 + } + }, + + items: [ + { + xtype: 'panel', + title: gettext('Cluster Information'), + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.store = Ext.create('Proxmox.data.UpdateStore', { + autoStart: true, + interval: 15 * 1000, + storeid: 'pve-cluster-info', + model: 'pve-cluster-info' + }); + view.store.on('load', this.onLoad, this); + view.on('destroy', view.store.stopUpdate); + }, + + onLoad: function(store, records, success) { + var vm = this.getViewModel(); + if (!success || !records || !records[0].data) { + vm.set('totem', {}); + vm.set('isInCluster', false); + vm.set('nodelist', []); + vm.set('preferred_node', { + name: '', + addr: '', + fp: '' + }); + return; + } + var data = records[0].data; + vm.set('totem', data.totem); + vm.set('isInCluster', !!data.totem.cluster_name); + vm.set('nodelist', data.nodelist); + + var nodeinfo = Ext.Array.findBy(data.nodelist, function (el) { + return el.name === data.preferred_node; + }); + + vm.set('preferred_node', { + name: data.preferred_node, + addr: nodeinfo.pve_addr, + ring_addr: [ nodeinfo.ring0_addr, nodeinfo.ring1_addr ], + fp: nodeinfo.pve_fp + }); + }, + + onCreate: function() { + var view = this.getView(); + view.store.stopUpdate(); + var win = Ext.create('PVE.ClusterCreateWindow', { + autoShow: true, + listeners: { + destroy: function() { + view.store.startUpdate(); + } + } + }); + }, + + onClusterInfo: function() { + var vm = this.getViewModel(); + var win = Ext.create('PVE.ClusterInfoWindow', { + joinInfo: { + ipAddress: vm.get('preferred_node.addr'), + fingerprint: vm.get('preferred_node.fp'), + ring_addr: vm.get('preferred_node.ring_addr'), + totem: vm.get('totem') + } + }); + win.show(); + }, + + onJoin: function() { + var view = this.getView(); + view.store.stopUpdate(); + var win = Ext.create('PVE.ClusterJoinNodeWindow', { + autoShow: true, + listeners: { + destroy: function() { + view.store.startUpdate(); + } + } + }); + } + }, + tbar: [ + { + text: gettext('Create Cluster'), + reference: 'createButton', + handler: 'onCreate', + bind: { + disabled: '{isInCluster}' + } + }, + { + text: gettext('Join Information'), + reference: 'addButton', + handler: 'onClusterInfo', + bind: { + disabled: '{!isInCluster}' + } + }, + { + text: gettext('Join Cluster'), + reference: 'joinButton', + handler: 'onJoin', + bind: { + disabled: '{isInCluster}' + } + } + ], + layout: 'hbox', + bodyPadding: 5, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Cluster Name'), + bind: { + value: '{totem.cluster_name}', + hidden: '{!isInCluster}' + }, + flex: 1 + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Config Version'), + bind: { + value: '{totem.config_version}', + hidden: '{!isInCluster}' + }, + flex: 1 + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Number of Nodes'), + labelWidth: 120, + bind: { + value: '{nodecount}', + hidden: '{!isInCluster}' + }, + flex: 1 + }, + { + xtype: 'displayfield', + value: gettext('Standalone node - no cluster defined'), + bind: { + hidden: '{isInCluster}' + }, + flex: 1 + } + ] + }, + { + xtype: 'grid', + title: gettext('Cluster Nodes'), + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + view.rstore = Ext.create('Proxmox.data.UpdateStore', { + autoLoad: true, + xtype: 'update', + interval: 5 * 1000, + autoStart: true, + storeid: 'pve-cluster-nodes', + model: 'pve-cluster-nodes' + }); + view.setStore(Ext.create('Proxmox.data.DiffStore', { + rstore: view.rstore, + sorters: { + property: 'nodeid', + order: 'DESC' + } + })); + Proxmox.Utils.monStoreErrors(view, view.rstore); + view.rstore.on('load', this.onLoad, this); + view.on('destroy', view.rstore.stopUpdate); + }, + + onLoad: function(store, records, success) { + var vm = this.getViewModel(); + if (!success || !records) { + vm.set('nodecount', 0); + return; + } + vm.set('nodecount', records.length); + } + }, + columns: [ + { + header: gettext('Nodename'), + flex: 2, + dataIndex: 'name' + }, + { + header: gettext('ID'), + flex: 1, + dataIndex: 'nodeid' + }, + { + header: gettext('Votes'), + flex: 1, + dataIndex: 'quorum_votes' + }, + { + header: Ext.String.format(gettext('Link {0}'), 0), + flex: 2, + dataIndex: 'ring0_addr' + }, + { + header: Ext.String.format(gettext('Link {0}'), 1), + flex: 2, + dataIndex: 'ring1_addr' + } + ] + } + ] +}); +/*jslint confusion: true*/ +Ext.define('PVE.ClusterCreateWindow', { + extend: 'Proxmox.window.Edit', + xtype: 'pveClusterCreateWindow', + + title: gettext('Create Cluster'), + width: 600, + + method: 'POST', + url: '/cluster/config', + + isCreate: true, + subject: gettext('Cluster'), + showTaskViewer: true, + + onlineHelp: 'pvecm_create_cluster', + + items: { + xtype: 'inputpanel', + items: [{ + xtype: 'textfield', + fieldLabel: gettext('Cluster Name'), + allowBlank: false, + name: 'clustername' + }, + { + xtype: 'proxmoxNetworkSelector', + fieldLabel: Ext.String.format(gettext('Link {0}'), 0), + emptyText: gettext("Optional, defaults to IP resolved by node's hostname"), + name: 'link0', + autoSelect: false, + valueField: 'address', + displayField: 'address', + skipEmptyText: true + }], + advancedItems: [{ + xtype: 'proxmoxNetworkSelector', + fieldLabel: Ext.String.format(gettext('Link {0}'), 1), + emptyText: gettext("Optional second link for redundancy"), + name: 'link1', + autoSelect: false, + valueField: 'address', + displayField: 'address', + skipEmptyText: true + }] + } +}); + +Ext.define('PVE.ClusterInfoWindow', { + extend: 'Ext.window.Window', + xtype: 'pveClusterInfoWindow', + mixins: ['Proxmox.Mixin.CBind'], + + width: 800, + modal: true, + resizable: false, + title: gettext('Cluster Join Information'), + + joinInfo: { + ipAddress: undefined, + fingerprint: undefined, + totem: {} + }, + + items: [ + { + xtype: 'component', + border: false, + padding: '10 10 10 10', + html: gettext("Copy the Join Information here and use it on the node you want to add.") + }, + { + xtype: 'container', + layout: 'form', + border: false, + padding: '0 10 10 10', + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('IP Address'), + cbind: { value: '{joinInfo.ipAddress}' }, + editable: false + }, + { + xtype: 'textfield', + fieldLabel: gettext('Fingerprint'), + cbind: { value: '{joinInfo.fingerprint}' }, + editable: false + }, + { + xtype: 'textarea', + inputId: 'pveSerializedClusterInfo', + fieldLabel: gettext('Join Information'), + grow: true, + cbind: { joinInfo: '{joinInfo}' }, + editable: false, + listeners: { + afterrender: function(field) { + if (!field.joinInfo) { + return; + } + var jsons = Ext.JSON.encode(field.joinInfo); + var base64s = Ext.util.Base64.encode(jsons); + field.setValue(base64s); + } + } + } + ] + } + ], + dockedItems: [{ + dock: 'bottom', + xtype: 'toolbar', + items: [{ + xtype: 'button', + handler: function(b) { + var el = document.getElementById('pveSerializedClusterInfo'); + el.select(); + document.execCommand("copy"); + }, + text: gettext('Copy Information') + }] + }] +}); + +Ext.define('PVE.ClusterJoinNodeWindow', { + extend: 'Proxmox.window.Edit', + xtype: 'pveClusterJoinNodeWindow', + + title: gettext('Cluster Join'), + width: 800, + + method: 'POST', + url: '/cluster/config/join', + + defaultFocus: 'textarea[name=serializedinfo]', + isCreate: true, + submitText: gettext('Join'), + showTaskViewer: true, + + onlineHelp: 'chapter_pvecm', + + viewModel: { + parent: null, + data: { + info: { + fp: '', + ip: '', + ring0Needed: false, + ring1Possible: false, + ring1Needed: false + } + }, + formulas: { + ring0EmptyText: function(get) { + if (get('info.ring0Needed')) { + return gettext("Cannot use default address safely"); + } else { + return gettext("Default: IP resolved by node's hostname"); + } + } + } + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + '#': { + close: function() { + delete PVE.Utils.silenceAuthFailures; + } + }, + 'proxmoxcheckbox[name=assistedEntry]': { + change: 'onInputTypeChange' + }, + 'textarea[name=serializedinfo]': { + change: 'recomputeSerializedInfo', + enable: 'resetField' + }, + 'proxmoxtextfield[name=ring1_addr]': { + enable: 'ring1Needed' + }, + 'textfield': { + disable: 'resetField' + } + }, + resetField: function(field) { + field.reset(); + }, + ring1Needed: function(f) { + var vm = this.getViewModel(); + f.allowBlank = !vm.get('info.ring1Needed'); + }, + onInputTypeChange: function(field, assistedInput) { + var vm = this.getViewModel(); + if (!assistedInput) { + vm.set('info.ring1Possible', true); + } + }, + recomputeSerializedInfo: function(field, value) { + var vm = this.getViewModel(); + var jsons = Ext.util.Base64.decode(value); + var joinInfo = Ext.JSON.decode(jsons, true); + + var info = { + fp: '', + ring1Needed: false, + ring1Possible: false, + ip: '' + }; + + var totem = {}; + if (!(joinInfo && joinInfo.totem)) { + field.valid = false; + } else { + var ring0Needed = false; + if (joinInfo.ring_addr !== undefined) { + ring0Needed = joinInfo.ring_addr[0] !== joinInfo.ipAddress; + } + + info = { + ip: joinInfo.ipAddress, + fp: joinInfo.fingerprint, + ring0Needed: ring0Needed, + ring1Possible: !!joinInfo.totem['interface']['1'], + ring1Needed: !!joinInfo.totem['interface']['1'] + }; + totem = joinInfo.totem; + field.valid = true; + } + + vm.set('info', info); + } + }, + + submit: function() { + // joining may produce temporarily auth failures, ignore as long the task runs + PVE.Utils.silenceAuthFailures = true; + this.callParent(); + }, + + taskDone: function(success) { + delete PVE.Utils.silenceAuthFailures; + if (success) { + var txt = gettext('Cluster join task finished, node certificate may have changed, reload GUI!'); + // ensure user cannot do harm + Ext.getBody().mask(txt, ['pve-static-mask']); + // TaskView may hide above mask, so tell him directly + Ext.Msg.show({ + title: gettext('Join Task Finished'), + icon: Ext.Msg.INFO, + msg: txt + }); + // reload always (if user wasn't faster), but wait a bit for pveproxy + Ext.defer(function() { + window.location.reload(true); + }, 5000); + } + }, + + items: [{ + xtype: 'proxmoxcheckbox', + reference: 'assistedEntry', + name: 'assistedEntry', + submitValue: false, + value: true, + autoEl: { + tag: 'div', + 'data-qtip': gettext('Select if join information should be extracted from pasted cluster information, deselect for manual entering') + }, + boxLabel: gettext('Assisted join: Paste encoded cluster join information and enter password.') + }, + { + xtype: 'textarea', + name: 'serializedinfo', + submitValue: false, + allowBlank: false, + fieldLabel: gettext('Information'), + emptyText: gettext('Paste encoded Cluster Information here'), + validator: function(val) { + return val === '' || this.valid || + gettext('Does not seem like a valid encoded Cluster Information!'); + }, + bind: { + disabled: '{!assistedEntry.checked}', + hidden: '{!assistedEntry.checked}' + }, + value: '' + }, + { + xtype: 'inputpanel', + column1: [ + { + xtype: 'textfield', + fieldLabel: gettext('Peer Address'), + allowBlank: false, + bind: { + value: '{info.ip}', + readOnly: '{assistedEntry.checked}' + }, + name: 'hostname' + }, + { + xtype: 'textfield', + inputType: 'password', + emptyText: gettext("Peer's root password"), + fieldLabel: gettext('Password'), + allowBlank: false, + name: 'password' + } + ], + column2: [ + { + xtype: 'proxmoxNetworkSelector', + fieldLabel: Ext.String.format(gettext('Link {0}'), 0), + bind: { + emptyText: '{ring0EmptyText}', + allowBlank: '{!info.ring0Needed}' + }, + skipEmptyText: true, + autoSelect: false, + valueField: 'address', + displayField: 'address', + name: 'link0' + }, + { + xtype: 'proxmoxNetworkSelector', + fieldLabel: Ext.String.format(gettext('Link {0}'), 1), + skipEmptyText: true, + autoSelect: false, + valueField: 'address', + displayField: 'address', + bind: { + disabled: '{!info.ring1Possible}', + allowBlank: '{!info.ring1Needed}', + }, + name: 'link1' + } + ], + columnB: [ + { + xtype: 'textfield', + fieldLabel: gettext('Fingerprint'), + allowBlank: false, + bind: { + value: '{info.fp}', + readOnly: '{assistedEntry.checked}' + }, + name: 'fingerprint' + } + ] + }] +}); +/* + * Workspace base class + * + * popup login window when auth fails (call onLogin handler) + * update (re-login) ticket every 15 minutes + * + */ + +Ext.define('PVE.Workspace', { + extend: 'Ext.container.Viewport', + + title: 'Proxmox Virtual Environment', + + loginData: null, // Data from last login call + + onLogin: function(loginData) {}, + + // private + updateLoginData: function(loginData) { + var me = this; + me.loginData = loginData; + Proxmox.Utils.setAuthData(loginData); + + var rt = me.down('pveResourceTree'); + rt.setDatacenterText(loginData.clustername); + + if (loginData.cap) { + Ext.state.Manager.set('GuiCap', loginData.cap); + } + me.response401count = 0; + + me.onLogin(loginData); + }, + + // private + showLogin: function() { + var me = this; + + Proxmox.Utils.authClear(); + Proxmox.UserName = null; + me.loginData = null; + + if (!me.login) { + me.login = Ext.create('PVE.window.LoginWindow', { + handler: function(data) { + me.login = null; + me.updateLoginData(data); + Proxmox.Utils.checked_command(function() {}); // display subscription status + } + }); + } + me.onLogin(null); + me.login.show(); + }, + + initComponent : function() { + var me = this; + + Ext.tip.QuickTipManager.init(); + + // fixme: what about other errors + Ext.Ajax.on('requestexception', function(conn, response, options) { + if (response.status == 401 && !PVE.Utils.silenceAuthFailures) { // auth failure + // don't immediately show as logged out to cope better with some big + // upgrades, which may temporarily produce a false positive 401 err + me.response401count++; + if (me.response401count > 5) { + me.showLogin(); + } + } + }); + + me.callParent(); + + if (!Proxmox.Utils.authOK()) { + me.showLogin(); + } else { + if (me.loginData) { + me.onLogin(me.loginData); + } + } + + Ext.TaskManager.start({ + run: function() { + var ticket = Proxmox.Utils.authOK(); + if (!ticket || !Proxmox.UserName) { + return; + } + + Ext.Ajax.request({ + params: { + username: Proxmox.UserName, + password: ticket + }, + url: '/api2/json/access/ticket', + method: 'POST', + success: function(response, opts) { + var obj = Ext.decode(response.responseText); + me.updateLoginData(obj.data); + } + }); + }, + interval: 15*60*1000 + }); + + } +}); + +Ext.define('PVE.StdWorkspace', { + extend: 'PVE.Workspace', + + alias: ['widget.pveStdWorkspace'], + + // private + setContent: function(comp) { + var me = this; + + var cont = me.child('#content'); + + var lay = cont.getLayout(); + + var cur = lay.getActiveItem(); + + if (comp) { + Proxmox.Utils.setErrorMask(cont, false); + comp.border = false; + cont.add(comp); + if (cur !== null && lay.getNext()) { + lay.next(); + var task = Ext.create('Ext.util.DelayedTask', function(){ + cont.remove(cur); + }); + task.delay(10); + } + } + else { + // helper for cleaning the content when logging out + cont.removeAll(); + } + }, + + selectById: function(nodeid) { + var me = this; + var tree = me.down('pveResourceTree'); + tree.selectById(nodeid); + }, + + onLogin: function(loginData) { + var me = this; + + me.updateUserInfo(); + + if (loginData) { + PVE.data.ResourceStore.startUpdate(); + + Proxmox.Utils.API2Request({ + url: '/version', + method: 'GET', + success: function(response) { + PVE.VersionInfo = response.result.data; + me.updateVersionInfo(); + } + }); + } + }, + + updateUserInfo: function() { + var me = this; + var ui = me.query('#userinfo')[0]; + ui.setText(Proxmox.UserName || ''); + ui.updateLayout(); + }, + + updateVersionInfo: function() { + var me = this; + + var ui = me.query('#versioninfo')[0]; + + if (PVE.VersionInfo) { + var version = PVE.VersionInfo.version; + ui.update('Virtual Environment ' + version); + } else { + ui.update('Virtual Environment'); + } + ui.updateLayout(); + }, + + initComponent : function() { + var me = this; + + Ext.History.init(); + + var sprovider = Ext.create('PVE.StateProvider'); + Ext.state.Manager.setProvider(sprovider); + + var selview = Ext.create('PVE.form.ViewSelector'); + + var rtree = Ext.createWidget('pveResourceTree', { + viewFilter: selview.getViewFilter(), + flex: 1, + selModel: { + selType: 'treemodel', + listeners: { + selectionchange: function(sm, selected) { + if (selected.length > 0) { + var n = selected[0]; + var tlckup = { + root: 'PVE.dc.Config', + node: 'PVE.node.Config', + qemu: 'PVE.qemu.Config', + lxc: 'PVE.lxc.Config', + storage: 'PVE.storage.Browser', + pool: 'pvePoolConfig' + }; + var comp = { + xtype: tlckup[n.data.type || 'root'] || + 'pvePanelConfig', + showSearch: (n.data.id === 'root') || + Ext.isDefined(n.data.groupbyid), + pveSelNode: n, + workspace: me, + viewFilter: selview.getViewFilter() + }; + PVE.curSelectedNode = n; + me.setContent(comp); + } + } + } + } + }); + + selview.on('select', function(combo, records) { + if (records) { + var view = combo.getViewFilter(); + rtree.setViewFilter(view); + } + }); + + var caps = sprovider.get('GuiCap'); + + var createVM = Ext.createWidget('button', { + pack: 'end', + margin: '3 5 0 0', + baseCls: 'x-btn', + iconCls: 'fa fa-desktop', + text: gettext("Create VM"), + disabled: !caps.vms['VM.Allocate'], + handler: function() { + var wiz = Ext.create('PVE.qemu.CreateWizard', {}); + wiz.show(); + } + }); + + var createCT = Ext.createWidget('button', { + pack: 'end', + margin: '3 5 0 0', + baseCls: 'x-btn', + iconCls: 'fa fa-cube', + text: gettext("Create CT"), + disabled: !caps.vms['VM.Allocate'], + handler: function() { + var wiz = Ext.create('PVE.lxc.CreateWizard', {}); + wiz.show(); + } + }); + + sprovider.on('statechange', function(sp, key, value) { + if (key === 'GuiCap' && value) { + caps = value; + createVM.setDisabled(!caps.vms['VM.Allocate']); + createCT.setDisabled(!caps.vms['VM.Allocate']); + } + }); + + Ext.apply(me, { + layout: { type: 'border' }, + border: false, + items: [ + { + region: 'north', + layout: { + type: 'hbox', + align: 'middle' + }, + baseCls: 'x-plain', + defaults: { + baseCls: 'x-plain' + }, + border: false, + margin: '2 0 2 5', + items: [ + { + html: '' + + '' + }, + { + minWidth: 150, + id: 'versioninfo', + html: 'Virtual Environment' + }, + { + xtype: 'pveGlobalSearchField', + tree: rtree + }, + { + flex: 1 + }, + { + xtype: 'proxmoxHelpButton', + hidden: false, + baseCls: 'x-btn', + iconCls: 'fa fa-book x-btn-icon-el-default-toolbar-small ', + listenToGlobalEvent: false, + onlineHelp: 'pve_documentation_index', + text: gettext('Documentation'), + margin: '0 5 0 0' + }, + createVM, + createCT, + { + pack: 'end', + margin: '0 5 0 0', + id: 'userinfo', + xtype: 'button', + baseCls: 'x-btn', + style: { + // proxmox dark grey p light grey as border + backgroundColor: '#464d4d', + borderColor: '#ABBABA' + }, + iconCls: 'fa fa-user', + menu: [ + { + iconCls: 'fa fa-gear', + text: gettext('My Settings'), + handler: function() { + var win = Ext.create('PVE.window.Settings'); + win.show(); + } + }, + { + text: gettext('Password'), + iconCls: 'fa fa-fw fa-key', + handler: function() { + var win = Ext.create('Proxmox.window.PasswordEdit', { + userid: Proxmox.UserName + }); + win.show(); + } + }, + { + text: 'TFA', + iconCls: 'fa fa-fw fa-lock', + handler: function(btn, event, rec) { + var win = Ext.create('PVE.window.TFAEdit',{ + userid: Proxmox.UserName + }); + win.show(); + } + }, + '-', + { + iconCls: 'fa fa-fw fa-sign-out', + text: gettext("Logout"), + handler: function() { + PVE.data.ResourceStore.loadData([], false); + me.showLogin(); + me.setContent(null); + var rt = me.down('pveResourceTree'); + rt.setDatacenterText(undefined); + rt.clearTree(); + + // empty the stores of the StatusPanel child items + var statusPanels = Ext.ComponentQuery.query('pveStatusPanel grid'); + Ext.Array.forEach(statusPanels, function(comp) { + if (comp.getStore()) { + comp.getStore().loadData([], false); + } + }); + } + } + ] + } + ] + }, + { + region: 'center', + stateful: true, + stateId: 'pvecenter', + minWidth: 100, + minHeight: 100, + id: 'content', + xtype: 'container', + layout: { type: 'card' }, + border: false, + margin: '0 5 0 0', + items: [] + }, + { + region: 'west', + stateful: true, + stateId: 'pvewest', + itemId: 'west', + xtype: 'container', + border: false, + layout: { type: 'vbox', align: 'stretch' }, + margin: '0 0 0 5', + split: true, + width: 200, + items: [ selview, rtree ], + listeners: { + resize: function(panel, width, height) { + var viewWidth = me.getSize().width; + if (width > viewWidth - 100) { + panel.setWidth(viewWidth - 100); + } + } + } + }, + { + xtype: 'pveStatusPanel', + stateful: true, + stateId: 'pvesouth', + itemId: 'south', + region: 'south', + margin:'0 5 5 5', + title: gettext('Logs'), + collapsible: true, + header: false, + height: 200, + split:true, + listeners: { + resize: function(panel, width, height) { + var viewHeight = me.getSize().height; + if (height > (viewHeight - 150)) { + panel.setHeight(viewHeight - 150); + } + } + } + } + ] + }); + + me.callParent(); + + me.updateUserInfo(); + + // on resize, center all modal windows + Ext.on('resize', function(){ + var wins = Ext.ComponentQuery.query('window[modal]'); + if (wins.length > 0) { + wins.forEach(function(win){ + win.alignTo(me, 'c-c'); + }); + } + }); + } +}); + diff --git a/serverside/jsmod/changes.md b/serverside/jsmod/changes.md index 66403b2..db9fc0c 100644 --- a/serverside/jsmod/changes.md +++ b/serverside/jsmod/changes.md @@ -16,3 +16,16 @@ White gauge text (`line 8935`) * **proxmoxlib.js** Blurple color for gauge filled meter (`line 4462`) Dark color for gauge meter background (`line 4463`) + +## 6.0-4 +* **pvemanagerlib.js** +`background-color` to `#23272a` for node summary background (`line 18019`) +`background-color` to `#23272a` for TFA QR code background (`line 35826`) +`background-color` to `#23272a` for S.M.A.R.T disk report (`line 16729`) +`background-color` to `#23272a` for system report (`line 18203`) +* **charts.js** +Remains roughly the same as 5-4.3 +* **proxmoxlib.js** +`defaultColor` to `#7289DA` (`line 5084`) +`backgroundColor` to `#2C2F33` (`line 5085`) +