From 3ea11b53c08b341d197c819d9d09d673732a53b3 Mon Sep 17 00:00:00 2001
From: Weilbyte <git@weilbyte.net>
Date: Wed, 23 Oct 2019 16:25:39 +0200
Subject: [PATCH] JSMod for 6.0-4

---
 serverside/jsmod/6.0-4/charts.js              | 22013 +++++++++
 serverside/jsmod/6.0-4/charts.js.original     |     1 +
 serverside/jsmod/6.0-4/proxmoxlib.js          |  7357 +++
 serverside/jsmod/6.0-4/proxmoxlib.js.original |  7357 +++
 serverside/jsmod/6.0-4/pvemanagerlib.js       | 39779 ++++++++++++++++
 .../jsmod/6.0-4/pvemanagerlib.js.original     | 39779 ++++++++++++++++
 serverside/jsmod/changes.md                   |    13 +
 7 files changed, 116299 insertions(+)
 create mode 100644 serverside/jsmod/6.0-4/charts.js
 create mode 100644 serverside/jsmod/6.0-4/charts.js.original
 create mode 100644 serverside/jsmod/6.0-4/proxmoxlib.js
 create mode 100644 serverside/jsmod/6.0-4/proxmoxlib.js.original
 create mode 100644 serverside/jsmod/6.0-4/pvemanagerlib.js
 create mode 100644 serverside/jsmod/6.0-4/pvemanagerlib.js.original

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 = '<?xml version="1.0" standalone="yes"?>',
+            f = Ext.getClassName(this),
+            a, g, d;
+        c += '<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" width="' + e.width + '" height="' + e.height + '">';
+        for (d = 0; d < b.length; d++) {
+            a = b[d];
+            if (Ext.getClassName(a) !== f) {
+                continue
+            }
+            g = a.getRect();
+            c += '<g transform="translate(' + g[0] + "," + g[1] + ')">';
+            c += this.serializeNode(a.svgElement.dom);
+            c += "</g>"
+        }
+        c += "</svg>";
+        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 += "</" + d.nodeName + ">";
+        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, "&amp;").replace(/"/g, "&quot;")
+        }
+
+        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(" <g_vml_:group", ' coordsize="', d * i, ",", d * p, '"', ' coordorigin="0,0"', ' style="width:', n(i * ap[0][0]), "px;height:", n(p * ap[1][1]), "px;position:absolute;", "top:", n(au.y / d), "px;left:", n(au.x / d), "px; rotation:", n(Math.atan(ap[0][1] / ap[1][1]) * 180 / Math.PI), ";");
+            at.push('" >', '<g_vml_:image src="', an.src, '"', ' style="width:', d * aj, "px;", " height:", d * ar, 'px"', ' cropleft="', al / ag, '"', ' croptop="', ak / aq, '"', ' cropright="', (ag - al - ao) / ag, '"', ' cropbottom="', (aq - ak - av) / aq, '"', " />", "</g_vml_:group>");
+            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("<g_vml_:shape", ' filled="', !!ak, '"', ' style="position:absolute;width:', m, "px;height:", al, 'px;left:0px;top:0px;"', ' coordorigin="0,0"', ' coordsize="', d * m, ",", d * al, '"', ' stroked="', !ak, '"', ' path="');
+            var Z = {
+                x: null,
+                y: null
+            };
+            var aj = {
+                x: null,
+                y: null
+            };
+            for (var ag = 0; ag < this.currentPath_.length; ag++) {
+                var j = this.currentPath_[ag];
+                var ah;
+                switch (j.type) {
+                    case "moveTo":
+                        ah = j;
+                        ai.push(" m ", n(j.x), ",", n(j.y));
+                        break;
+                    case "lineTo":
+                        ai.push(" l ", n(j.x), ",", n(j.y));
+                        break;
+                    case "close":
+                        ai.push(" x ");
+                        j = null;
+                        break;
+                    case "bezierCurveTo":
+                        ai.push(" c ", n(j.cp1x), ",", n(j.cp1y), ",", n(j.cp2x), ",", n(j.cp2y), ",", n(j.x), ",", n(j.y));
+                        break;
+                    case "at":
+                    case "wa":
+                        ai.push(" ", j.type, " ", n(j.x - this.arcScaleX_ * j.radius), ",", n(j.y - this.arcScaleY_ * j.radius), " ", n(j.x + this.arcScaleX_ * j.radius), ",", n(j.y + this.arcScaleY_ * j.radius), " ", n(j.xStart), ",", n(j.yStart), " ", n(j.xEnd), ",", n(j.yEnd));
+                        break
+                }
+                if (j) {
+                    if (Z.x == null || j.x < Z.x) {
+                        Z.x = j.x
+                    }
+                    if (aj.x == null || j.x > 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("</g_vml_:shape>");
+            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("<g_vml_:stroke", ' opacity="', Z, '"', ' joinstyle="', m.lineJoin, '"', ' dashstyle="', m.lineDash.join(" "), '"', ' miterlimit="', m.miterLimit, '"', ' endcap="', S(m.lineCap), '"', ' weight="', i, 'px"', ' color="', p, '" />')
+        }
+
+        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('<g_vml_:fill type="', aj.type_, '"', ' method="none" focus="100%"', ' color="', au, '"', ' color2="', at, '"', ' colors="', aE.join(","), '"', ' opacity="', ay, '"', ' g_o_:opacity2="', az, '"', ' angle="', an, '"', ' focusposition="', aF.x, ",", aF.y, '" />')
+            } else {
+                if (aj instanceof T) {
+                    if (j && p) {
+                        var ah = -aK.x;
+                        var aC = -aK.y;
+                        ai.push("<g_vml_:fill", ' position="', ah / j * aB * aB, ",", aC / p * aA * aA, '"', ' type="tile"', ' src="', aj.src_, '" />')
+                    }
+                } else {
+                    var aL = F(aq.fillStyle);
+                    var aw = aL.color;
+                    var aG = aL.alpha * aq.globalAlpha;
+                    ai.push('<g_vml_:fill color="', aw, '" opacity="', aG, '" />')
+                }
+            }
+        }
+        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('<g_vml_:line from="', -j, ' 0" to="', ar, ' 0.05" ', ' coordsize="100 100" coordorigin="0 0"', ' filled="', !ai, '" stroked="', !!ai, '" style="position:absolute;width:1px;height:1px;left:0px;top:0px;">');
+            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('<g_vml_:skew on="t" matrix="', an, '" ', ' offset="', al, '" origin="', j, ' 0" />', '<g_vml_:path textpathok="true" />', '<g_vml_:textpath on="true" string="', af(am), '" style="v-text-align:', Z, ";font:", af(p), '" /></g_vml_:line>');
+            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 = '<span style="position:absolute;top:-20000px;left:0;padding:0;margin:0;border:none;white-space:pre;"></span>';
+                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: ['<div class="', Ext.baseCSSPrefix, 'legend-container">', '<tpl for=".">', '<div class="', Ext.baseCSSPrefix, 'legend-item">', "<span ", 'class="', Ext.baseCSSPrefix, "legend-item-marker {[ values.disabled ? Ext.baseCSSPrefix + 'legend-inactive' : '' ]}\" ", 'style="background:{mark};">', "</span>{name}", "</div>", "</tpl>", "</div>"],
+        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("<br>"))
+    },
+    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<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='<?xml version="1.0" standalone="yes"?>',f=Ext.getClassName(this),a,g,d;c+='<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" width="'+e.width+'" height="'+e.height+'">';for(d=0;d<b.length;d++){a=b[d];if(Ext.getClassName(a)!==f){continue}g=a.getRect();c+='<g transform="translate('+g[0]+","+g[1]+')">';c+=this.serializeNode(a.svgElement.dom);c+="</g>"}c+="</svg>";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+="</"+d.nodeName+">";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,"&amp;").replace(/"/g,"&quot;")}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(" <g_vml_:group",' coordsize="',d*i,",",d*p,'"',' coordorigin="0,0"',' style="width:',n(i*ap[0][0]),"px;height:",n(p*ap[1][1]),"px;position:absolute;","top:",n(au.y/d),"px;left:",n(au.x/d),"px; rotation:",n(Math.atan(ap[0][1]/ap[1][1])*180/Math.PI),";");at.push('" >','<g_vml_:image src="',an.src,'"',' style="width:',d*aj,"px;"," height:",d*ar,'px"',' cropleft="',al/ag,'"',' croptop="',ak/aq,'"',' cropright="',(ag-al-ao)/ag,'"',' cropbottom="',(aq-ak-av)/aq,'"'," />","</g_vml_:group>");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("<g_vml_:shape",' filled="',!!ak,'"',' style="position:absolute;width:',m,"px;height:",al,'px;left:0px;top:0px;"',' coordorigin="0,0"',' coordsize="',d*m,",",d*al,'"',' stroked="',!ak,'"',' path="');var Z={x:null,y:null};var aj={x:null,y:null};for(var ag=0;ag<this.currentPath_.length;ag++){var j=this.currentPath_[ag];var ah;switch(j.type){case"moveTo":ah=j;ai.push(" m ",n(j.x),",",n(j.y));break;case"lineTo":ai.push(" l ",n(j.x),",",n(j.y));break;case"close":ai.push(" x ");j=null;break;case"bezierCurveTo":ai.push(" c ",n(j.cp1x),",",n(j.cp1y),",",n(j.cp2x),",",n(j.cp2y),",",n(j.x),",",n(j.y));break;case"at":case"wa":ai.push(" ",j.type," ",n(j.x-this.arcScaleX_*j.radius),",",n(j.y-this.arcScaleY_*j.radius)," ",n(j.x+this.arcScaleX_*j.radius),",",n(j.y+this.arcScaleY_*j.radius)," ",n(j.xStart),",",n(j.yStart)," ",n(j.xEnd),",",n(j.yEnd));break}if(j){if(Z.x==null||j.x<Z.x){Z.x=j.x}if(aj.x==null||j.x>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("</g_vml_:shape>");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("<g_vml_:stroke",' opacity="',Z,'"',' joinstyle="',m.lineJoin,'"',' dashstyle="',m.lineDash.join(" "),'"',' miterlimit="',m.miterLimit,'"',' endcap="',S(m.lineCap),'"',' weight="',i,'px"',' color="',p,'" />')}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('<g_vml_:fill type="',aj.type_,'"',' method="none" focus="100%"',' color="',au,'"',' color2="',at,'"',' colors="',aE.join(","),'"',' opacity="',ay,'"',' g_o_:opacity2="',az,'"',' angle="',an,'"',' focusposition="',aF.x,",",aF.y,'" />')}else{if(aj instanceof T){if(j&&p){var ah=-aK.x;var aC=-aK.y;ai.push("<g_vml_:fill",' position="',ah/j*aB*aB,",",aC/p*aA*aA,'"',' type="tile"',' src="',aj.src_,'" />')}}else{var aL=F(aq.fillStyle);var aw=aL.color;var aG=aL.alpha*aq.globalAlpha;ai.push('<g_vml_:fill color="',aw,'" opacity="',aG,'" />')}}}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('<g_vml_:line from="',-j,' 0" to="',ar,' 0.05" ',' coordsize="100 100" coordorigin="0 0"',' filled="',!ai,'" stroked="',!!ai,'" style="position:absolute;width:1px;height:1px;left:0px;top:0px;">');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('<g_vml_:skew on="t" matrix="',an,'" ',' offset="',al,'" origin="',j,' 0" />','<g_vml_:path textpathok="true" />','<g_vml_:textpath on="true" string="',af(am),'" style="v-text-align:',Z,";font:",af(p),'" /></g_vml_:line>');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='<span style="position:absolute;top:-20000px;left:0;padding:0;margin:0;border:none;white-space:pre;"></span>';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:"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;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:['<div class="',Ext.baseCSSPrefix,'legend-container">','<tpl for=".">','<div class="',Ext.baseCSSPrefix,'legend-item">',"<span ",'class="',Ext.baseCSSPrefix,"legend-item-marker {[ values.disabled ? Ext.baseCSSPrefix + 'legend-inactive' : '' ]}\" ",'style="background:{mark};">',"</span>{name}","</div>","</tpl>","</div>"],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("<br>"))},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/proxmoxlib.js b/serverside/jsmod/6.0-4/proxmoxlib.js
new file mode 100644
index 0000000..cb8b6d1
--- /dev/null
+++ b/serverside/jsmod/6.0-4/proxmoxlib.js
@@ -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 <a target="_blank" href="{0}">www.proxmox.com</a> 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 += "<br>";
+		Ext.Object.each(result.errors, function(prop, desc) {
+		    msg += "<br><b>" + Ext.htmlEncode(prop) + "</b>: " +
+			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' + "<br>" + 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' + "<br>" + 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') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999',
+
+    BondName: function(v) {
+        return (/^bond\d{1,4}$/).test(v);
+    },
+    BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
+
+    InterfaceName: function(v) {
+        return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
+    },
+    InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'" + "<br />" +
+		       gettext("Minimum characters") + ": 2" + "<br />" +
+		       gettext("Maximum characters") + ": 21" + "<br />" +
+		       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', '-', '_', '.'" + "<br />" +
+		   gettext("Minimum characters") + ": 2" + "<br />" +
+		   gettext("Must start with") + ": 'A-Z', 'a-z'<br />" +
+		   gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'<br />",
+
+    ConfigId:  function(v) {
+        return (/^[a-z][a-z0-9\_]+$/i).test(v);
+    },
+    ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'" + "<br />" +
+		  gettext("Minimum characters") + ": 2" + "<br />" +
+		  gettext("Must start with") + ": " + gettext("letter"),
+
+    HttpProxy:  function(v) {
+        return (/^http:\/\/.*$/).test(v);
+    },
+    HttpProxyText: gettext('Example') + ": http://username:password&#64;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);
+            //<debug>
+            // tell the spec runner to ignore this element when checking if the dom is clean
+	    el.dom.setAttribute('data-sticky', true);
+            //</debug>
+	}
+
+        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: [
+        '<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>'
+    ],
+    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 = '<div style="text-decoration: line-through;">'+ current +'</div>';
+	    }
+	}
+
+	if (pending) {
+	    return current + '<div style="color:red">' + pending + '</div>';
+	} 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('<br>'), 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('<br>');
+
+	    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 + '<br>' + view.content : text;
+		} else if (!top && num) {
+		    view.content = view.content ? view.content + '<br>' + 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 +
+			    '<br>' + 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: '<h3>{title}</h3>'
+	},
+	{
+	    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>
+    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: '<div style="padding: 1em">' +
+			Ext.String.htmlEncode(data.Description) +
+			'</div>',
+		    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: '<div style="display:table; width:100%; height:100%;"><div style="display:table-cell; vertical-align: middle; text-align:center;"><b>' + gettext('No updates available.') + '</div></div>'
+	    },
+	    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("<pre>" + Ext.htmlEncode(changes) + "</pre>");
+			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('<br>') || '';
+	    };
+	};
+
+	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 <a target="_blank" href="{0}">www.proxmox.com</a> 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 += "<br>";
+		Ext.Object.each(result.errors, function(prop, desc) {
+		    msg += "<br><b>" + Ext.htmlEncode(prop) + "</b>: " +
+			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' + "<br>" + 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' + "<br>" + 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') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999',
+
+    BondName: function(v) {
+        return (/^bond\d{1,4}$/).test(v);
+    },
+    BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
+
+    InterfaceName: function(v) {
+        return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
+    },
+    InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'" + "<br />" +
+		       gettext("Minimum characters") + ": 2" + "<br />" +
+		       gettext("Maximum characters") + ": 21" + "<br />" +
+		       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', '-', '_', '.'" + "<br />" +
+		   gettext("Minimum characters") + ": 2" + "<br />" +
+		   gettext("Must start with") + ": 'A-Z', 'a-z'<br />" +
+		   gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'<br />",
+
+    ConfigId:  function(v) {
+        return (/^[a-z][a-z0-9\_]+$/i).test(v);
+    },
+    ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'" + "<br />" +
+		  gettext("Minimum characters") + ": 2" + "<br />" +
+		  gettext("Must start with") + ": " + gettext("letter"),
+
+    HttpProxy:  function(v) {
+        return (/^http:\/\/.*$/).test(v);
+    },
+    HttpProxyText: gettext('Example') + ": http://username:password&#64;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);
+            //<debug>
+            // tell the spec runner to ignore this element when checking if the dom is clean
+	    el.dom.setAttribute('data-sticky', true);
+            //</debug>
+	}
+
+        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: [
+        '<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>'
+    ],
+    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 = '<div style="text-decoration: line-through;">'+ current +'</div>';
+	    }
+	}
+
+	if (pending) {
+	    return current + '<div style="color:red">' + pending + '</div>';
+	} 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('<br>'), 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('<br>');
+
+	    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 + '<br>' + view.content : text;
+		} else if (!top && num) {
+		    view.content = view.content ? view.content + '<br>' + 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 +
+			    '<br>' + 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: '<h3>{title}</h3>'
+	},
+	{
+	    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>
+    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: '<div style="padding: 1em">' +
+			Ext.String.htmlEncode(data.Description) +
+			'</div>',
+		    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: '<div style="display:table; width:100%; height:100%;"><div style="display:table-cell; vertical-align: middle; text-align:center;"><b>' + gettext('No updates available.') + '</div></div>'
+	    },
+	    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("<pre>" + Ext.htmlEncode(changes) + "</pre>");
+			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('<br>') || '';
+	    };
+	};
+
+	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 <a target="_blank" href="https://www.proxmox.com/products/proxmox-ve/subscription-service-plans">www.proxmox.com</a> 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 "<i class='fa " + cls + "'></i> ";
+    },
+
+    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 '<i class="fa fa-' + iconCls + '"></i> ' + 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 = '<i class="fa-fw x-grid-icon-custom ' + cls  + '"></i> ';
+	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 '<a target="_blank" href="' + value + '">' + value + '</a>';
+	}
+	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('<br>');
+	}
+	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('<br>');
+		}
+	    }
+	]
+    },
+
+    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: [
+	'<ul class="x-list-plain"><tpl for=".">',
+	    '<li role="option" class="x-boundlist-item">{text}</li>',
+	'</tpl></ul>'
+    ],
+
+    displayTpl: [
+	'<tpl for=".">',
+	'{value}',
+	'</tpl>'
+    ]
+
+});
+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: '<h3>{title}:</h3>'
+	},
+	{
+	    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 + '<br />' +
+			    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: [
+		'<div class="left-aligned">',
+		'<tpl if="iconCls">',
+		'<i class="{iconCls}"></i> ',
+		'</tpl>',
+		'{title}</div>&nbsp;<div class="right-aligned">{usage}</div>'
+	    ]
+	},
+	{
+	    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: [
+	'<h3>{title}</h3>',
+	'<i class="fa fa-5x {iconCls}"></i>',
+	'<br /><br/>',
+	'{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 '<p class="install-mask">' +
+		    Ext.String.format(gettext('{0} is not initialized.'), 'Ceph') + ' '+
+		    gettext('You need to create a initial config once.') + '</p>';
+		} else {
+		    return '<p class="install-mask">' +
+		    Ext.String.format(gettext('{0} is not installed on this node.'), 'Ceph') + '<br>' +
+		    gettext('Would you like to install it now?') + '</p>';
+		}
+	    }
+	}
+    },
+    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: '<div style="margin: auto; padding: 2.5px;"><b>/</b></div>'
+		},
+		{
+		    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 = '<p>' +  Ext.htmlEncode(errors[name]) + '</p>';
+		metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + 
+		    html.replace(/\"/g,'&quot;') + '"';
+	    }
+	    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 = '<div class="usage-wrapper">';
+		status += '<div class="usage-negative" style="height: ';
+		status += neg_height + '%"></div>';
+		status += '<div class="usage" style="height: '+ height +'%"></div>';
+		status += '</div> ';
+	    }
+	}
+
+	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: [ '<b>IPSet:</b>', 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 = '<p>' +  Ext.htmlEncode(msg) + '</p>';
+		    metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + 
+			html.replace(/\"/g,'&quot;') + '"';
+		}
+	    }
+	    return value;
+	};
+
+	Ext.apply(me, {
+	    tbar: [ '<b>IP/CIDR:</b>', 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 '<b>! </b>' + 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 ' +
+			   '<a target="_blank" href="' + Proxmox.Utils.get_help_link('chapter_pveceph') + '">the reference documentation</a>.',
+		}
+	    ]
+	});
+
+	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) +
+			   "<br>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 + ' <i class="fa ' + updownicon + '"></i> / ' +
+		inout + ' <i class="fa ' + inouticon + '"></i>';
+
+	    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<br>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: [
+		    '<tpl if="osd">',
+		    'osd.{osd}:',
+		    '<tpl else>',
+		    gettext('No OSD selected'),
+		    '</tpl>'
+		]
+	    },
+	    {
+		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 '<i class="fa fa-fw ' + classes + '"></i>';
+			    },
+			    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: [
+						    '<span>' + Ext.htmlEncode(record.data.summary) + '</span>',
+						    '<pre>' + Ext.htmlEncode(record.data.detail) + '</pre>'
+						]
+					    }]
+					});
+					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: [
+	    '<h3>' + 'OSDs' + '</h3>',
+	    '<table class="osds">',
+	    '<tr><td></td>',
+	    '<td><i class="fa fa-fw good fa-circle"></i>',
+	    gettext('In'),
+	    '</td>',
+	    '<td><i class="fa fa-fw warning fa-circle-o"></i>',
+	    gettext('Out'),
+	    '</td>',
+	    '</tr>',
+	    '<tr>',
+	    '<td><i class="fa fa-fw good fa-arrow-circle-up"></i>',
+	    gettext('Up'),
+	    '</td>',
+	    '<td>{upin}</td>',
+	    '<td>{upout}</td>',
+	    '</tr>',
+	    '<tr>',
+	    '<td><i class="fa fa-fw critical fa-arrow-circle-down"></i>',
+	    gettext('Down'),
+	    '</td>',
+	    '<td>{downin}</td>',
+	    '<td>{downout}</td>',
+	    '</tr>',
+	    '</table>',
+	    '<br /><div>',
+	    gettext('Total'),
+	    ': {total}',
+	    '</div><br />',
+	    '<tpl if="oldosds.length &gt; 0">',
+	    '<i class="fa fa-refresh warning"></i> ' + gettext('Outdated OSDs') + "<br>",
+	    '<div class="osds">',
+	    '<tpl for="oldosds">',
+	    '<div class="left-aligned">osd.{id}:</div>',
+	    '<div class="right-aligned">{version}</div><br />',
+	    '<div style="clear:both"></div>',
+	    '</tpl>',
+	    '</div>',
+	    '</tpl>'
+	]
+    },
+    {
+	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 += '<br>';
+			record.get('states').forEach(function(state) {
+			    html += '<br>' +
+				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: [
+	    '<h3>' + 'PGs' + '</h3>',
+	    '<tpl for="states">',
+	    '<div class="left-aligned"><i class ="fa fa-circle {cls}"></i> {state_name}:</div>',
+	    '<div class="right-aligned">{count}</div><br />',
+	    '<div style="clear:both"></div>',
+	    '</tpl>'
+	]
+    }],
+
+    // 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 = '<b>' + mgr.title + '</b>';
+		    mgr.statuses.push(gettext('Status') + ': <b>active</b>');
+		} 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 = '<b>' + mds.title + '</b>';
+		    mds.statuses.push(gettext('Status') + ': <b>' + mdsmessages[mds.name]+"</b>");
+		} 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('<br>');
+
+		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: '<h3>{title}</h3>'
+	}
+    ],
+
+    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}: ',
+	'<i class="fa fa-fw {iconCls}"></i>'
+    ],
+
+    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: '<h3>Ceph?</h3>'+
+		'<blockquote cite="https://ceph.com/"><p>"<b>Ceph</b> is a unified, distributed storage system designed for excellent performance, reliability and scalability."</p></blockquote>'+
+		'<p><b>Ceph</b> is currently <b>not installed</b> 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.</p>'+
+		'<p>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 <a target="_blank" href="http://docs.ceph.com/docs/master/">ceph.com</a>.</p>',
+		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: '<h3>Installation successful!</h3>'+
+		'<p>The basic installation and configuration is completed, depending on your setup some of the following steps are required to start using Ceph:</p>'+
+		    '<ol><li>Install Ceph on other nodes</li>'+
+		    '<li>Create additional Ceph Monitors</li>'+
+		    '<li>Create Ceph OSDs</li>'+
+		    '<li>Create Ceph Pools</li></ol>'+
+		'<p>To learn more click on the help button below.</p>',
+		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 ' +
+			   '<a target="_blank" href="' + Proxmox.Utils.get_help_link('chapter_zfs') + '">the reference documentation</a>.',
+		}
+	    ]
+	});
+
+        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('<br>');
+		    }
+		    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 '<i class="fa fa-exclamation-triangle warning"></i> ';
+			case 'error':
+			    return '<i class="fa fa-times critical"></i>';
+			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('<pre>' + lines.join('\n') + '</pre>');
+	    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 = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
+	    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: [
+		'<tpl if="lock">',
+		'<i class="fa fa-lg fa-lock"></i> ({lock})',
+		'</tpl>'
+	    ]
+	});
+
+	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('<br>');
+		    }
+		}
+	    ]
+	}
+    ]
+});
+
+Ext.define('PVE.qemu.AgentIPView', {
+    extend: 'Ext.container.Container',
+    xtype: 'pveAgentIPView',
+
+    layout: {
+	type: 'hbox',
+	align: 'top'
+    },
+
+    nics: [],
+
+    items: [
+	{
+	    xtype: 'box',
+	    html: '<i class="fa fa-exchange"></i> 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('<br>');
+		}
+	    } else if (me.nics && me.nics.error) {
+		var msg = gettext('Cannot get info from Guest Agent<br>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 = '<i class="' + rowdef.iconCls + '"></i> ';
+	}
+	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 += ' <span style="color:gray">(' + gettext('with options') + ')</span>';
+				}
+				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('<br>');
+		    } 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 + "<br>" + 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 + "<br>" + 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: [
+		'<tpl if="lock">',
+		'<i class="fa fa-lg fa-lock"></i> ({lock})',
+		'</tpl>'
+	    ]
+	});
+
+
+	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 += '<p>' + gettext('Note: Rollback stops CT') + '</p>';
+
+		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 += '<br />' + 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 = '<p>' +  Ext.htmlEncode(msg) + '</p>';
+		    metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + 
+			html.replace(/\"/g,'&quot;') + '"';
+		}
+	    }
+	    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('<i class="fa fa-ban warning" title="'
+					+ gettext("Removal Scheduled") + '"></i>');
+			    states.push(gettext("Removal Scheduled"));
+			}
+
+			if (record.data.error) {
+			    icons.push('<i class="fa fa-times critical" title="'
+					+ gettext("Error") + '"></i>');
+			    states.push(record.data.error);
+			}
+
+			if (icons.length == 0) {
+			    icons.push('<i class="fa fa-check good"></i>');
+			    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: [
+		'<h3>' + gettext('Nodes') + '</h3><br />',
+		'<div style="width: 150px;margin: auto;font-size: 12pt">',
+		'<div class="left-aligned">',
+		'<i class="good fa fa-fw fa-check">&nbsp;</i>',
+		gettext('Online'),
+		'</div>',
+		'<div class="right-aligned">{online}</div>',
+		'<br /><br />',
+		'<div class="left-aligned">',
+		'<i class="critical fa fa-fw fa-times">&nbsp;</i>',
+		gettext('Offline'),
+		'</div>',
+		'<div class="right-aligned">{offline}</div>',
+		'</div>'
+	    ]
+	},
+	{
+	    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: [
+	    '<h3>' + gettext("Virtual Machines") + '</h3>',
+	    '<div class="left-aligned">',
+		'<i class="good fa fa-fw fa-play-circle">&nbsp;</i>',
+		gettext('Running'),
+	    '</div>',
+	    '<div class="right-aligned">{running}</div>' + '<br />',
+	    '<tpl if="paused &gt; 0">',
+		'<div class="left-aligned">',
+		    '<i class="warning fa fa-fw fa-pause-circle">&nbsp;</i>',
+		    gettext('Paused'),
+		'</div>',
+		'<div class="right-aligned">{paused}</div>' + '<br />',
+	    '</tpl>',
+	    '<div class="left-aligned">',
+		'<i class="faded fa fa-fw fa-stop-circle">&nbsp;</i>',
+		gettext('Stopped'),
+	    '</div>',
+	    '<div class="right-aligned">{stopped}</div>' + '<br />',
+	    '<tpl if="template &gt; 0">',
+		'<div class="left-aligned">',
+		    '<i class="fa fa-fw fa-circle-o">&nbsp;</i>',
+		    gettext('Templates'),
+		'</div>',
+		'<div class="right-aligned">{template}</div>',
+	    '</tpl>'
+	]
+    },{
+	itemId: 'lxc',
+	data: {
+	    running: 0,
+	    paused: 0,
+	    stopped: 0,
+	    template: 0
+	},
+	tpl: [
+	    '<h3>' + gettext("LXC Container") + '</h3>',
+	    '<div class="left-aligned">',
+		'<i class="good fa fa-fw fa-play-circle">&nbsp;</i>',
+		gettext('Running'),
+	    '</div>',
+	    '<div class="right-aligned">{running}</div>' + '<br />',
+	    '<tpl if="paused &gt; 0">',
+		'<div class="left-aligned">',
+		    '<i class="warning fa fa-fw fa-pause-circle">&nbsp;</i>',
+		    gettext('Paused'),
+		'</div>',
+		'<div class="right-aligned">{paused}</div>' + '<br />',
+	    '</tpl>',
+	    '<div class="left-aligned">',
+		'<i class="faded fa fa-fw fa-stop-circle">&nbsp;</i>',
+		gettext('Stopped'),
+	    '</div>',
+	    '<div class="right-aligned">{stopped}</div>' + '<br />',
+	    '<tpl if="template &gt; 0">',
+		'<div class="left-aligned">',
+		    '<i class="fa fa-fw fa-circle-o">&nbsp;</i>',
+		    gettext('Templates'),
+		'</div>',
+		'<div class="right-aligned">{template}</div>',
+	    '</tpl>'
+	]
+    },{
+	itemId: 'error',
+	colspan: 2,
+	data: {
+	    num: 0
+	},
+	columnWidth: 1,
+	padding: '10 250 0 250',
+	tpl: [
+	    '<tpl if="num &gt; 0">',
+		'<div class="left-aligned">',
+		    '<i class="critical fa fa-fw fa-times-circle">&nbsp;</i>',
+		    gettext('Error'),
+		'</div>',
+		'<div class="right-aligned">{num}</div>',
+	    '</tpl>'
+	]
+    }],
+
+    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: '<h1>No valid subscription</h1>' + PVE.Utils.noSubKeyHtml,
+
+    communityHtml: 'Please use the public community <a target="_blank" href="https://forum.proxmox.com">forum</a> for any questions.',
+
+    activeHtml: 'Please use our <a target="_blank" href="https://my.proxmox.com">support portal</a> for any questions. You can also use the public community <a target="_blank" href="https://forum.proxmox.com">forum</a> to get additional information.',
+
+    bugzillaHtml: '<h1>Bug Tracking</h1>Our bug tracking system is available <a target="_blank" href="https://bugzilla.proxmox.com">here</a>.',
+
+    docuHtml: function() {
+	var me = this;
+	var guideUrl = window.location.origin + me.pveGuidePath;
+	var text = Ext.String.format('<h1>Documentation</h1>'
+	+ 'The official Proxmox VE Administration Guide'
+	+ ' is included with this installation and can be browsed at '
+	+ '<a target="_blank" href="{0}">{0}</a>', guideUrl);
+	return text;
+    },
+
+    updateActive: function(data) {
+	var me = this;
+	
+	var html = '<h1>' + data.productname + '</h1>' + me.activeHtml; 
+	html += '<br><br>' + me.docuHtml();
+	html += '<br><br>' + me.bugzillaHtml;
+
+	me.update(html);
+    },
+
+    updateCommunity: function(data) {
+	var me = this;
+
+	var html = '<h1>' + data.productname + '</h1>' + me.communityHtml; 
+	html += '<br><br>' + me.docuHtml();
+	html += '<br><br>' + 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: [ '<b>' + gettext('Group') + ':</b>', 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: '<b>' + gettext('Rules') + ':</b>',
+	    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  '<i class="fa ' + PVE.Utils.get_health_icon(cls) + '"><i/>';
+	    }
+	},
+	{
+	    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: '<a class="x-unselectable" target=_blank href="https://www.proxmox.com">' +
+				'<img style="padding-top:4px;padding-right:5px" src="/pve2/images/proxmox_logo.png"/></a>'
+			},
+			{
+			    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 <a target="_blank" href="https://www.proxmox.com/products/proxmox-ve/subscription-service-plans">www.proxmox.com</a> 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 "<i class='fa " + cls + "'></i> ";
+    },
+
+    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 '<i class="fa fa-' + iconCls + '"></i> ' + 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 = '<i class="fa-fw x-grid-icon-custom ' + cls  + '"></i> ';
+	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 '<a target="_blank" href="' + value + '">' + value + '</a>';
+	}
+	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('<br>');
+	}
+	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('<br>');
+		}
+	    }
+	]
+    },
+
+    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: [
+	'<ul class="x-list-plain"><tpl for=".">',
+	    '<li role="option" class="x-boundlist-item">{text}</li>',
+	'</tpl></ul>'
+    ],
+
+    displayTpl: [
+	'<tpl for=".">',
+	'{value}',
+	'</tpl>'
+    ]
+
+});
+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: '<h3>{title}:</h3>'
+	},
+	{
+	    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 + '<br />' +
+			    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: [
+		'<div class="left-aligned">',
+		'<tpl if="iconCls">',
+		'<i class="{iconCls}"></i> ',
+		'</tpl>',
+		'{title}</div>&nbsp;<div class="right-aligned">{usage}</div>'
+	    ]
+	},
+	{
+	    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: [
+	'<h3>{title}</h3>',
+	'<i class="fa fa-5x {iconCls}"></i>',
+	'<br /><br/>',
+	'{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 '<p class="install-mask">' +
+		    Ext.String.format(gettext('{0} is not initialized.'), 'Ceph') + ' '+
+		    gettext('You need to create a initial config once.') + '</p>';
+		} else {
+		    return '<p class="install-mask">' +
+		    Ext.String.format(gettext('{0} is not installed on this node.'), 'Ceph') + '<br>' +
+		    gettext('Would you like to install it now?') + '</p>';
+		}
+	    }
+	}
+    },
+    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: '<div style="margin: auto; padding: 2.5px;"><b>/</b></div>'
+		},
+		{
+		    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 = '<p>' +  Ext.htmlEncode(errors[name]) + '</p>';
+		metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + 
+		    html.replace(/\"/g,'&quot;') + '"';
+	    }
+	    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 = '<div class="usage-wrapper">';
+		status += '<div class="usage-negative" style="height: ';
+		status += neg_height + '%"></div>';
+		status += '<div class="usage" style="height: '+ height +'%"></div>';
+		status += '</div> ';
+	    }
+	}
+
+	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: [ '<b>IPSet:</b>', 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 = '<p>' +  Ext.htmlEncode(msg) + '</p>';
+		    metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + 
+			html.replace(/\"/g,'&quot;') + '"';
+		}
+	    }
+	    return value;
+	};
+
+	Ext.apply(me, {
+	    tbar: [ '<b>IP/CIDR:</b>', 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 '<b>! </b>' + 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 ' +
+			   '<a target="_blank" href="' + Proxmox.Utils.get_help_link('chapter_pveceph') + '">the reference documentation</a>.',
+		}
+	    ]
+	});
+
+	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) +
+			   "<br>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 + ' <i class="fa ' + updownicon + '"></i> / ' +
+		inout + ' <i class="fa ' + inouticon + '"></i>';
+
+	    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<br>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: [
+		    '<tpl if="osd">',
+		    'osd.{osd}:',
+		    '<tpl else>',
+		    gettext('No OSD selected'),
+		    '</tpl>'
+		]
+	    },
+	    {
+		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 '<i class="fa fa-fw ' + classes + '"></i>';
+			    },
+			    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: [
+						    '<span>' + Ext.htmlEncode(record.data.summary) + '</span>',
+						    '<pre>' + Ext.htmlEncode(record.data.detail) + '</pre>'
+						]
+					    }]
+					});
+					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: [
+	    '<h3>' + 'OSDs' + '</h3>',
+	    '<table class="osds">',
+	    '<tr><td></td>',
+	    '<td><i class="fa fa-fw good fa-circle"></i>',
+	    gettext('In'),
+	    '</td>',
+	    '<td><i class="fa fa-fw warning fa-circle-o"></i>',
+	    gettext('Out'),
+	    '</td>',
+	    '</tr>',
+	    '<tr>',
+	    '<td><i class="fa fa-fw good fa-arrow-circle-up"></i>',
+	    gettext('Up'),
+	    '</td>',
+	    '<td>{upin}</td>',
+	    '<td>{upout}</td>',
+	    '</tr>',
+	    '<tr>',
+	    '<td><i class="fa fa-fw critical fa-arrow-circle-down"></i>',
+	    gettext('Down'),
+	    '</td>',
+	    '<td>{downin}</td>',
+	    '<td>{downout}</td>',
+	    '</tr>',
+	    '</table>',
+	    '<br /><div>',
+	    gettext('Total'),
+	    ': {total}',
+	    '</div><br />',
+	    '<tpl if="oldosds.length &gt; 0">',
+	    '<i class="fa fa-refresh warning"></i> ' + gettext('Outdated OSDs') + "<br>",
+	    '<div class="osds">',
+	    '<tpl for="oldosds">',
+	    '<div class="left-aligned">osd.{id}:</div>',
+	    '<div class="right-aligned">{version}</div><br />',
+	    '<div style="clear:both"></div>',
+	    '</tpl>',
+	    '</div>',
+	    '</tpl>'
+	]
+    },
+    {
+	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 += '<br>';
+			record.get('states').forEach(function(state) {
+			    html += '<br>' +
+				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: [
+	    '<h3>' + 'PGs' + '</h3>',
+	    '<tpl for="states">',
+	    '<div class="left-aligned"><i class ="fa fa-circle {cls}"></i> {state_name}:</div>',
+	    '<div class="right-aligned">{count}</div><br />',
+	    '<div style="clear:both"></div>',
+	    '</tpl>'
+	]
+    }],
+
+    // 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 = '<b>' + mgr.title + '</b>';
+		    mgr.statuses.push(gettext('Status') + ': <b>active</b>');
+		} 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 = '<b>' + mds.title + '</b>';
+		    mds.statuses.push(gettext('Status') + ': <b>' + mdsmessages[mds.name]+"</b>");
+		} 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('<br>');
+
+		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: '<h3>{title}</h3>'
+	}
+    ],
+
+    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}: ',
+	'<i class="fa fa-fw {iconCls}"></i>'
+    ],
+
+    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: '<h3>Ceph?</h3>'+
+		'<blockquote cite="https://ceph.com/"><p>"<b>Ceph</b> is a unified, distributed storage system designed for excellent performance, reliability and scalability."</p></blockquote>'+
+		'<p><b>Ceph</b> is currently <b>not installed</b> 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.</p>'+
+		'<p>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 <a target="_blank" href="http://docs.ceph.com/docs/master/">ceph.com</a>.</p>',
+		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: '<h3>Installation successful!</h3>'+
+		'<p>The basic installation and configuration is completed, depending on your setup some of the following steps are required to start using Ceph:</p>'+
+		    '<ol><li>Install Ceph on other nodes</li>'+
+		    '<li>Create additional Ceph Monitors</li>'+
+		    '<li>Create Ceph OSDs</li>'+
+		    '<li>Create Ceph Pools</li></ol>'+
+		'<p>To learn more click on the help button below.</p>',
+		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 ' +
+			   '<a target="_blank" href="' + Proxmox.Utils.get_help_link('chapter_zfs') + '">the reference documentation</a>.',
+		}
+	    ]
+	});
+
+        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('<br>');
+		    }
+		    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 '<i class="fa fa-exclamation-triangle warning"></i> ';
+			case 'error':
+			    return '<i class="fa fa-times critical"></i>';
+			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('<pre>' + lines.join('\n') + '</pre>');
+	    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 = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
+	    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: [
+		'<tpl if="lock">',
+		'<i class="fa fa-lg fa-lock"></i> ({lock})',
+		'</tpl>'
+	    ]
+	});
+
+	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('<br>');
+		    }
+		}
+	    ]
+	}
+    ]
+});
+
+Ext.define('PVE.qemu.AgentIPView', {
+    extend: 'Ext.container.Container',
+    xtype: 'pveAgentIPView',
+
+    layout: {
+	type: 'hbox',
+	align: 'top'
+    },
+
+    nics: [],
+
+    items: [
+	{
+	    xtype: 'box',
+	    html: '<i class="fa fa-exchange"></i> 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('<br>');
+		}
+	    } else if (me.nics && me.nics.error) {
+		var msg = gettext('Cannot get info from Guest Agent<br>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 = '<i class="' + rowdef.iconCls + '"></i> ';
+	}
+	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 += ' <span style="color:gray">(' + gettext('with options') + ')</span>';
+				}
+				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('<br>');
+		    } 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 + "<br>" + 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 + "<br>" + 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: [
+		'<tpl if="lock">',
+		'<i class="fa fa-lg fa-lock"></i> ({lock})',
+		'</tpl>'
+	    ]
+	});
+
+
+	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 += '<p>' + gettext('Note: Rollback stops CT') + '</p>';
+
+		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 += '<br />' + 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 = '<p>' +  Ext.htmlEncode(msg) + '</p>';
+		    metaData.tdAttr = 'data-qwidth=600 data-qtitle="ERROR" data-qtip="' + 
+			html.replace(/\"/g,'&quot;') + '"';
+		}
+	    }
+	    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('<i class="fa fa-ban warning" title="'
+					+ gettext("Removal Scheduled") + '"></i>');
+			    states.push(gettext("Removal Scheduled"));
+			}
+
+			if (record.data.error) {
+			    icons.push('<i class="fa fa-times critical" title="'
+					+ gettext("Error") + '"></i>');
+			    states.push(record.data.error);
+			}
+
+			if (icons.length == 0) {
+			    icons.push('<i class="fa fa-check good"></i>');
+			    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: [
+		'<h3>' + gettext('Nodes') + '</h3><br />',
+		'<div style="width: 150px;margin: auto;font-size: 12pt">',
+		'<div class="left-aligned">',
+		'<i class="good fa fa-fw fa-check">&nbsp;</i>',
+		gettext('Online'),
+		'</div>',
+		'<div class="right-aligned">{online}</div>',
+		'<br /><br />',
+		'<div class="left-aligned">',
+		'<i class="critical fa fa-fw fa-times">&nbsp;</i>',
+		gettext('Offline'),
+		'</div>',
+		'<div class="right-aligned">{offline}</div>',
+		'</div>'
+	    ]
+	},
+	{
+	    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: [
+	    '<h3>' + gettext("Virtual Machines") + '</h3>',
+	    '<div class="left-aligned">',
+		'<i class="good fa fa-fw fa-play-circle">&nbsp;</i>',
+		gettext('Running'),
+	    '</div>',
+	    '<div class="right-aligned">{running}</div>' + '<br />',
+	    '<tpl if="paused &gt; 0">',
+		'<div class="left-aligned">',
+		    '<i class="warning fa fa-fw fa-pause-circle">&nbsp;</i>',
+		    gettext('Paused'),
+		'</div>',
+		'<div class="right-aligned">{paused}</div>' + '<br />',
+	    '</tpl>',
+	    '<div class="left-aligned">',
+		'<i class="faded fa fa-fw fa-stop-circle">&nbsp;</i>',
+		gettext('Stopped'),
+	    '</div>',
+	    '<div class="right-aligned">{stopped}</div>' + '<br />',
+	    '<tpl if="template &gt; 0">',
+		'<div class="left-aligned">',
+		    '<i class="fa fa-fw fa-circle-o">&nbsp;</i>',
+		    gettext('Templates'),
+		'</div>',
+		'<div class="right-aligned">{template}</div>',
+	    '</tpl>'
+	]
+    },{
+	itemId: 'lxc',
+	data: {
+	    running: 0,
+	    paused: 0,
+	    stopped: 0,
+	    template: 0
+	},
+	tpl: [
+	    '<h3>' + gettext("LXC Container") + '</h3>',
+	    '<div class="left-aligned">',
+		'<i class="good fa fa-fw fa-play-circle">&nbsp;</i>',
+		gettext('Running'),
+	    '</div>',
+	    '<div class="right-aligned">{running}</div>' + '<br />',
+	    '<tpl if="paused &gt; 0">',
+		'<div class="left-aligned">',
+		    '<i class="warning fa fa-fw fa-pause-circle">&nbsp;</i>',
+		    gettext('Paused'),
+		'</div>',
+		'<div class="right-aligned">{paused}</div>' + '<br />',
+	    '</tpl>',
+	    '<div class="left-aligned">',
+		'<i class="faded fa fa-fw fa-stop-circle">&nbsp;</i>',
+		gettext('Stopped'),
+	    '</div>',
+	    '<div class="right-aligned">{stopped}</div>' + '<br />',
+	    '<tpl if="template &gt; 0">',
+		'<div class="left-aligned">',
+		    '<i class="fa fa-fw fa-circle-o">&nbsp;</i>',
+		    gettext('Templates'),
+		'</div>',
+		'<div class="right-aligned">{template}</div>',
+	    '</tpl>'
+	]
+    },{
+	itemId: 'error',
+	colspan: 2,
+	data: {
+	    num: 0
+	},
+	columnWidth: 1,
+	padding: '10 250 0 250',
+	tpl: [
+	    '<tpl if="num &gt; 0">',
+		'<div class="left-aligned">',
+		    '<i class="critical fa fa-fw fa-times-circle">&nbsp;</i>',
+		    gettext('Error'),
+		'</div>',
+		'<div class="right-aligned">{num}</div>',
+	    '</tpl>'
+	]
+    }],
+
+    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: '<h1>No valid subscription</h1>' + PVE.Utils.noSubKeyHtml,
+
+    communityHtml: 'Please use the public community <a target="_blank" href="https://forum.proxmox.com">forum</a> for any questions.',
+
+    activeHtml: 'Please use our <a target="_blank" href="https://my.proxmox.com">support portal</a> for any questions. You can also use the public community <a target="_blank" href="https://forum.proxmox.com">forum</a> to get additional information.',
+
+    bugzillaHtml: '<h1>Bug Tracking</h1>Our bug tracking system is available <a target="_blank" href="https://bugzilla.proxmox.com">here</a>.',
+
+    docuHtml: function() {
+	var me = this;
+	var guideUrl = window.location.origin + me.pveGuidePath;
+	var text = Ext.String.format('<h1>Documentation</h1>'
+	+ 'The official Proxmox VE Administration Guide'
+	+ ' is included with this installation and can be browsed at '
+	+ '<a target="_blank" href="{0}">{0}</a>', guideUrl);
+	return text;
+    },
+
+    updateActive: function(data) {
+	var me = this;
+	
+	var html = '<h1>' + data.productname + '</h1>' + me.activeHtml; 
+	html += '<br><br>' + me.docuHtml();
+	html += '<br><br>' + me.bugzillaHtml;
+
+	me.update(html);
+    },
+
+    updateCommunity: function(data) {
+	var me = this;
+
+	var html = '<h1>' + data.productname + '</h1>' + me.communityHtml; 
+	html += '<br><br>' + me.docuHtml();
+	html += '<br><br>' + 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: [ '<b>' + gettext('Group') + ':</b>', 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: '<b>' + gettext('Rules') + ':</b>',
+	    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  '<i class="fa ' + PVE.Utils.get_health_icon(cls) + '"><i/>';
+	    }
+	},
+	{
+	    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: '<a class="x-unselectable" target=_blank href="https://www.proxmox.com">' +
+				'<img style="padding-top:4px;padding-right:5px" src="/pve2/images/proxmox_logo.png"/></a>'
+			},
+			{
+			    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`)
+
-- 
GitLab