Merge pull request !2871 from rhodecode-enterprise-ce Polymer-lit-migration-1

Changes from branch: Polymer lit migration 1
This commit is contained in:
Andrii Verbytskyi 2025-12-10 16:17:29 +00:00
commit 10ad0686cb
29 changed files with 5694 additions and 14679 deletions

View file

@ -4,10 +4,9 @@
"env", "env",
{ {
"targets": { "targets": {
"browsers": [ "esmodules": true
"last 2 versions" },
] "exclude": ["transform-es2015-classes"]
}
} }
] ]
], ],

2
.gitignore vendored
View file

@ -39,7 +39,7 @@ rhodecode/public/js/src/components/**/*.css
/rcextensions/ /rcextensions/
/result /result
/rhodecode/public/css/style.css /rhodecode/public/css/style.css
/rhodecode/public/css/style-polymer.css /rhodecode/public/css/style-lit.css
/rhodecode/public/css/style-ipython.css /rhodecode/public/css/style-ipython.css
/rhodecode/public/js/rhodecode-components.html /rhodecode/public/js/rhodecode-components.html
/rhodecode/public/js/rhodecode-components.js /rhodecode/public/js/rhodecode-components.js

View file

@ -13,10 +13,10 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-less'); grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-terser');
grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-webpack'); grunt.loadNpmTasks('grunt-webpack');
grunt.registerTask('default', ['less:production', 'less:components', 'copy', 'webpack', 'concat:dist', 'uglify:dist']); grunt.registerTask('default', ['less:production', 'less:components', 'copy', 'webpack', 'concat:dist', 'terser:dist']);
}; };

View file

@ -160,9 +160,10 @@ docs-cleanup:
.PHONY: web-build .PHONY: web-build
# web-build: Build JS packages static/js # web-build: Build JS packages static/js
# TODO need to upgrade to webpack 5 and remove -e NODE_OPTIONS=--openssl-legacy-provider
web-build: web-build:
rm -rf node_modules rm -rf node_modules
docker run -it --rm -v $(PWD):/project --workdir=/project rhodecode/static-files-build:16 -c "npm install && /project/node_modules/.bin/grunt" docker run -it --rm -e NODE_OPTIONS=--openssl-legacy-provider -v $(PWD):/project --workdir=/project rhodecode/static-files-build:24 -c "npm install && /project/node_modules/.bin/grunt"
# run static file check # run static file check
./rhodecode/tests/scripts/static-file-check.sh rhodecode/public/ ./rhodecode/tests/scripts/static-file-check.sh rhodecode/public/
rm -rf node_modules rm -rf node_modules

View file

@ -667,13 +667,14 @@ search.location = %(here)s/data/index
channelstream.enabled = true channelstream.enabled = true
; server address for channelstream server on the backend ; server address for channelstream server on the backend
channelstream.server = channelstream:9800 channelstream.server = channelstream:8000
; location of the channelstream server from outside world ; location of the channelstream server from outside world
; use ws:// for http or wss:// for https. This address needs to be handled ; use ws:// for http or wss:// for https. This address needs to be handled
; by external HTTP server such as Nginx or Apache ; by external HTTP server such as Nginx or Apache
; see Nginx/Apache configuration examples in our docs ; see Nginx/Apache configuration examples in our docs
channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream ; For development, comment this out to use auto-generated proxy URL
;channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream
channelstream.secret = ENV_GENERATED channelstream.secret = ENV_GENERATED
channelstream.history.location = /var/opt/rhodecode_data/channelstream_history channelstream.history.location = /var/opt/rhodecode_data/channelstream_history

View file

@ -629,7 +629,7 @@ search.location = %(here)s/data/index
channelstream.enabled = true channelstream.enabled = true
; server address for channelstream server on the backend ; server address for channelstream server on the backend
channelstream.server = channelstream:9800 channelstream.server = channelstream:8000
; location of the channelstream server from outside world ; location of the channelstream server from outside world
; use ws:// for http or wss:// for https. This address needs to be handled ; use ws:// for http or wss:// for https. This address needs to be handled

View file

@ -52,10 +52,12 @@ Supported Databases
Supported Browsers Supported Browsers
------------------ ------------------
* Chrome * Chrome 67+
* Safari * Firefox 63+
* Firefox * Safari 11.1+
* Internet Explorer 10 & 11 * Edge 79+
Note: Internet Explorer is no longer supported
System Requirements System Requirements
------------------- -------------------

View file

@ -21,8 +21,8 @@
"dest": "<%= dirs.js.dest %>/vendors" "dest": "<%= dirs.js.dest %>/vendors"
}, },
{ {
"src": "<%= dirs.css.src %>/style-polymer.css", "src": "<%= dirs.css.src %>/style-lit.css",
"dest": "<%= dirs.js.dest %>/src/components/style-polymer.css" "dest": "<%= dirs.js.dest %>/src/components/style-lit.css"
} }
] ]
} }
@ -102,10 +102,47 @@
"nonull": true "nonull": true
} }
}, },
"uglify": { "terser": {
"dist": { "dist": {
"src": "<%= dirs.js.dest %>/scripts.js", "options": {
"dest": "<%= dirs.js.dest %>/scripts.min.js" "compress": {
"ecma": 6,
"arrows": false,
"collapse_vars": false,
"comparisons": false,
"computed_props": false,
"hoist_funs": false,
"hoist_props": false,
"hoist_vars": false,
"inline": false,
"loops": false,
"negate_iife": false,
"properties": false,
"reduce_funcs": false,
"reduce_vars": false,
"switches": false,
"toplevel": false,
"typeofs": false,
"booleans": true,
"if_return": true,
"sequences": true,
"unused": true,
"conditionals": true,
"dead_code": true,
"evaluate": true
},
"mangle": {
"keep_classnames": true,
"keep_fnames": false,
"reserved": ["noChange", "nothing"]
},
"output": {
"ecma": 6
}
},
"files": {
"<%= dirs.js.dest %>/scripts.min.js": "<%= dirs.js.dest %>/scripts.js"
}
} }
}, },
"less": { "less": {
@ -113,11 +150,12 @@
"options": { "options": {
"compress": false, "compress": false,
"yuicompress": false, "yuicompress": false,
"optimization": 0 "optimization": 0,
"math": "always"
}, },
"files": { "files": {
"<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less", "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
"<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less", "<%= dirs.css.dest %>/style-lit.css": "<%= dirs.css.src %>/lit-components.less",
"<%= dirs.css.dest %>/style-ipython.css": "<%= dirs.css.src %>/ipython.less" "<%= dirs.css.dest %>/style-ipython.css": "<%= dirs.css.src %>/ipython.less"
} }
}, },
@ -125,15 +163,19 @@
"options": { "options": {
"compress": true, "compress": true,
"yuicompress": true, "yuicompress": true,
"optimization": 2 "optimization": 2,
"math": "always"
}, },
"files": { "files": {
"<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less", "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
"<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less", "<%= dirs.css.dest %>/style-lit.css": "<%= dirs.css.src %>/lit-components.less",
"<%= dirs.css.dest %>/style-ipython.css": "<%= dirs.css.src %>/ipython.less" "<%= dirs.css.dest %>/style-ipython.css": "<%= dirs.css.src %>/ipython.less"
} }
}, },
"components": { "components": {
"options": {
"math": "always"
},
"files": [ "files": [
{ {
"cwd": "<%= dirs.js.src %>/components/", "cwd": "<%= dirs.js.src %>/components/",
@ -156,7 +198,6 @@
"tasks": [ "tasks": [
"less:development", "less:development",
"less:components", "less:components",
"concat:polymercss",
"webpack", "webpack",
"concat:dist" "concat:dist"
] ]
@ -169,7 +210,6 @@
], ],
"tasks": [ "tasks": [
"less:components", "less:components",
"concat:polymercss",
"webpack", "webpack",
"concat:dist" "concat:dist"
] ]

18420
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,18 +9,11 @@
"url": "https://code.rhodecode.com/rhodecode-enterprise-ce" "url": "https://code.rhodecode.com/rhodecode-enterprise-ce"
}, },
"devDependencies": { "devDependencies": {
"@polymer/iron-a11y-keys": "^3.0.0", "@webcomponents/webcomponentsjs": "^2.8.0",
"@polymer/iron-ajax": "^3.0.0",
"@polymer/iron-autogrow-textarea": "^3.0.0",
"@polymer/paper-button": "^3.0.0",
"@polymer/paper-spinner": "^3.0.0",
"@polymer/paper-toast": "^3.0.0",
"@polymer/paper-toggle-button": "^3.0.0",
"@polymer/paper-tooltip": "^3.0.0",
"@polymer/polymer": "^3.0.0",
"@webcomponents/webcomponentsjs": "^2.0.0",
"babel-core": "^6.26.3", "babel-core": "^6.26.3",
"babel-loader": "^7.1.2", "babel-loader": "^7.1.2",
"babel-plugin-syntax-class-properties": "^6.13.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-env": "^1.6.0", "babel-preset-env": "^1.6.0",
"clipboard": "^2.0.1", "clipboard": "^2.0.1",
@ -29,15 +22,15 @@
"dropzone": "^5.5.0", "dropzone": "^5.5.0",
"exports-loader": "^0.6.4", "exports-loader": "^0.6.4",
"favico.js": "^0.3.10", "favico.js": "^0.3.10",
"grunt": "^0.4.5", "grunt": "^1.6.1",
"grunt-cli": "^1.4.3", "grunt-cli": "^1.4.3",
"grunt-contrib-concat": "^0.5.1", "grunt-contrib-concat": "^2.1.0",
"grunt-contrib-copy": "^1.0.0", "grunt-contrib-copy": "^1.0.0",
"grunt-contrib-jshint": "^0.12.0", "grunt-contrib-jshint": "^3.2.0",
"grunt-contrib-less": "^1.1.0", "grunt-contrib-less": "^3.0.0",
"grunt-contrib-uglify": "^4.0.1", "grunt-contrib-watch": "^1.1.0",
"grunt-contrib-watch": "^0.6.1", "grunt-terser": "^2.0.0",
"grunt-webpack": "^3.1.3", "grunt-webpack": "^4.0.3",
"html-loader": "^0.4.4", "html-loader": "^0.4.4",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"imports-loader": "^0.7.1", "imports-loader": "^0.7.1",
@ -47,15 +40,17 @@
"luxon": "^3.7.2", "luxon": "^3.7.2",
"mark.js": "8.11.1", "mark.js": "8.11.1",
"mousetrap": "^1.6.1", "mousetrap": "^1.6.1",
"polymer-webpack-loader": "^2.0.1",
"raw-loader": "1.0.0-beta.0", "raw-loader": "1.0.0-beta.0",
"sticky-sidebar": "3.3.1", "sticky-sidebar": "3.3.1",
"style-loader": "^0.21.0", "style-loader": "^0.21.0",
"sweetalert2": "^9.10.12", "sweetalert2": "^9.10.12",
"ts-loader": "^1.3.3", "ts-loader": "^1.3.3",
"waypoints": "4.0.1", "waypoints": "4.0.1",
"webpack": "4.23.1", "webpack": "4.46.0",
"webpack-cli": "3.1.2", "webpack-cli": "3.3.12"
"webpack-uglify-js-plugin": "^1.1.9" },
"dependencies": {
"lit": "^2.8.0",
"regenerator-runtime": "^0.13.9"
} }
} }

View file

@ -25,6 +25,8 @@ controllers
import logging import logging
import socket import socket
import base64 import base64
import hashlib
import os
import markupsafe import markupsafe
import ipaddress import ipaddress

View file

@ -249,13 +249,13 @@
.field-sm { .field-sm {
.label { .label {
padding-top: @input-padding-px / 2 + @input-border-thickness; padding-top: (@input-padding-px / 2) + @input-border-thickness;
} }
.checkboxes, .checkboxes,
.radios { .radios {
// TODO: johbo: We get a 4px margin from the from-bootstrap, // TODO: johbo: We get a 4px margin from the from-bootstrap,
// compensating here to align well with labels on the left. // compensating here to align well with labels on the left.
padding-top: @input-padding-px / 2 + @input-border-thickness - 3px; padding-top: (@input-padding-px / 2) + @input-border-thickness - 3px;
} }
} }

View file

@ -2117,7 +2117,7 @@ BIN_FILENODE = 7
.notification-header{ .notification-header{
display: table; display: table;
width: 100%; width: 100%;
padding: floor(@basefontsize/2) 0; padding: floor((@basefontsize / 2)) 0;
line-height: 1em; line-height: 1em;
.desc, .delete-notifications, .read-notifications{ .desc, .delete-notifications, .read-notifications{

View file

@ -56,7 +56,7 @@
.circle (@radius:20,@color:#000) { .circle (@radius:20,@color:#000) {
height: @radius; height: @radius;
width: @radius; width: @radius;
padding: round(@radius/2); padding: round((@radius / 2));
font-size: @radius; font-size: @radius;
line-height:1em; line-height:1em;
.border-radius(50%); .border-radius(50%);

View file

@ -1,577 +1,445 @@
import {Polymer, html} from '@polymer/polymer/polymer-legacy'; import {LitElement} from 'lit';
import '@polymer/iron-ajax/iron-ajax.js';
const elemTemplate = html` /**
<iron-ajax * Simplified Lit version of channelstream-connection
id="ajaxConnect" * Replaces iron-ajax with native fetch()
url="" */
handle-as="json" export class ChannelstreamConnection extends LitElement {
method="post" static properties = {
content-type="application/json" channels: {type: Array},
loading="{{loadingConnect}}" username: {type: String},
last-response="{{connectLastResponse}}" connectionId: {type: String},
on-response="_handleConnect" websocket: {type: Object},
on-error="_handleConnectError" websocketUrl: {type: String},
debounce-duration="100"></iron-ajax> connectUrl: {type: String},
disconnectUrl: {type: String},
subscribeUrl: {type: String},
unsubscribeUrl: {type: String},
messageUrl: {type: String},
longPollUrl: {type: String},
shouldReconnect: {type: Boolean},
heartbeats: {type: Boolean},
increaseBounceIv: {type: Number},
_currentBounceIv: {type: Number},
useWebsocket: {type: Boolean},
connected: {type: Boolean},
channelsState: {type: Object},
userState: {type: Object}
};
<iron-ajax constructor() {
id="ajaxDisconnect" super();
url="" this.channels = [];
handle-as="json" this.username = 'Anonymous';
method="post" this.connectionId = '';
content-type="application/json" this.websocket = null;
loading="{{loadingDisconnect}}" this.websocketUrl = '';
last-response="{{_disconnectLastResponse}}" this.connectUrl = '';
on-response="_handleDisconnect" this.disconnectUrl = '';
debounce-duration="100"></iron-ajax> this.subscribeUrl = '';
this.unsubscribeUrl = '';
this.messageUrl = '';
this.longPollUrl = '';
this.shouldReconnect = true;
this.heartbeats = true;
this.increaseBounceIv = 2000;
this._currentBounceIv = 0;
this.useWebsocket = true;
this.connected = false;
this.channelsState = {};
this.userState = {};
<iron-ajax // Mutators - simplified to just arrays
id="ajaxSubscribe" this.mutators = {
url="" connect: [],
handle-as="json" message: [],
method="post" subscribe: [],
content-type="application/json" unsubscribe: [],
loading="{{loadingSubscribe}}" disconnect: []
last-response="{{subscribeLastResponse}}" };
on-response="_handleSubscribe"
debounce-duration="100"></iron-ajax>
<iron-ajax this._heartbeatInterval = null;
id="ajaxUnsubscribe" }
url=""
handle-as="json"
method="post"
content-type="application/json"
loading="{{loadingUnsubscribe}}"
last-response="{{unsubscribeLastResponse}}"
on-response="_handleUnsubscribe"
debounce-duration="100"></iron-ajax>
<iron-ajax // No render needed - this component has no template
id="ajaxMessage" render() {
url="" return null;
handle-as="json" }
method="post"
content-type="application/json"
loading="{{loadingMessage}}"
last-response="{{messageLastResponse}}"
on-response="_handleMessage"
on-error="_handleMessageError"
debounce-duration="100"></iron-ajax>
<iron-ajax updated(changedProperties) {
id="ajaxListen" if (changedProperties.has('channels')) {
url="" this.dispatchEvent(new CustomEvent('channelstream-channels-changed', {
handle-as="text" detail: {value: this.channels}
loading="{{loadingListen}}" }));
last-response="{{listenLastResponse}}"
on-request="_handleListenOpen"
on-error="_handleListenError"
on-response="_handleListenMessageEvent"
debounce-duration="100"></iron-ajax>
`
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: [ connectedCallback() {
'_handleChannelsChange(channels.splices)' super.connectedCallback();
],
listeners: { // Set up self-event listeners (replaces Lit's 'listeners' property)
'channelstream-connected': 'startListening', this.addEventListener('channelstream-connected', this.startListening.bind(this));
'channelstream-connect-error': 'retryConnection', this.addEventListener('channelstream-connect-error', this.retryConnection.bind(this));
}, }
/** /**
* Mutators hold functions that you can set locally to change the data * Helper method for fetch requests
* 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: { async _fetchJSON(url, options = {}) {
connect: function () { const {body, headers, ...fetchOptions} = options;
return []
}(), const response = await fetch(url, {
message: function () { method: 'POST',
return [] headers: {
}(), 'Content-Type': 'application/json',
subscribe: function () { ...headers
return [] },
}(), body: body ? JSON.stringify(body) : undefined,
unsubscribe: function () { ...fetchOptions
return [] });
}(),
disconnect: function () { if (!response.ok) {
return [] throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}() }
},
ready: function () { return response.json();
this.isReady = true; }
},
/** /**
* Connects user and fetches connection id from the server. * Connects user and fetches connection id from the server
*
*/ */
connect: function () { async connect() {
var request = this.$['ajaxConnect']; const body = {
request.url = this.connectUrl;
request.body = {
username: this.username, username: this.username,
channels: this.channels channels: this.channels
}; };
for (var i = 0; i < this.mutators.connect.length; i++) {
this.mutators.connect[i](request); // Apply mutators
for (let mutator of this.mutators.connect) {
mutator({body});
} }
request.generateRequest()
}, try {
const response = await this._fetchJSON(this.connectUrl, {body});
this._handleConnect({detail: {response}});
} catch (error) {
this._handleConnectError({detail: {error}});
}
}
/** /**
* Overwrite with custom function that will * Subscribes user to channels
*/ */
addMutator: function (type, func) { async subscribe(channels) {
this.mutators[type].push(func); const body = {
},
/**
* Subscribes user to channels.
*
*/
subscribe: function (channels) {
var request = this.$['ajaxSubscribe'];
request.url = this.subscribeUrl;
request.body = {
channels: channels, channels: channels,
conn_id: this.connectionId 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; // Apply mutators
request.body = { for (let mutator of this.mutators.subscribe) {
channels: unsubscribe, mutator({body});
}
if (body.channels.length) {
try {
const response = await this._fetchJSON(this.subscribeUrl, {body});
this._handleSubscribe({detail: {response}});
} catch (error) {
this.dispatchEvent(new CustomEvent('channelstream-subscribe-error', {detail: error}));
}
}
}
/**
* Unsubscribes user from channels
*/
async unsubscribe(channels) {
const body = {
channels: channels,
conn_id: this.connectionId conn_id: this.connectionId
}; };
for (var i = 0; i < this.mutators.unsubscribe.length; i++) {
this.mutators.unsubscribe[i](request); // Apply mutators
for (let mutator of this.mutators.unsubscribe) {
mutator({body});
} }
request.generateRequest()
}, try {
const response = await this._fetchJSON(this.unsubscribeUrl, {body});
this._handleUnsubscribe({detail: {response}});
} catch (error) {
this.dispatchEvent(new CustomEvent('channelstream-unsubscribe-error', {detail: error}));
}
}
/** /**
* calculates list of channels we should add user to based on difference * Calculates list of channels to add user to
* between channels property and passed channel list
*/ */
calculateSubscribe: function (channels) { calculateSubscribe(channels) {
var currentlySubscribed = this.channels; const currentlySubscribed = this.channels;
var toSubscribe = []; const toSubscribe = [];
for (var i = 0; i < channels.length; i++) { for (let i = 0; i < channels.length; i++) {
if (currentlySubscribed.indexOf(channels[i]) === -1) { if (currentlySubscribed.indexOf(channels[i]) === -1) {
toSubscribe.push(channels[i]); toSubscribe.push(channels[i]);
} }
} }
return toSubscribe return toSubscribe;
}, }
/** /**
* calculates list of channels we should remove user from based difference * Calculates list of channels to remove user from
* between channels property and passed channel list
*/ */
calculateUnsubscribe: function (channels) { calculateUnsubscribe(channels) {
var currentlySubscribed = this.channels; const currentlySubscribed = this.channels;
var toUnsubscribe = []; const toUnsubscribe = [];
for (var i = 0; i < channels.length; i++) { for (let i = 0; i < channels.length; i++) {
if (currentlySubscribed.indexOf(channels[i]) !== -1) { if (currentlySubscribed.indexOf(channels[i]) !== -1) {
toUnsubscribe.push(channels[i]); toUnsubscribe.push(channels[i]);
} }
} }
return toUnsubscribe 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. * Marks the connection as expired
*
*/ */
message: function (message) { async disconnect() {
var request = this.$['ajaxMessage']; const params = new URLSearchParams({conn_id: this.connectionId});
request.url = this.messageUrl;
request.body = message; // Apply mutators
for (var i = 0; i < this.mutators.message.length; i++) { for (let mutator of this.mutators.disconnect) {
this.mutators.message[i](request) mutator({params});
} }
request.generateRequest();
}, try {
const response = await fetch(`${this.disconnectUrl}?${params}`);
const data = await response.json();
this._handleDisconnect({detail: {response: data}});
} catch (error) {
}
// Disconnect existing connection
this.closeConnection();
}
/** /**
* Opens "long lived" (websocket/longpoll) connection to the channelstream server. * Sends a message to the server
*
*/ */
startListening: function (event) { async message(messageData) {
this.fire('start-listening', {}); const body = messageData;
// Apply mutators
for (let mutator of this.mutators.message) {
mutator({body});
}
try {
const response = await this._fetchJSON(this.messageUrl, {body});
this._handleMessage({detail: {response}});
} catch (error) {
this._handleMessageError({detail: {error}});
}
}
/**
* Opens long lived connection (websocket/longpoll)
*/
startListening(event) {
this.dispatchEvent(new CustomEvent('start-listening', {detail: {}}));
if (this.useWebsocket) { if (this.useWebsocket) {
this.useWebsocket = window.WebSocket ? true : false; this.useWebsocket = window.WebSocket ? true : false;
} }
if (this.useWebsocket) { if (this.useWebsocket) {
this.openWebsocket(); this.openWebsocket();
} } else {
else {
this.openLongPoll(); this.openLongPoll();
} }
}, }
/** /**
* Opens websocket connection. * Opens websocket connection
*
*/ */
openWebsocket: function () { openWebsocket() {
var url = this.websocketUrl + '?conn_id=' + this.connectionId; const url = this.websocketUrl + '?conn_id=' + this.connectionId;
this.websocket = new WebSocket(url); this.websocket = new WebSocket(url);
this.websocket.onopen = this._handleListenOpen.bind(this); this.websocket.onopen = this._handleListenOpen.bind(this);
this.websocket.onclose = this._handleListenCloseEvent.bind(this); this.websocket.onclose = this._handleListenCloseEvent.bind(this);
this.websocket.onerror = this._handleListenErrorEvent.bind(this); this.websocket.onerror = this._handleListenErrorEvent.bind(this);
this.websocket.onmessage = this._handleListenMessageEvent.bind(this); this.websocket.onmessage = this._handleListenMessageEvent.bind(this);
}, }
/** /**
* Opens long-poll connection. * Opens long-poll connection
*
*/ */
openLongPoll: function () { async openLongPoll() {
var request = this.$['ajaxListen']; const url = this.longPollUrl + '?conn_id=' + this.connectionId;
request.url = this.longPollUrl + '?conn_id=' + this.connectionId;
request.generateRequest() try {
}, this._handleListenOpen();
const response = await fetch(url);
const text = await response.text();
this._handleListenMessageEvent({data: text});
} catch (error) {
this._handleListenError({detail: {error}});
}
}
/** /**
* Retries `connect()` call while incrementing interval between tries up to 1 minute. * Retries connect() call with increasing interval
*
*/ */
retryConnection: function () { retryConnection() {
if (!this.shouldReconnect) { if (!this.shouldReconnect) {
return; return;
} }
if (this._currentBounceIv < 60000) { if (this._currentBounceIv < 60000) {
this._currentBounceIv = this._currentBounceIv + this.increaseBounceIv; this._currentBounceIv = this._currentBounceIv + this.increaseBounceIv;
} } else {
else {
this._currentBounceIv = 60000; this._currentBounceIv = 60000;
} }
setTimeout(this.connect.bind(this), this._currentBounceIv); setTimeout(this.connect.bind(this), this._currentBounceIv);
}, }
/** /**
* Closes listening connection. * Closes listening connection
*
*/ */
closeConnection: function () { closeConnection() {
var request = this.$['ajaxListen'];
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) { if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
this.websocket.onclose = null; this.websocket.onclose = null;
this.websocket.onerror = null; this.websocket.onerror = null;
this.websocket.close(); this.websocket.close();
} }
if (request.loading) {
request.lastRequest.abort(); if (this._heartbeatInterval) {
clearInterval(this._heartbeatInterval);
this._heartbeatInterval = null;
} }
this.connected = false; this.connected = false;
}, }
_handleChannelsChange: function (event) { /**
// do not fire the event if set() didn't mutate anything * Add mutator function
// is this a reliable way to do it? */
if (!this.isReady || event === undefined) { addMutator(type, func) {
return this.mutators[type].push(func);
}
/**
* Create heartbeat interval
*/
createHeartBeats() {
if (!this._heartbeatInterval && this.websocket !== null && this.heartbeats) {
this._heartbeatInterval = setInterval(this._sendHeartBeat.bind(this), 10000);
} }
this.fire('channelstream-channels-changed', event) }
},
_handleListenOpen: function (event) { /**
this.connected = true; * Send heartbeat
this.fire('channelstream-listen-opened', event); */
this.createHeartBeats(); _sendHeartBeat() {
}, if (this.websocket && this.websocket.readyState === WebSocket.OPEN && this.heartbeats) {
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'})); 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);
} }
});
// Event handlers
_handleListenOpen(event) {
this.connected = true;
this.dispatchEvent(new CustomEvent('channelstream-listen-opened', {detail: event}));
this.createHeartBeats();
}
_handleListenError(event) {
this.connected = false;
this.retryConnection();
}
_handleConnectError(event) {
this.connected = false;
this.dispatchEvent(new CustomEvent('channelstream-connect-error', {detail: event.detail}));
}
_handleListenMessageEvent(event) {
let data = null;
// From long-poll (fetch)
if (event.detail && event.detail.response) {
data = JSON.parse(event.detail.response);
setTimeout(this.openLongPoll.bind(this), 0);
}
// From websocket
else if (event.data) {
data = JSON.parse(event.data);
}
if (data) {
this.dispatchEvent(new CustomEvent('channelstream-listen-message', {detail: data}));
}
}
_handleListenCloseEvent(event) {
this.connected = false;
this.dispatchEvent(new CustomEvent('channelstream-listen-closed', {detail: event}));
this.retryConnection();
}
_handleListenErrorEvent(event) {
this.connected = false;
this.dispatchEvent(new CustomEvent('channelstream-listen-error', {detail: {}}));
}
_handleConnect(event) {
this._currentBounceIv = 0;
this.connectionId = event.detail.response.conn_id;
this.dispatchEvent(new CustomEvent('channelstream-connected', {detail: event.detail.response}));
}
_handleDisconnect(event) {
this.connected = false;
this.dispatchEvent(new CustomEvent('channelstream-disconnected', {detail: {}}));
}
_handleMessage(event) {
this.dispatchEvent(new CustomEvent('channelstream-message-sent', {detail: event.detail.response}));
}
_handleMessageError(event) {
this.dispatchEvent(new CustomEvent('channelstream-message-error', {detail: event.detail}));
}
_handleSubscribe(event) {
this.dispatchEvent(new CustomEvent('channelstream-subscribed', {detail: event.detail.response}));
}
_handleUnsubscribe(event) {
this.dispatchEvent(new CustomEvent('channelstream-unsubscribed', {detail: event.detail.response}));
}
// Polymer compatibility methods - keep these for rhodecode-app
push(path, value) {
if (path === 'channels') {
this.channels = [...this.channels, value];
}
}
set(path, value) {
if (typeof path === 'string') {
this[path] = value;
this.requestUpdate(path);
} else if (Array.isArray(path)) {
// Handle nested paths like ['channelsState', key] or ['userState', key]
if (path.length === 2 && path[0] === 'channelsState') {
this.channelsState = {...this.channelsState, [path[1]]: value};
} else if (path.length === 2 && path[0] === 'userState') {
this.userState = {...this.userState, [path[1]]: value};
}
}
}
}
customElements.define('channelstream-connection', ChannelstreamConnection);

View file

@ -1,4 +1,4 @@
import '@polymer/iron-ajax/iron-ajax.js'; import 'regenerator-runtime/runtime.js';
import './shared-styles.js'; import './shared-styles.js';
import './channelstream-connection/channelstream-connection.js'; import './channelstream-connection/channelstream-connection.js';
import './rhodecode-toast/rhodecode-toast.js'; import './rhodecode-toast/rhodecode-toast.js';

View file

@ -1,211 +1,222 @@
import {PolymerElement, html} from '@polymer/polymer/polymer-element.js'; import { html, LitElement, css } from "lit";
import '../channelstream-connection/channelstream-connection.js';
import '../rhodecode-toast/rhodecode-toast.js';
import '../rhodecode-favicon/rhodecode-favicon.js';
var ccLog = Logger.get('RhodeCodeApp'); 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); ccLog.setLevel(Logger.OFF);
export class RhodecodeApp extends PolymerElement { export class RhodecodeApp extends LitElement {
static get is() {
return "rhodecode-app";
}
static get is() { render() {
return 'rhodecode-app'; return html`
<channelstream-connection
id="channelstream-connection"
@channelstream-listen-message="${this.receivedMessage}"
@channelstream-connected="${this.handleConnected}"
@channelstream-subscribed="${this.handleSubscribed}"
>
</channelstream-connection>
<rhodecode-favicon></rhodecode-favicon>
`;
}
connectedCallback() {
super.connectedCallback();
ccLog.debug("rhodeCodeApp created");
}
firstUpdated() {
$.Topic("/notifications").subscribe(this.handleNotifications.bind(this));
$.Topic("/comment").subscribe(this.handleComment.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),
);
}
static get template(){ initPlugins() {
return html` for (var i = 0; i < window.APPLICATION_PLUGINS.length; i++) {
<channelstream-connection var pluginDef = window.APPLICATION_PLUGINS[i];
id="channelstream-connection" if (pluginDef.component) {
on-channelstream-listen-message="receivedMessage" var pluginElem = document.createElement(pluginDef.component);
on-channelstream-connected="handleConnected" this.shadowRoot.appendChild(pluginElem);
on-channelstream-subscribed="handleSubscribed"> if (typeof pluginElem.init !== "undefined") {
</channelstream-connection> pluginElem.init();
<rhodecode-favicon></rhodecode-favicon>
`
}
connectedCallback() {
super.connectedCallback();
ccLog.debug('rhodeCodeApp created');
$.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
$.Topic('/comment').subscribe(this.handleComment.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)); /** proxy to channelstream connection */
getChannelStreamConnection() {
return this.renderRoot.querySelector("#channelstream-connection");
}
handleNotifications(data) {
var elem = document.getElementById("notifications");
if (elem) {
elem.handleNotification(data);
}
}
handleComment(data) {
if (data.message.comment_data.length !== 0) {
if (window.refreshAllComments !== undefined) {
refreshAllComments();
}
var json_data = data.message.comment_data;
if (window.commentsController !== undefined) {
window.commentsController.attachComment(json_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";
channelstreamConnection.username =
window.templateContext.rhodecode_user.username;
// 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);
} }
initPlugins() { if (templateContext.commit_data.commit_id) {
for (var i = 0; i < window.APPLICATION_PLUGINS.length; i++) { var channelName =
var pluginDef = window.APPLICATION_PLUGINS[i]; "/repo$" +
if (pluginDef.component) { templateContext.repo_name +
var pluginElem = document.createElement(pluginDef.component); "$/commit/" +
this.shadowRoot.appendChild(pluginElem); String(templateContext.commit_data.commit_id);
if (typeof pluginElem.init !== 'undefined') { channels.push(channelName);
pluginElem.init(); }
}
} 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]);
} }
}
} }
}
/** proxy to channelstream connection */ /** publish received messages into correct topic */
getChannelStreamConnection() { receivedMessage(event) {
return this.$['channelstream-connection']; 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);
}
} }
}
handleNotifications(data) { handleConnected(event) {
var elem = document.getElementById('notifications'); var channelstreamConnection = this.getChannelStreamConnection();
if (elem) { channelstreamConnection.set("channelsState", event.detail.channels_info);
elem.handleNotification(data); 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();
}
handleComment(data) { /** propagates channel states on topics */
propagageChannelsState(event) {
if (data.message.comment_data.length !== 0) { var channelstreamConnection = this.getChannelStreamConnection();
if (window.refreshAllComments !== undefined) { var channel_data = channelstreamConnection.channelsState;
refreshAllComments() var channels = channelstreamConnection.channels;
} for (var i = 0; i < channels.length; i++) {
var json_data = data.message.comment_data; var key = channels[i];
$.Topic("/connection_controller/channel_update").publish({
if (window.commentsController !== undefined) { channel: key,
state: channel_data[key],
window.commentsController.attachComment(json_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); customElements.define(RhodecodeApp.is, RhodecodeApp);

View file

@ -1,33 +1,30 @@
import {PolymerElement, html} from '@polymer/polymer/polymer-element.js'; import {LitElement} from 'lit';
export class RhodecodeFavicon extends PolymerElement { export class RhodecodeFavicon extends LitElement {
static properties = {
favicon: {type: Object},
counter: {type: Number}
};
static get is() { constructor() {
return 'rhodecode-favicon'; super();
this.favicon = null;
this.counter = 0;
}
connectedCallback() {
super.connectedCallback();
this.favicon = new Favico({
type: 'rectangle',
animation: 'none'
});
}
updated(changedProperties) {
if (changedProperties.has('counter') && this.favicon) {
this.favicon.badge(this.counter);
} }
}
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); customElements.define('rhodecode-favicon', RhodecodeFavicon);

View file

@ -1,231 +1,241 @@
import {PolymerElement, html} from '@polymer/polymer/polymer-element.js'; import {LitElement, html, css} from 'lit';
import '@polymer/paper-toggle-button/paper-toggle-button.js'; import {classMap} from 'lit/directives/class-map.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'; import '../rhodecode-unsafe-html/rhodecode-unsafe-html.js';
export class RhodecodeToast extends mixinBehaviors([IronA11yKeysBehavior], PolymerElement) { export class RhodecodeToast extends LitElement {
static styles = css`
static get is() { .absolute-center {
return 'rhodecode-toast'; margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
} }
static get template(){ .top-left-rounded-corner {
return html` -webkit-border-top-left-radius: 2px;
<style include="shared-styles"> -khtml-border-radius-topleft: 2px;
/* inset border for buttons - does not work in ie */ border-top-left-radius: 2px;
/* rounded borders */ }
/* rounded borders - bottom only */
/* rounded borders - top only */
/* text shadow */
/* centers text in a circle - input diameter of circle and color */
/* pill version of the circle */
.absolute-center {
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.top-left-rounded-corner { .top-right-rounded-corner {
-webkit-border-top-left-radius: 2px; -webkit-border-top-right-radius: 2px;
-khtml-border-radius-topleft: 2px; -khtml-border-radius-topright: 2px;
border-top-left-radius: 2px; border-top-right-radius: 2px;
} }
.top-right-rounded-corner { .bottom-left-rounded-corner {
-webkit-border-top-right-radius: 2px; -webkit-border-bottom-left-radius: 2px;
-khtml-border-radius-topright: 2px; -khtml-border-radius-bottomleft: 2px;
border-top-right-radius: 2px; border-bottom-left-radius: 2px;
} }
.bottom-left-rounded-corner { .bottom-right-rounded-corner {
-webkit-border-bottom-left-radius: 2px; -webkit-border-bottom-right-radius: 2px;
-khtml-border-radius-bottomleft: 2px; -khtml-border-radius-bottomright: 2px;
border-bottom-left-radius: 2px; border-bottom-right-radius: 2px;
} }
.bottom-right-rounded-corner { .top-left-rounded-corner-mid {
-webkit-border-bottom-right-radius: 2px; -webkit-border-top-left-radius: 2px;
-khtml-border-radius-bottomright: 2px; -khtml-border-radius-topleft: 2px;
border-bottom-right-radius: 2px; border-top-left-radius: 2px;
} }
.top-left-rounded-corner-mid { .top-right-rounded-corner-mid {
-webkit-border-top-left-radius: 2px; -webkit-border-top-right-radius: 2px;
-khtml-border-radius-topleft: 2px; -khtml-border-radius-topright: 2px;
border-top-left-radius: 2px; border-top-right-radius: 2px;
} }
.top-right-rounded-corner-mid { .bottom-left-rounded-corner-mid {
-webkit-border-top-right-radius: 2px; -webkit-border-bottom-left-radius: 2px;
-khtml-border-radius-topright: 2px; -khtml-border-radius-bottomleft: 2px;
border-top-right-radius: 2px; border-bottom-left-radius: 2px;
} }
.bottom-left-rounded-corner-mid { .bottom-right-rounded-corner-mid {
-webkit-border-bottom-left-radius: 2px; -webkit-border-bottom-right-radius: 2px;
-khtml-border-radius-bottomleft: 2px; -khtml-border-radius-bottomright: 2px;
border-bottom-left-radius: 2px; border-bottom-right-radius: 2px;
} }
.bottom-right-rounded-corner-mid { .alert {
-webkit-border-bottom-right-radius: 2px; clear: both;
-khtml-border-radius-bottomright: 2px; padding: 15px;
border-bottom-right-radius: 2px; margin: 10px 0;
} border: 1px solid;
border-radius: 2px;
color: #7E7F7F;
border-color: #84a5d2;
background-color: #e6edf6;
}
.alert { .alert-success {
margin: 10px 0; border-color: #0ac878;
} background-color: #daf7eb;
}
.toast-close { .alert-error {
margin: 0; border-color: #e85e4d;
float: right; background-color: #fbdfdb;
cursor: pointer; }
}
.toast-message-holder { .alert-warning {
background: rgba(255, 255, 255, 0.25); border-color: #ffc854;
} background-color: #fff4dd;
}
.toast-message-holder.fixed { .alert-info {
position: fixed; border-color: #84a5d2;
padding: 10px 0; background-color: #e6edf6;
margin-left: 10px; }
margin-right: 10px;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
</style>
<template is="dom-if" if="[[hasToasts]]"> .toast-close {
<div class$="container toast-message-holder [[conditionalClass(isFixed)]]"> margin: 0;
<template is="dom-repeat" items="[[toasts]]"> float: right;
<div class$="alert alert-[[item.level]]"> cursor: pointer;
<div on-click="dismissNotification" class="toast-close" index-pos="[[index]]"> }
<span>[[_gettext('Close')]]</span>
</div> .toast-message-holder {
<rhodecode-unsafe-html text="[[item.message]]"></rhodecode-unsafe-html> background: rgba(255, 255, 255, 0.25);
</div> }
</template>
.toast-message-holder.fixed {
position: fixed;
padding: 10px 0;
margin-left: 10px;
margin-right: 10px;
top: 0;
left: 0;
right: 0;
z-index: 100;
}
`;
static properties = {
toasts: {type: Array},
isFixed: {type: Boolean}
};
constructor() {
super();
this.toasts = [];
this.isFixed = false;
this._headerNode = null;
this._debouncedCalcBound = this._debouncedCalc.bind(this);
this._handleKeyupBound = this._handleKeyup.bind(this);
this._debounceTimeout = null;
}
get hasToasts() {
return this.toasts && this.toasts.length > 0;
}
connectedCallback() {
super.connectedCallback();
this._headerNode = document.querySelector('.header');
window.addEventListener('scroll', this._debouncedCalcBound);
window.addEventListener('resize', this._debouncedCalcBound);
window.addEventListener('keyup', this._handleKeyupBound);
this._debouncedCalc();
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('scroll', this._debouncedCalcBound);
window.removeEventListener('resize', this._debouncedCalcBound);
window.removeEventListener('keyup', this._handleKeyupBound);
if (this._debounceTimeout) {
clearTimeout(this._debounceTimeout);
}
}
updated(changedProperties) {
if (changedProperties.has('toasts')) {
$.Topic('/favicon/update').publish({count: this.toasts.length});
}
}
render() {
if (!this.hasToasts) {
return html``;
}
return html`
<div class=${classMap({
'container': true,
'toast-message-holder': true,
'fixed': this.isFixed
})}>
${this.toasts.map((item, index) => html`
<div class="alert alert-${item.level}">
<div class="toast-close" @click=${() => this.dismissNotification(index)}>
<span>${this._gettext('Close')}</span>
</div> </div>
</template> <rhodecode-unsafe-html .text=${item.message}></rhodecode-unsafe-html>
` </div>
`)}
</div>
`;
}
_handleKeyup(event) {
if (event.key === 'Escape') {
this.dismissNotifications();
} }
}
static get properties() { _debouncedCalc() {
return { if (this._debounceTimeout) {
toasts: { clearTimeout(this._debounceTimeout);
type: Array,
value() {
return []
}
},
isFixed: {
type: Boolean,
value: false
},
hasToasts: {
type: Boolean,
computed: '_computeHasToasts(toasts.*)'
},
keyEventTarget: {
type: Object,
value() {
return document.body;
}
}
}
} }
this._debounceTimeout = setTimeout(() => {
this.toastInWindow();
}, 25);
}
get keyBindings() { toastInWindow() {
return { if (!this._headerNode) {
'esc:keyup': '_hideOnEsc' return true;
}
} }
const headerHeight = this._headerNode.offsetHeight;
const scrollPosition = window.scrollY;
static get observers() { if (this.isFixed) {
return [ this.isFixed = 1 <= scrollPosition;
'_changedToasts(toasts.splices)' } else {
] this.isFixed = headerHeight <= scrollPosition;
} }
}
_hideOnEsc(event) { dismissNotification(index) {
return this.dismissNotifications(); $.Topic('/favicon/update').publish({count: this.toasts.length - 1});
} this.toasts = [
...this.toasts.slice(0, index),
_computeHasToasts() { ...this.toasts.slice(index + 1)
return this.toasts.length > 0; ];
} }
_debouncedCalc() { dismissNotifications() {
// calculate once in a while $.Topic('/favicon/update').publish({count: 0});
this.debounce('debouncedCalc', this.toastInWindow, 25); this.toasts = [];
} }
conditionalClass() { handleNotification(data) {
return this.isFixed ? 'fixed' : ''; if (!templateContext.rhodecode_user.notification_status && !data.message.force) {
} return;
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)
} }
this.toasts = [...this.toasts, {
level: data.message.level,
message: data.message.message
}];
}
_gettext(x) {
return _gettext(x);
}
} }
customElements.define(RhodecodeToast.is, RhodecodeToast); customElements.define('rhodecode-toast', RhodecodeToast);

View file

@ -1,30 +1,19 @@
import {PolymerElement, html} from '@polymer/polymer/polymer-element.js'; import {LitElement, html} from 'lit';
import {unsafeHTML} from 'lit/directives/unsafe-html.js';
export class RhodecodeUnsafeHtml extends PolymerElement { export class RhodecodeUnsafeHtml extends LitElement {
static properties = {
text: {type: String}
};
static get is() { constructor() {
return 'rhodecode-unsafe-html'; super();
} this.text = '';
}
static get template() { render() {
return html` return html`${unsafeHTML(this.text)}`;
<style include="shared-styles"></style> }
<slot></slot>
`;
}
static get properties() {
return {
text: {
type: String,
observer: '_handleText'
}
}
}
_handleText(newVal, oldVal) {
this.innerHTML = this.text;
}
} }
customElements.define(RhodecodeUnsafeHtml.is, RhodecodeUnsafeHtml); customElements.define('rhodecode-unsafe-html', RhodecodeUnsafeHtml);

View file

@ -1,7 +1,9 @@
const styleElement = document.createElement('dom-module'); const styleElement = document.createElement('dom-module');
// this will use raw-loader // this is a workaround to make shared styles work for Lit components.
import sharedCSS from './style-polymer.css'; // Needs proper refactoring to make sure styling follows Lit patterns
import sharedCSS from './style-lit.css';
styleElement.setAttribute('id', 'shared-styles');
styleElement.innerHTML = styleElement.innerHTML =
`<template> `<template>
<style> <style>
@ -9,4 +11,4 @@ styleElement.innerHTML =
</style> </style>
</template>`; </template>`;
styleElement.register('shared-styles'); document.head.appendChild(styleElement);

View file

@ -58,7 +58,6 @@ c.template_context['attachment_store'] = {
</%def> </%def>
${self.robots()} ${self.robots()}
<link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" /> <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
<script src="${h.asset('js/vendors/webcomponentsjs/custom-elements-es5-adapter.js', ver=c.rhodecode_version_hash)}"></script>
<script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-bundle.js', ver=c.rhodecode_version_hash)}"></script> <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-bundle.js', ver=c.rhodecode_version_hash)}"></script>
## CSS definitions ## CSS definitions

View file

@ -21,7 +21,7 @@
<h3>Alert Messages</h3> <h3>Alert Messages</h3>
<p> <p>
Alert messages are produced using the custom Polymer element Alert messages are produced using the custom Lit element
<code>rhodecode-toast</code> which is passed a message and level. <code>rhodecode-toast</code> which is passed a message and level.
</p> </p>

File diff suppressed because one or more lines are too long

View file

@ -588,7 +588,7 @@ search.location = %(here)s/.rc-test-data/data/index
channelstream.enabled = false channelstream.enabled = false
; server address for channelstream server on the backend ; server address for channelstream server on the backend
channelstream.server = channelstream:9800 channelstream.server = channelstream:8000
; location of the channelstream server from outside world ; location of the channelstream server from outside world
; use ws:// for http or wss:// for https. This address needs to be handled ; use ws:// for http or wss:// for https. This address needs to be handled

View file

@ -7,7 +7,7 @@ robots.txt \
js/scripts.min.js \ js/scripts.min.js \
js/rhodecode-components.js \ js/rhodecode-components.js \
css/style.css \ css/style.css \
css/style-polymer.css \ css/style-lit.css \
css/style-ipython.css" css/style-ipython.css"
static_path=$1 static_path=$1

View file

@ -4,7 +4,7 @@
## ** BUILD ** ## ** BUILD **
# docker build --tag rhodecode/static-files-build:16 -f static-build.dockerfile . # docker build --tag rhodecode/static-files-build:16 -f static-build.dockerfile .
# docker build --tag rhodecode/static-files-build:18 -f static-build.dockerfile . # docker build --tag rhodecode/static-files-build:24 -f static-build.dockerfile .
## run grunt on top of the source code ## run grunt on top of the source code
## cd rhodecode-enterprise-ce ## cd rhodecode-enterprise-ce
@ -14,8 +14,9 @@
## docker run -it --entrypoint bash --rm -v $PWD:/project --workdir=/project rhodecode/static-files-build:16 -c "npm install && grunt -v" ## docker run -it --entrypoint bash --rm -v $PWD:/project --workdir=/project rhodecode/static-files-build:16 -c "npm install && grunt -v"
# THIS WORKS NOW # THIS WORKS NOW
FROM node:16-buster #FROM node:16-buster
#FROM node:18-buster FROM node:24.11.1-bookworm-slim
WORKDIR /static_build WORKDIR /static_build

View file

@ -15,11 +15,19 @@ let babelRCOptions = {
"presets": [ "presets": [
["env", { ["env", {
"targets": { "targets": {
"browsers": ["last 2 versions"] "esmodules": true
} },
"exclude": [
"transform-es2015-classes",
"transform-regenerator",
"transform-async-to-generator"
]
}] }]
], ],
"plugins": ["transform-object-rest-spread"] "plugins": [
["transform-class-properties", { "loose": true }],
"transform-object-rest-spread"
]
}; };
module.exports = { module.exports = {
@ -32,8 +40,8 @@ module.exports = {
path: path.resolve(destinationDirectory) path: path.resolve(destinationDirectory)
}, },
// Tell Webpack which directories to look in to resolve import statements. // Tell Webpack which directories to look in to resolve import statements.
// Normally Webpack will look in node_modules by default but since were overriding // Normally Webpack will look in node_modules by default but since we're overriding
// the property well need to tell it to look there. // the property we'll need to tell it to look there.
resolve: { resolve: {
modules: [ modules: [
path.resolve(__dirname, 'node_modules'), path.resolve(__dirname, 'node_modules'),
@ -45,34 +53,15 @@ module.exports = {
module: { module: {
rules: [ rules: [
{ {
test: /style-polymer.css/, test: /style-lit.css/,
use: 'raw-loader' use: 'raw-loader'
}, },
{ {
// If you see a file that ends in .html, send it to these loaders. // Babel-loader for JS files
test: /\.html$/,
// This is an example of chained loaders in Webpack.
// Chained loaders run last to first. So it will run
// polymer-webpack-loader, and hand the output to
// babel-loader. This let's us transpile JS in our `<script>` elements.
use: [
{loader: 'babel-loader', options: babelRCOptions},
{loader: 'polymer-webpack-loader',
options: {
processStyleLinks: true,
}
}
],
},
{
// If you see a file that ends in .js, just send it to the babel-loader.
test: /\.js$/, test: /\.js$/,
exclude: /node_modules/,
use: {loader: 'babel-loader', options: babelRCOptions} use: {loader: 'babel-loader', options: babelRCOptions}
// Optionally exclude node_modules from transpilation except for polymer-webpack-loader:
// exclude: /node_modules\/(?!polymer-webpack-loader\/).*/
}, },
// this is required because of bug:
// https://github.com/webpack-contrib/polymer-webpack-loader/issues/49
{ {
test: /intl-messageformat.min.js/, test: /intl-messageformat.min.js/,
use: 'imports-loader?this=>window' use: 'imports-loader?this=>window'