diff --git a/package.json b/package.json
index ba811482..4f85f39a 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,16 @@
"style-loader": "^0.21.0",
"webpack-uglify-js-plugin": "^1.1.9",
"raw-loader": "1.0.0-beta.0",
- "ts-loader": "^1.3.3"
+ "ts-loader": "^1.3.3",
+ "@webcomponents/webcomponentsjs": "^2.0.0",
+ "@polymer/polymer": "^3.0.0",
+ "@polymer/paper-button": "^3.0.0",
+ "@polymer/paper-spinner": "^3.0.0",
+ "@polymer/paper-tooltip": "^3.0.0",
+ "@polymer/paper-toast": "^3.0.0",
+ "@polymer/paper-toggle-button": "^3.0.0",
+ "@polymer/iron-ajax": "^3.0.0",
+ "@polymer/iron-autogrow-textarea": "^3.0.0",
+ "@polymer/iron-a11y-keys": "^3.0.0"
}
}
diff --git a/pkgs/node-packages.nix b/pkgs/node-packages.nix
index 43edef48..045f321d 100644
--- a/pkgs/node-packages.nix
+++ b/pkgs/node-packages.nix
@@ -4,6 +4,222 @@
let
sources = {
+ "@polymer/font-roboto-3.0.2" = {
+ name = "_at_polymer_slash_font-roboto";
+ packageName = "@polymer/font-roboto";
+ version = "3.0.2";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/font-roboto/-/font-roboto-3.0.2.tgz";
+ sha512 = "tx5TauYSmzsIvmSqepUPDYbs4/Ejz2XbZ1IkD7JEGqkdNUJlh+9KU85G56Tfdk/xjEZ8zorFfN09OSwiMrIQWA==";
+ };
+ };
+ "@polymer/iron-a11y-announcer-3.0.1" = {
+ name = "_at_polymer_slash_iron-a11y-announcer";
+ packageName = "@polymer/iron-a11y-announcer";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-a11y-announcer/-/iron-a11y-announcer-3.0.1.tgz";
+ sha512 = "Xiqmpz0AEEbMNGYPpbrXBIrcI/xaR4tn77pmSLfxVKGGwjEUR/YrRgyIwXp4EN7lvst1dFC8kyl2hLga0uDIVQ==";
+ };
+ };
+ "@polymer/iron-a11y-keys-3.0.1" = {
+ name = "_at_polymer_slash_iron-a11y-keys";
+ packageName = "@polymer/iron-a11y-keys";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-a11y-keys/-/iron-a11y-keys-3.0.1.tgz";
+ sha512 = "zmTi8xHeY4ZMJLAitW2hAmW5zXZ35hVy/eHQUFadAlOccuBK3oRRmoPRQRaZgpyJrCVFDAQRXXzzJtUDil/0CA==";
+ };
+ };
+ "@polymer/iron-a11y-keys-behavior-3.0.1" = {
+ name = "_at_polymer_slash_iron-a11y-keys-behavior";
+ packageName = "@polymer/iron-a11y-keys-behavior";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-a11y-keys-behavior/-/iron-a11y-keys-behavior-3.0.1.tgz";
+ sha512 = "lnrjKq3ysbBPT/74l0Fj0U9H9C35Tpw2C/tpJ8a+5g8Y3YJs1WSZYnEl1yOkw6sEyaxOq/1DkzH0+60gGu5/PQ==";
+ };
+ };
+ "@polymer/iron-ajax-3.0.1" = {
+ name = "_at_polymer_slash_iron-ajax";
+ packageName = "@polymer/iron-ajax";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-ajax/-/iron-ajax-3.0.1.tgz";
+ sha512 = "7+TPEAfWsRdhj1Y8UeF1759ktpVu+c3sG16rJiUC3wF9+woQ9xI1zUm2d59i7Yc3aDEJrR/Q8Y262KlOvyGVNg==";
+ };
+ };
+ "@polymer/iron-autogrow-textarea-3.0.1" = {
+ name = "_at_polymer_slash_iron-autogrow-textarea";
+ packageName = "@polymer/iron-autogrow-textarea";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-autogrow-textarea/-/iron-autogrow-textarea-3.0.1.tgz";
+ sha512 = "FgSL7APrOSL9Vu812sBCFlQ17hvnJsBAV2C2e1UAiaHhB+dyfLq8gGdGUpqVWuGJ50q4Y/49QwCNnLf85AdVYA==";
+ };
+ };
+ "@polymer/iron-behaviors-3.0.1" = {
+ name = "_at_polymer_slash_iron-behaviors";
+ packageName = "@polymer/iron-behaviors";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-behaviors/-/iron-behaviors-3.0.1.tgz";
+ sha512 = "IMEwcv1lhf1HSQxuyWOUIL0lOBwmeaoSTpgCJeP9IBYnuB1SPQngmfRuHKgK6/m9LQ9F9miC7p3HeQQUdKAE0w==";
+ };
+ };
+ "@polymer/iron-checked-element-behavior-3.0.1" = {
+ name = "_at_polymer_slash_iron-checked-element-behavior";
+ packageName = "@polymer/iron-checked-element-behavior";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-checked-element-behavior/-/iron-checked-element-behavior-3.0.1.tgz";
+ sha512 = "aDr0cbCNVq49q+pOqa6CZutFh+wWpwPMLpEth9swx+GkAj+gCURhuQkaUYhIo5f2egDbEioR1aeHMnPlU9dQZA==";
+ };
+ };
+ "@polymer/iron-fit-behavior-3.0.1" = {
+ name = "_at_polymer_slash_iron-fit-behavior";
+ packageName = "@polymer/iron-fit-behavior";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-fit-behavior/-/iron-fit-behavior-3.0.1.tgz";
+ sha512 = "/M0B1L30k31vmwNBaGuZcxzUAhJSHoGccb/DF0CDKI/hT8UlkTvcyemaWdOpmHHLgY52ceKIkRwA3AeXrKyvaQ==";
+ };
+ };
+ "@polymer/iron-flex-layout-3.0.1" = {
+ name = "_at_polymer_slash_iron-flex-layout";
+ packageName = "@polymer/iron-flex-layout";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-flex-layout/-/iron-flex-layout-3.0.1.tgz";
+ sha512 = "7gB869czArF+HZcPTVSgvA7tXYFze9EKckvM95NB7SqYF+NnsQyhoXgKnpFwGyo95lUjUW9TFDLUwDXnCYFtkw==";
+ };
+ };
+ "@polymer/iron-form-element-behavior-3.0.1" = {
+ name = "_at_polymer_slash_iron-form-element-behavior";
+ packageName = "@polymer/iron-form-element-behavior";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-form-element-behavior/-/iron-form-element-behavior-3.0.1.tgz";
+ sha512 = "G/e2KXyL5AY7mMjmomHkGpgS0uAf4ovNpKhkuUTRnMuMJuf589bKqE85KN4ovE1Tzhv2hJoh/igyD6ekHiYU1A==";
+ };
+ };
+ "@polymer/iron-meta-3.0.1" = {
+ name = "_at_polymer_slash_iron-meta";
+ packageName = "@polymer/iron-meta";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-meta/-/iron-meta-3.0.1.tgz";
+ sha512 = "pWguPugiLYmWFV9UWxLWzZ6gm4wBwQdDy4VULKwdHCqR7OP7u98h+XDdGZsSlDPv6qoryV/e3tGHlTIT0mbzJA==";
+ };
+ };
+ "@polymer/iron-overlay-behavior-3.0.2" = {
+ name = "_at_polymer_slash_iron-overlay-behavior";
+ packageName = "@polymer/iron-overlay-behavior";
+ version = "3.0.2";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-overlay-behavior/-/iron-overlay-behavior-3.0.2.tgz";
+ sha512 = "j1qmt6mJHCwpe1mKOvqK5kcCUPQr5LSrlqpgRDbUuLgUfNJ/vGTipjrkBlfbEUagm5FEQdc1VLPLSQP6WVuP9g==";
+ };
+ };
+ "@polymer/iron-resizable-behavior-3.0.1" = {
+ name = "_at_polymer_slash_iron-resizable-behavior";
+ packageName = "@polymer/iron-resizable-behavior";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-resizable-behavior/-/iron-resizable-behavior-3.0.1.tgz";
+ sha512 = "FyHxRxFspVoRaeZSWpT3y0C9awomb4tXXolIJcZ7RvXhMP632V5lez+ch5G5SwK0LpnAPkg35eB0LPMFv+YMMQ==";
+ };
+ };
+ "@polymer/iron-validatable-behavior-3.0.1" = {
+ name = "_at_polymer_slash_iron-validatable-behavior";
+ packageName = "@polymer/iron-validatable-behavior";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/iron-validatable-behavior/-/iron-validatable-behavior-3.0.1.tgz";
+ sha512 = "wwpYh6wOa4fNI+jH5EYKC7TVPYQ2OfgQqocWat7GsNWcsblKYhLYbwsvEY5nO0n2xKqNfZzDLrUom5INJN7msQ==";
+ };
+ };
+ "@polymer/paper-behaviors-3.0.1" = {
+ name = "_at_polymer_slash_paper-behaviors";
+ packageName = "@polymer/paper-behaviors";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/paper-behaviors/-/paper-behaviors-3.0.1.tgz";
+ sha512 = "6knhj69fPJejv8qR0kCSUY+Q0XjaUf0OSnkjRjmTJPAwSrRYtgqE+l6P1FfA+py1X/cUjgne9EF5rMZAKJIg1g==";
+ };
+ };
+ "@polymer/paper-button-3.0.1" = {
+ name = "_at_polymer_slash_paper-button";
+ packageName = "@polymer/paper-button";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/paper-button/-/paper-button-3.0.1.tgz";
+ sha512 = "JRNBc+Oj9EWnmyLr7FcCr8T1KAnEHPh6mosln9BUdkM+qYaYsudSICh3cjTIbnj6AuF5OJidoLkM1dlyj0j6Zg==";
+ };
+ };
+ "@polymer/paper-ripple-3.0.1" = {
+ name = "_at_polymer_slash_paper-ripple";
+ packageName = "@polymer/paper-ripple";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/paper-ripple/-/paper-ripple-3.0.1.tgz";
+ sha512 = "dgOe12GyCF1VZBLUQqnzGWlf3xb255FajNCVB1VFj/AtskYtoamnafa7m3a+1vs+C8qbg4Benn5KwgxVDSW4cg==";
+ };
+ };
+ "@polymer/paper-spinner-3.0.1" = {
+ name = "_at_polymer_slash_paper-spinner";
+ packageName = "@polymer/paper-spinner";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/paper-spinner/-/paper-spinner-3.0.1.tgz";
+ sha512 = "MYIU6qWZnhZ5yNFOBzROPgBteGfxKEnDZ6bCgjrvUtJkBuQEz0MQZzSE/zmZc0oaJ9u5QK5xAFuYdudsGv7+sQ==";
+ };
+ };
+ "@polymer/paper-styles-3.0.1" = {
+ name = "_at_polymer_slash_paper-styles";
+ packageName = "@polymer/paper-styles";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/paper-styles/-/paper-styles-3.0.1.tgz";
+ sha512 = "y6hmObLqlCx602TQiSBKHqjwkE7xmDiFkoxdYGaNjtv4xcysOTdVJsDR/R9UHwIaxJ7gHlthMSykir1nv78++g==";
+ };
+ };
+ "@polymer/paper-toast-3.0.1" = {
+ name = "_at_polymer_slash_paper-toast";
+ packageName = "@polymer/paper-toast";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/paper-toast/-/paper-toast-3.0.1.tgz";
+ sha512 = "pizuogzObniDdICUc6dSLrnDt2VzzoRne1gCmbD6sfOATVv5tc8UfrqhA2iHngbNBEbniBiciS3iogdp5KTVUQ==";
+ };
+ };
+ "@polymer/paper-toggle-button-3.0.1" = {
+ name = "_at_polymer_slash_paper-toggle-button";
+ packageName = "@polymer/paper-toggle-button";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/paper-toggle-button/-/paper-toggle-button-3.0.1.tgz";
+ sha512 = "jadZB60fycT7YnSAH0H23LYo6/2HYmMZTtNr9LpdSIRFPLX6mqqxewex92cFz019bMKaRJgORn308hRlJo2u6A==";
+ };
+ };
+ "@polymer/paper-tooltip-3.0.1" = {
+ name = "_at_polymer_slash_paper-tooltip";
+ packageName = "@polymer/paper-tooltip";
+ version = "3.0.1";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/paper-tooltip/-/paper-tooltip-3.0.1.tgz";
+ sha512 = "yiUk09opTEnE1lK+tb501ENb+yQBi4p++Ep0eGJAHesVYKVMPNgPphVKkIizkDaU+n0SE+zXfTsRbYyOMDYXSg==";
+ };
+ };
+ "@polymer/polymer-3.1.0" = {
+ name = "_at_polymer_slash_polymer";
+ packageName = "@polymer/polymer";
+ version = "3.1.0";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.1.0.tgz";
+ sha512 = "hwN8IMERsFATz/9dSMxYHL+84J9uBkPuuarxJWlTsppZ4CAYTZKnepBfNrKoyNsafBmA3yXBiiKPPf+fJtza7A==";
+ };
+ };
"@types/clone-0.1.30" = {
name = "_at_types_slash_clone";
packageName = "@types/clone";
@@ -193,6 +409,24 @@ let
sha512 = "mJ3QKWtCchL1vhU/kZlJnLPuQZnlDOdZsyP0bbLWPGdYsQDnSBvyTLhzwBA3QAMlzEL9V4JHygEmK6/OTEyytA==";
};
};
+ "@webcomponents/shadycss-1.5.2" = {
+ name = "_at_webcomponents_slash_shadycss";
+ packageName = "@webcomponents/shadycss";
+ version = "1.5.2";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.5.2.tgz";
+ sha512 = "0OyrmVc7S+INtzoqP2ofAo+OdVn2Nj0Qvq4wD9FEGN7nMmLRxaD2mzy6hD6EslzxUSuGH302CDU4KXiY66SEqg==";
+ };
+ };
+ "@webcomponents/webcomponentsjs-2.1.3" = {
+ name = "_at_webcomponents_slash_webcomponentsjs";
+ packageName = "@webcomponents/webcomponentsjs";
+ version = "2.1.3";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.1.3.tgz";
+ sha512 = "0UHJNY88lR3pnEYtBVT7F8cuuxOiITQGWJa0LxoELqkBSB7IabzJFOj5K99PajD3CGAsWpjB0CAeijfe376Y1w==";
+ };
+ };
"@xtuc/ieee754-1.2.0" = {
name = "_at_xtuc_slash_ieee754";
packageName = "@xtuc/ieee754";
@@ -1454,13 +1688,13 @@ let
sha1 = "b534e7c734c4f81ec5fbe8aca2ad24354b962c6c";
};
};
- "caniuse-db-1.0.30000899" = {
+ "caniuse-db-1.0.30000900" = {
name = "caniuse-db";
packageName = "caniuse-db";
- version = "1.0.30000899";
+ version = "1.0.30000900";
src = fetchurl {
- url = "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000899.tgz";
- sha512 = "MSCUohyoLU4/PGapapw/PLQkmQ+sFgzX6e3tM6ue8HX9HW9rBD5gRiAYKhC8r0QkvUE0pWTA8Ze6f3jrzBizVg==";
+ url = "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000900.tgz";
+ sha512 = "fvicVRDlhHIQpt/bmbLl3hDHKUZb5ZP8O2OuZLz2fSEPlUBbvwwbhhqhGS617ldN6bDoo9A3+MQKQyFq0p7UXA==";
};
};
"caniuse-lite-1.0.30000899" = {
@@ -7482,6 +7716,30 @@ let
version = "1.0.0";
src = ./..;
dependencies = [
+ sources."@polymer/font-roboto-3.0.2"
+ sources."@polymer/iron-a11y-announcer-3.0.1"
+ sources."@polymer/iron-a11y-keys-3.0.1"
+ sources."@polymer/iron-a11y-keys-behavior-3.0.1"
+ sources."@polymer/iron-ajax-3.0.1"
+ sources."@polymer/iron-autogrow-textarea-3.0.1"
+ sources."@polymer/iron-behaviors-3.0.1"
+ sources."@polymer/iron-checked-element-behavior-3.0.1"
+ sources."@polymer/iron-fit-behavior-3.0.1"
+ sources."@polymer/iron-flex-layout-3.0.1"
+ sources."@polymer/iron-form-element-behavior-3.0.1"
+ sources."@polymer/iron-meta-3.0.1"
+ sources."@polymer/iron-overlay-behavior-3.0.2"
+ sources."@polymer/iron-resizable-behavior-3.0.1"
+ sources."@polymer/iron-validatable-behavior-3.0.1"
+ sources."@polymer/paper-behaviors-3.0.1"
+ sources."@polymer/paper-button-3.0.1"
+ sources."@polymer/paper-ripple-3.0.1"
+ sources."@polymer/paper-spinner-3.0.1"
+ sources."@polymer/paper-styles-3.0.1"
+ sources."@polymer/paper-toast-3.0.1"
+ sources."@polymer/paper-toggle-button-3.0.1"
+ sources."@polymer/paper-tooltip-3.0.1"
+ sources."@polymer/polymer-3.1.0"
sources."@types/clone-0.1.30"
sources."@types/node-6.14.0"
sources."@types/parse5-2.2.34"
@@ -7503,6 +7761,8 @@ let
sources."@webassemblyjs/wasm-parser-1.7.10"
sources."@webassemblyjs/wast-parser-1.7.10"
sources."@webassemblyjs/wast-printer-1.7.10"
+ sources."@webcomponents/shadycss-1.5.2"
+ sources."@webcomponents/webcomponentsjs-2.1.3"
sources."@xtuc/ieee754-1.2.0"
sources."@xtuc/long-4.2.1"
sources."abbrev-1.1.1"
@@ -7716,7 +7976,7 @@ let
sources."browserslist-1.7.7"
];
})
- sources."caniuse-db-1.0.30000899"
+ sources."caniuse-db-1.0.30000900"
sources."caniuse-lite-1.0.30000899"
sources."caseless-0.12.0"
sources."center-align-0.1.3"
diff --git a/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.html b/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.html
deleted file mode 100644
index 36d81d5b..00000000
--- a/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.html
+++ /dev/null
@@ -1,610 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.js b/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.js
new file mode 100644
index 00000000..1d665f40
--- /dev/null
+++ b/rhodecode/public/js/src/components/channelstream-connection/channelstream-connection.js
@@ -0,0 +1,577 @@
+import {Polymer, html} from '@polymer/polymer/polymer-legacy';
+import '@polymer/iron-ajax/iron-ajax.js';
+
+const elemTemplate = html`
+
+
+
+
+
+
+
+
+
+
+
+`
+
+Polymer({
+ is: 'channelstream-connection',
+
+ _template: elemTemplate,
+
+ /**
+ * Fired when `channels` array changes.
+ *
+ * @event channelstream-channels-changed
+ */
+
+ /**
+ * Fired when `connect()` method succeeds.
+ *
+ * @event channelstream-connected
+ */
+
+ /**
+ * Fired when `connect` fails.
+ *
+ * @event channelstream-connect-error
+ */
+
+ /**
+ * Fired when `disconnect()` succeeds.
+ *
+ * @event channelstream-disconnected
+ */
+
+ /**
+ * Fired when `message()` succeeds.
+ *
+ * @event channelstream-message-sent
+ */
+
+ /**
+ * Fired when `message()` fails.
+ *
+ * @event channelstream-message-error
+ */
+
+ /**
+ * Fired when `subscribe()` succeeds.
+ *
+ * @event channelstream-subscribed
+ */
+
+ /**
+ * Fired when `subscribe()` fails.
+ *
+ * @event channelstream-subscribe-error
+ */
+
+ /**
+ * Fired when `unsubscribe()` succeeds.
+ *
+ * @event channelstream-unsubscribed
+ */
+
+ /**
+ * Fired when `unsubscribe()` fails.
+ *
+ * @event channelstream-unsubscribe-error
+ */
+
+ /**
+ * Fired when listening connection receives a message.
+ *
+ * @event channelstream-listen-message
+ */
+
+ /**
+ * Fired when listening connection is opened.
+ *
+ * @event channelstream-listen-opened
+ */
+
+ /**
+ * Fired when listening connection is closed.
+ *
+ * @event channelstream-listen-closed
+ */
+
+ /**
+ * Fired when listening connection suffers an error.
+ *
+ * @event channelstream-listen-error
+ */
+
+ properties: {
+ isReady: Boolean,
+ /** List of channels user should be subscribed to. */
+ channels: {
+ type: Array,
+ value: function () {
+ return []
+ },
+ notify: true
+ },
+ /** Username of connecting user. */
+ username: {
+ type: String,
+ value: 'Anonymous',
+ reflectToAttribute: true
+ },
+ /** Connection identifier. */
+ connectionId: {
+ type: String,
+ reflectToAttribute: true
+ },
+ /** Websocket instance. */
+ websocket: {
+ type: Object,
+ value: null
+ },
+ /** Websocket connection url. */
+ websocketUrl: {
+ type: String,
+ value: ''
+ },
+ /** URL used in `connect()`. */
+ connectUrl: {
+ type: String,
+ value: ''
+ },
+ /** URL used in `disconnect()`. */
+ disconnectUrl: {
+ type: String,
+ value: ''
+ },
+ /** URL used in `subscribe()`. */
+ subscribeUrl: {
+ type: String,
+ value: ''
+ },
+ /** URL used in `unsubscribe()`. */
+ unsubscribeUrl: {
+ type: String,
+ value: ''
+ },
+ /** URL used in `message()`. */
+ messageUrl: {
+ type: String,
+ value: ''
+ },
+ /** Long-polling connection url. */
+ longPollUrl: {
+ type: String,
+ value: ''
+ },
+ /** Long-polling connection url. */
+ shouldReconnect: {
+ type: Boolean,
+ value: true
+ },
+ /** Should send heartbeats. */
+ heartbeats: {
+ type: Boolean,
+ value: true
+ },
+ /** How much should every retry interval increase (in milliseconds) */
+ increaseBounceIv: {
+ type: Number,
+ value: 2000
+ },
+ _currentBounceIv: {
+ type: Number,
+ reflectToAttribute: true,
+ value: 0
+ },
+ /** Should use websockets or long-polling by default */
+ useWebsocket: {
+ type: Boolean,
+ reflectToAttribute: true,
+ value: true
+ },
+ connected: {
+ type: Boolean,
+ reflectToAttribute: true,
+ value: false
+ }
+ },
+
+ observers: [
+ '_handleChannelsChange(channels.splices)'
+ ],
+
+ listeners: {
+ 'channelstream-connected': 'startListening',
+ 'channelstream-connect-error': 'retryConnection',
+ },
+
+ /**
+ * Mutators hold functions that you can set locally to change the data
+ * that the client is sending to all endpoints
+ * you can call it like `elem.mutators('connect', yourFunc())`
+ * mutators will be executed in order they were pushed onto arrays
+ *
+ */
+ mutators: {
+ connect: function () {
+ return []
+ }(),
+ message: function () {
+ return []
+ }(),
+ subscribe: function () {
+ return []
+ }(),
+ unsubscribe: function () {
+ return []
+ }(),
+ disconnect: function () {
+ return []
+ }()
+ },
+ ready: function () {
+ this.isReady = true;
+ },
+
+ /**
+ * Connects user and fetches connection id from the server.
+ *
+ */
+ connect: function () {
+ var request = this.$['ajaxConnect'];
+ request.url = this.connectUrl;
+ request.body = {
+ username: this.username,
+ channels: this.channels
+ };
+ for (var i = 0; i < this.mutators.connect.length; i++) {
+ this.mutators.connect[i](request);
+ }
+ request.generateRequest()
+ },
+ /**
+ * Overwrite with custom function that will
+ */
+ addMutator: function (type, func) {
+ this.mutators[type].push(func);
+ },
+ /**
+ * Subscribes user to channels.
+ *
+ */
+ subscribe: function (channels) {
+ var request = this.$['ajaxSubscribe'];
+ request.url = this.subscribeUrl;
+ request.body = {
+ channels: channels,
+ conn_id: this.connectionId
+ };
+ for (var i = 0; i < this.mutators.subscribe.length; i++) {
+ this.mutators.subscribe[i](request);
+ }
+ if (request.body.channels.length) {
+ request.generateRequest();
+ }
+ },
+ /**
+ * Unsubscribes user from channels.
+ *
+ */
+ unsubscribe: function (unsubscribe) {
+ var request = this.$['ajaxUnsubscribe'];
+
+ request.url = this.unsubscribeUrl;
+ request.body = {
+ channels: unsubscribe,
+ conn_id: this.connectionId
+ };
+ for (var i = 0; i < this.mutators.unsubscribe.length; i++) {
+ this.mutators.unsubscribe[i](request);
+ }
+ request.generateRequest()
+ },
+
+ /**
+ * calculates list of channels we should add user to based on difference
+ * between channels property and passed channel list
+ */
+ calculateSubscribe: function (channels) {
+ var currentlySubscribed = this.channels;
+ var toSubscribe = [];
+ for (var i = 0; i < channels.length; i++) {
+ if (currentlySubscribed.indexOf(channels[i]) === -1) {
+ toSubscribe.push(channels[i]);
+ }
+ }
+ return toSubscribe
+ },
+ /**
+ * calculates list of channels we should remove user from based difference
+ * between channels property and passed channel list
+ */
+ calculateUnsubscribe: function (channels) {
+ var currentlySubscribed = this.channels;
+ var toUnsubscribe = [];
+ for (var i = 0; i < channels.length; i++) {
+ if (currentlySubscribed.indexOf(channels[i]) !== -1) {
+ toUnsubscribe.push(channels[i]);
+ }
+ }
+ return toUnsubscribe
+ },
+ /**
+ * Marks the connection as expired.
+ *
+ */
+ disconnect: function () {
+ var request = this.$['ajaxDisconnect'];
+ request.url = this.disconnectUrl;
+ request.params = {
+ conn_id: this.connectionId
+ };
+ for (var i = 0; i < this.mutators.disconnect.length; i++) {
+ this.mutators.disconnect[i](request);
+ }
+ // mark connection as expired
+ request.generateRequest();
+ // disconnect existing connection
+ this.closeConnection();
+ },
+
+ /**
+ * Sends a message to the server.
+ *
+ */
+ message: function (message) {
+ var request = this.$['ajaxMessage'];
+ request.url = this.messageUrl;
+ request.body = message;
+ for (var i = 0; i < this.mutators.message.length; i++) {
+ this.mutators.message[i](request)
+ }
+ request.generateRequest();
+ },
+ /**
+ * Opens "long lived" (websocket/longpoll) connection to the channelstream server.
+ *
+ */
+ startListening: function (event) {
+ this.fire('start-listening', {});
+ if (this.useWebsocket) {
+ this.useWebsocket = window.WebSocket ? true : false;
+ }
+ if (this.useWebsocket) {
+ this.openWebsocket();
+ }
+ else {
+ this.openLongPoll();
+ }
+ },
+ /**
+ * Opens websocket connection.
+ *
+ */
+ openWebsocket: function () {
+ var url = this.websocketUrl + '?conn_id=' + this.connectionId;
+ this.websocket = new WebSocket(url);
+ this.websocket.onopen = this._handleListenOpen.bind(this);
+ this.websocket.onclose = this._handleListenCloseEvent.bind(this);
+ this.websocket.onerror = this._handleListenErrorEvent.bind(this);
+ this.websocket.onmessage = this._handleListenMessageEvent.bind(this);
+ },
+ /**
+ * Opens long-poll connection.
+ *
+ */
+ openLongPoll: function () {
+ var request = this.$['ajaxListen'];
+ request.url = this.longPollUrl + '?conn_id=' + this.connectionId;
+ request.generateRequest()
+ },
+ /**
+ * Retries `connect()` call while incrementing interval between tries up to 1 minute.
+ *
+ */
+ retryConnection: function () {
+ if (!this.shouldReconnect) {
+ return;
+ }
+ if (this._currentBounceIv < 60000) {
+ this._currentBounceIv = this._currentBounceIv + this.increaseBounceIv;
+ }
+ else {
+ this._currentBounceIv = 60000;
+ }
+ setTimeout(this.connect.bind(this), this._currentBounceIv);
+ },
+ /**
+ * Closes listening connection.
+ *
+ */
+ closeConnection: function () {
+ var request = this.$['ajaxListen'];
+ if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
+ this.websocket.onclose = null;
+ this.websocket.onerror = null;
+ this.websocket.close();
+ }
+ if (request.loading) {
+ request.lastRequest.abort();
+ }
+ this.connected = false;
+ },
+
+ _handleChannelsChange: function (event) {
+ // do not fire the event if set() didn't mutate anything
+ // is this a reliable way to do it?
+ if (!this.isReady || event === undefined) {
+ return
+ }
+ this.fire('channelstream-channels-changed', event)
+ },
+
+ _handleListenOpen: function (event) {
+ this.connected = true;
+ this.fire('channelstream-listen-opened', event);
+ this.createHeartBeats();
+ },
+
+ createHeartBeats: function () {
+ if (typeof self._heartbeat === 'undefined' && this.websocket !== null
+ && this.heartbeats) {
+ self._heartbeat = setInterval(this._sendHeartBeat.bind(this), 10000);
+ }
+ },
+
+ _sendHeartBeat: function () {
+ if (this.websocket.readyState === WebSocket.OPEN && this.heartbeats) {
+ this.websocket.send(JSON.stringify({type: 'heartbeat'}));
+ }
+ },
+
+ _handleListenError: function (event) {
+ this.connected = false;
+ this.retryConnection();
+ },
+ _handleConnectError: function (event) {
+ this.connected = false;
+ this.fire('channelstream-connect-error', event.detail);
+ },
+
+ _handleListenMessageEvent: function (event) {
+ var data = null;
+ // comes from iron-ajax
+ if (event.detail) {
+ data = JSON.parse(event.detail.response)
+ // comes from websocket
+ setTimeout(this.openLongPoll.bind(this), 0);
+ } else {
+ data = JSON.parse(event.data)
+ }
+ this.fire('channelstream-listen-message', data);
+
+ },
+
+ _handleListenCloseEvent: function (event) {
+ this.connected = false;
+ this.fire('channelstream-listen-closed', event.detail);
+ this.retryConnection();
+ },
+
+ _handleListenErrorEvent: function (event) {
+ this.connected = false;
+ this.fire('channelstream-listen-error', {})
+ },
+
+ _handleConnect: function (event) {
+ this.currentBounceIv = 0;
+ this.connectionId = event.detail.response.conn_id;
+ this.fire('channelstream-connected', event.detail.response);
+ },
+
+ _handleDisconnect: function (event) {
+ this.connected = false;
+ this.fire('channelstream-disconnected', {});
+ },
+
+ _handleMessage: function (event) {
+ this.fire('channelstream-message-sent', event.detail.response);
+ },
+ _handleMessageError: function (event) {
+ this.fire('channelstream-message-error', event.detail);
+ },
+
+ _handleSubscribe: function (event) {
+ this.fire('channelstream-subscribed', event.detail.response);
+ },
+
+ _handleSubscribeError: function (event) {
+ this.fire('channelstream-subscribe-error', event.detail);
+ },
+
+ _handleUnsubscribe: function (event) {
+ this.fire('channelstream-unsubscribed', event.detail.response);
+ },
+
+ _handleUnsubscribeError: function (event) {
+ this.fire('channelstream-unsubscribe-error', event.detail);
+ }
+});
diff --git a/rhodecode/public/js/src/components/index.js b/rhodecode/public/js/src/components/index.js
index 3dfb7084..d482a4ce 100644
--- a/rhodecode/public/js/src/components/index.js
+++ b/rhodecode/public/js/src/components/index.js
@@ -1,7 +1,7 @@
-import '../../../../../bower_components/iron-ajax/iron-ajax.html';
+import '@polymer/iron-ajax/iron-ajax.js';
import './root-styles.gen.html';
-import './channelstream-connection/channelstream-connection.html';
-import './rhodecode-toast/rhodecode-toast.html';
-import './rhodecode-toggle/rhodecode-toggle.html';
-import './rhodecode-unsafe-html/rhodecode-unsafe-html.html';
-import './rhodecode-app/rhodecode-app.html';
+import './channelstream-connection/channelstream-connection.js';
+import './rhodecode-toast/rhodecode-toast.js';
+import './rhodecode-toggle/rhodecode-toggle.js';
+import './rhodecode-unsafe-html/rhodecode-unsafe-html.js';
+import './rhodecode-app/rhodecode-app.js';
diff --git a/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.html b/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.html
deleted file mode 100644
index b354610b..00000000
--- a/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.html
+++ /dev/null
@@ -1,195 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.js b/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.js
new file mode 100644
index 00000000..ad90b911
--- /dev/null
+++ b/rhodecode/public/js/src/components/rhodecode-app/rhodecode-app.js
@@ -0,0 +1,194 @@
+import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
+import '../channelstream-connection/channelstream-connection.js';
+import '../rhodecode-toast/rhodecode-toast.js';
+import '../rhodecode-favicon/rhodecode-favicon.js';
+
+var ccLog = Logger.get('RhodeCodeApp');
+ccLog.setLevel(Logger.OFF);
+
+export class RhodecodeApp extends PolymerElement {
+
+ static get is() {
+ return 'rhodecode-app';
+ }
+
+ static get template(){
+ return html`
+
+
+
+ `
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ ccLog.debug('rhodeCodeApp created');
+ $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
+ $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
+ $.Topic('/connection_controller/subscribe').subscribe(
+ this.subscribeToChannelTopic.bind(this));
+ // this event can be used to coordinate plugins to do their
+ // initialization before channelstream is kicked off
+ $.Topic('/__MAIN_APP__').publish({});
+
+ for (var i = 0; i < alertMessagePayloads.length; i++) {
+ $.Topic('/notifications').publish(alertMessagePayloads[i]);
+ }
+ this.initPlugins();
+ // after rest of application loads and topics get fired, launch connection
+ $(document).ready(function () {
+ this.kickoffChannelstreamPlugin();
+ }.bind(this));
+ }
+
+ initPlugins() {
+ for (var i = 0; i < window.APPLICATION_PLUGINS.length; i++) {
+ var pluginDef = window.APPLICATION_PLUGINS[i];
+ if (pluginDef.component) {
+ var pluginElem = document.createElement(pluginDef.component);
+ this.shadowRoot.appendChild(pluginElem);
+ if (typeof pluginElem.init !== 'undefined') {
+ pluginElem.init();
+ }
+ }
+ }
+ }
+
+ /** proxy to channelstream connection */
+ getChannelStreamConnection() {
+ return this.$['channelstream-connection'];
+ }
+
+ handleNotifications(data) {
+ var elem = document.getElementById('notifications');
+ if (elem) {
+ elem.handleNotification(data);
+ }
+
+ }
+
+ faviconUpdate(data) {
+ this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
+ }
+
+ /** opens connection to ws server */
+ kickoffChannelstreamPlugin(data) {
+ ccLog.debug('kickoffChannelstreamPlugin');
+ var channels = ['broadcast'];
+ var addChannels = this.checkViewChannels();
+ for (var i = 0; i < addChannels.length; i++) {
+ channels.push(addChannels[i]);
+ }
+ if (window.CHANNELSTREAM_SETTINGS && CHANNELSTREAM_SETTINGS.enabled) {
+ var channelstreamConnection = this.getChannelStreamConnection();
+ channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
+ channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
+ channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
+ channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
+ // some channels might already be registered by topic
+ for (var i = 0; i < channels.length; i++) {
+ channelstreamConnection.push('channels', channels[i]);
+ }
+ // append any additional channels registered in other plugins
+ $.Topic('/connection_controller/subscribe').processPrepared();
+ channelstreamConnection.connect();
+ }
+ }
+
+ checkViewChannels() {
+ // subscribe to different channels data is sent.
+
+ var channels = [];
+ // subscribe to PR repo channel for PR's'
+ if (templateContext.pull_request_data.pull_request_id) {
+ var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
+ String(templateContext.pull_request_data.pull_request_id);
+ channels.push(channelName);
+ }
+
+ if (templateContext.commit_data.commit_id) {
+ var channelName = '/repo$' + templateContext.repo_name + '$/commit/' +
+ String(templateContext.commit_data.commit_id);
+ channels.push(channelName);
+ }
+
+ return channels;
+ }
+
+ /** subscribes users from channels in channelstream */
+ subscribeToChannelTopic(channels) {
+ var channelstreamConnection = this.getChannelStreamConnection();
+ var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
+ ccLog.debug('subscribeToChannelTopic', toSubscribe);
+ if (toSubscribe.length > 0) {
+ // if we are connected then subscribe
+ if (channelstreamConnection.connected) {
+ channelstreamConnection.subscribe(toSubscribe);
+ }
+ // not connected? just push channels onto the stack
+ else {
+ for (var i = 0; i < toSubscribe.length; i++) {
+ channelstreamConnection.push('channels', toSubscribe[i]);
+ }
+ }
+ }
+ }
+
+ /** publish received messages into correct topic */
+ receivedMessage(event) {
+ for (var i = 0; i < event.detail.length; i++) {
+ var message = event.detail[i];
+ if (message.message.topic) {
+ ccLog.debug('publishing', message.message.topic);
+ $.Topic(message.message.topic).publish(message);
+ }
+ else if (message.type === 'presence') {
+ $.Topic('/connection_controller/presence').publish(message);
+ }
+ else {
+ ccLog.warn('unhandled message', message);
+ }
+ }
+ }
+
+ handleConnected(event) {
+ var channelstreamConnection = this.getChannelStreamConnection();
+ channelstreamConnection.set('channelsState',
+ event.detail.channels_info);
+ channelstreamConnection.set('userState', event.detail.state);
+ channelstreamConnection.set('channels', event.detail.channels);
+ this.propagageChannelsState();
+ }
+
+ handleSubscribed(event) {
+ var channelstreamConnection = this.getChannelStreamConnection();
+ var channelInfo = event.detail.channels_info;
+ var channelKeys = Object.keys(event.detail.channels_info);
+ for (var i = 0; i < channelKeys.length; i++) {
+ var key = channelKeys[i];
+ channelstreamConnection.set(['channelsState', key], channelInfo[key]);
+ }
+ channelstreamConnection.set('channels', event.detail.channels);
+ this.propagageChannelsState();
+ }
+
+ /** propagates channel states on topics */
+ propagageChannelsState(event) {
+ var channelstreamConnection = this.getChannelStreamConnection();
+ var channel_data = channelstreamConnection.channelsState;
+ var channels = channelstreamConnection.channels;
+ for (var i = 0; i < channels.length; i++) {
+ var key = channels[i];
+ $.Topic('/connection_controller/channel_update').publish(
+ {channel: key, state: channel_data[key]}
+ );
+ }
+ }
+
+}
+
+customElements.define(RhodecodeApp.is, RhodecodeApp);
diff --git a/rhodecode/public/js/src/components/rhodecode-favicon/rhodecode-favicon.html b/rhodecode/public/js/src/components/rhodecode-favicon/rhodecode-favicon.html
deleted file mode 100644
index af630dea..00000000
--- a/rhodecode/public/js/src/components/rhodecode-favicon/rhodecode-favicon.html
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
diff --git a/rhodecode/public/js/src/components/rhodecode-favicon/rhodecode-favicon.js b/rhodecode/public/js/src/components/rhodecode-favicon/rhodecode-favicon.js
new file mode 100644
index 00000000..b78d6226
--- /dev/null
+++ b/rhodecode/public/js/src/components/rhodecode-favicon/rhodecode-favicon.js
@@ -0,0 +1,33 @@
+import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
+
+export class RhodecodeFavicon extends PolymerElement {
+
+ static get is() {
+ return 'rhodecode-favicon';
+ }
+
+ static get properties() {
+ return {
+ favicon: Object,
+ counter: {
+ type: Number,
+ observer: '_handleCounter'
+ }
+ }
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.favicon = new Favico({
+ type: 'rectangle',
+ animation: 'none'
+ });
+ }
+
+ _handleCounter(newVal, oldVal) {
+ this.favicon.badge(this.counter);
+ }
+
+}
+
+customElements.define(RhodecodeFavicon.is, RhodecodeFavicon);
diff --git a/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.html b/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.html
deleted file mode 100644
index 071bb4a6..00000000
--- a/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.html
+++ /dev/null
@@ -1,233 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- [[_gettext('Close')]]
-
-
-
-
-
-
-
-
-
-
diff --git a/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js b/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js
new file mode 100644
index 00000000..379adeac
--- /dev/null
+++ b/rhodecode/public/js/src/components/rhodecode-toast/rhodecode-toast.js
@@ -0,0 +1,231 @@
+import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
+import '../rhodecode-unsafe-html/rhodecode-unsafe-html.js';
+
+export class RhodecodeToast extends mixinBehaviors([IronA11yKeysBehavior], PolymerElement) {
+
+ static get is() {
+ return 'rhodecode-toast';
+ }
+
+ static get template(){
+ return html`
+
+
+
+
+
+
+
+ [[_gettext('Close')]]
+
+
+
+
+
+
+ `
+ }
+
+ static get properties() {
+ return {
+ toasts: {
+ type: Array,
+ value() {
+ return []
+ }
+ },
+ isFixed: {
+ type: Boolean,
+ value: false
+ },
+ hasToasts: {
+ type: Boolean,
+ computed: '_computeHasToasts(toasts.*)'
+ },
+ keyEventTarget: {
+ type: Object,
+ value() {
+ return document.body;
+ }
+ }
+ }
+ }
+
+ get keyBindings() {
+ return {
+ 'esc:keyup': '_hideOnEsc'
+ }
+ }
+
+ static get observers() {
+ return [
+ '_changedToasts(toasts.splices)'
+ ]
+ }
+
+ _hideOnEsc(event) {
+ return this.dismissNotifications();
+ }
+
+ _computeHasToasts() {
+ return this.toasts.length > 0;
+ }
+
+ _debouncedCalc() {
+ // calculate once in a while
+ this.debounce('debouncedCalc', this.toastInWindow, 25);
+ }
+
+ conditionalClass() {
+ return this.isFixed ? 'fixed' : '';
+ }
+
+ toastInWindow() {
+ if (!this._headerNode) {
+ return true
+ }
+ var headerHeight = this._headerNode.offsetHeight;
+ var scrollPosition = window.scrollY;
+
+ if (this.isFixed) {
+ this.isFixed = 1 <= scrollPosition;
+ }
+ else {
+ this.isFixed = headerHeight <= scrollPosition;
+ }
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this._headerNode = document.querySelector('.header', document);
+ this.listen(window, 'scroll', '_debouncedCalc');
+ this.listen(window, 'resize', '_debouncedCalc');
+ this._debouncedCalc();
+ }
+
+ _changedToasts(newValue, oldValue) {
+ $.Topic('/favicon/update').publish({count: this.toasts.length});
+ }
+
+ dismissNotification(e) {
+ $.Topic('/favicon/update').publish({count: this.toasts.length - 1});
+ var idx = e.target.parentNode.indexPos
+ this.splice('toasts', idx, 1);
+
+ }
+
+ dismissNotifications() {
+ $.Topic('/favicon/update').publish({count: 0});
+ this.splice('toasts', 0);
+ }
+
+ handleNotification(data) {
+ if (!templateContext.rhodecode_user.notification_status && !data.message.force) {
+ // do not act if notifications are disabled
+ return
+ }
+ this.push('toasts', {
+ level: data.message.level,
+ message: data.message.message
+ });
+ }
+
+ _gettext(x){
+ return _gettext(x)
+ }
+
+}
+
+customElements.define(RhodecodeToast.is, RhodecodeToast);
diff --git a/rhodecode/public/js/src/components/rhodecode-toggle/rhodecode-toggle.html b/rhodecode/public/js/src/components/rhodecode-toggle/rhodecode-toggle.html
deleted file mode 100644
index 8af67abe..00000000
--- a/rhodecode/public/js/src/components/rhodecode-toggle/rhodecode-toggle.html
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
[[labelStatus(checked)]]
-
[[tooltipText]]
-
-
-
-
-
-
-
-
diff --git a/rhodecode/public/js/src/components/rhodecode-toggle/rhodecode-toggle.js b/rhodecode/public/js/src/components/rhodecode-toggle/rhodecode-toggle.js
new file mode 100644
index 00000000..a5852ada
--- /dev/null
+++ b/rhodecode/public/js/src/components/rhodecode-toggle/rhodecode-toggle.js
@@ -0,0 +1,57 @@
+import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+import '@polymer/paper-spinner/paper-spinner.js';
+import '@polymer/paper-tooltip/paper-tooltip.js';
+
+export class RhodecodeToggle extends PolymerElement {
+
+ static get is() {
+ return 'rhodecode-toggle';
+ }
+
+ static get template() {
+ return html`
+
+
+
[[labelStatus(checked)]]
+
+
[[tooltipText]]
+
+
+
+
+ `;
+ }
+
+ static get properties() {
+ return {
+ noSpinner: {type: Boolean, value: false, reflectToAttribute: true},
+ tooltipText: {type: String, value: "Click to toggle", reflectToAttribute: true},
+ checked: {type: Boolean, value: false, reflectToAttribute: true},
+ active: {type: Boolean, value: false, reflectToAttribute: true, notify: true}
+ }
+ }
+
+ shouldShow() {
+ return !this.noSpinner
+ }
+
+ labelStatus(isActive) {
+ return this.checked ? 'Enabled' : "Disabled"
+ }
+}
+
+customElements.define(RhodecodeToggle.is, RhodecodeToggle);
diff --git a/rhodecode/public/js/src/components/rhodecode-unsafe-html/rhodecode-unsafe-html.html b/rhodecode/public/js/src/components/rhodecode-unsafe-html/rhodecode-unsafe-html.html
deleted file mode 100644
index 143f1bf5..00000000
--- a/rhodecode/public/js/src/components/rhodecode-unsafe-html/rhodecode-unsafe-html.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/rhodecode/public/js/src/components/rhodecode-unsafe-html/rhodecode-unsafe-html.js b/rhodecode/public/js/src/components/rhodecode-unsafe-html/rhodecode-unsafe-html.js
new file mode 100644
index 00000000..7290a207
--- /dev/null
+++ b/rhodecode/public/js/src/components/rhodecode-unsafe-html/rhodecode-unsafe-html.js
@@ -0,0 +1,30 @@
+import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
+
+export class RhodecodeUnsafeHtml extends PolymerElement {
+
+ static get is() {
+ return 'rhodecode-unsafe-html';
+ }
+
+ static get template() {
+ return html`
+
+
+ `;
+ }
+
+ static get properties() {
+ return {
+ text: {
+ type: String,
+ observer: '_handleText'
+ }
+ }
+ }
+
+ _handleText(newVal, oldVal) {
+ this.innerHTML = this.text;
+ }
+}
+
+customElements.define(RhodecodeUnsafeHtml.is, RhodecodeUnsafeHtml);