UI Toggle between Classic and Modern (#6763)

* [ENH] Add toggle switch for new MeshCentral UI in settings and top header of the classic UI * [ENH] Add toggle for new MeshCentral UI in settings and in top headbar of the modern UI * add showModernUIToggle and store uiviewmode in db --------- Co-authored-by: kambereBr <brunokambere@gmail.com>

Simon Smith committed Feb 9, 2025 at 19:41 UTC 5734bcc33a2fee4560a24409404c12735b87212b
7 files changed +114 -4
meshcentral-config-schema.json
+5
@@ -1173,6 +1173,11 @@
1173 "default": 2,
1174 "description": "Valid numbers are 1 and 2, changes the style of the login page and some secondary pages."
1175 },
1176 + "showModernUIToggle": {
1177 + "type": "boolean",
1178 + "default": false,
1179 + "description": "When set to true, the user will be able to toggle between the modern and classic UI."
1180 + },
1181 "title": {
1182 "type": "string",
1183 "default": "MeshCentral",
public/styles/style-bootstrap.css
+18 -1
@@ -266,6 +266,16 @@ body {
266 right: 3px;
267 }
268
269 +.textnewui {
270 + color: white;
271 + font-weight: bold;
272 + padding-top: 5px;
273 + cursor: pointer;
274 + position: absolute;
275 + right: 0;
276 + margin-right: 10px;
277 +}
278 +
279 .LogoffLinkColor {
280 color:white;
281 }
@@ -2877,7 +2887,7 @@ nav .lbbuttonsel2 {
2887 width: 28px;
2888 }
2889
2880 -.viewSelector3 {
2890 +.viewSelector3, .uiSelector7 {
2891 margin-left: 2px;
2892 margin-top: 2px;
2893 background: url(../images/views.png) -56px 0px;
@@ -2977,6 +2987,13 @@ nav .lbbuttonsel2 {
2987 background-color: #AAA;
2988 }
2989
2990 +.uiSelector_end {
2991 + width: 32px;
2992 + height: 32px;
2993 + float: left;
2994 + margin: 3px;
2995 +}
2996 +
2997 .uiSelectorSel {
2998 background-color: #BBB;
2999 opacity: 0.8;
public/styles/style.css
+18 -1
@@ -263,6 +263,16 @@ body {
263 right: 3px;
264 }
265
266 +.textnewui {
267 + color: white;
268 + font-weight: bold;
269 + padding-top: 5px;
270 + cursor: pointer;
271 + position: absolute;
272 + right: 0;
273 + margin-right: 10px;
274 +}
275 +
276 .LogoffLinkColor {
277 color:white;
278 }
@@ -2896,7 +2906,7 @@ a {
2906 width: 28px;
2907 }
2908
2899 -.viewSelector3 {
2909 +.viewSelector3, .uiSelector7 {
2910 margin-left: 2px;
2911 margin-top: 2px;
2912 background: url(../images/views.png) -56px 0px;
@@ -2996,6 +3006,13 @@ a {
3006 background-color: #AAA;
3007 }
3008
3009 +.uiSelector_end {
3010 + width: 32px;
3011 + height: 32px;
3012 + float: left;
3013 + margin: 3px;
3014 +}
3015 +
3016 .uiSelectorSel {
3017 background-color: #BBB;
3018 opacity: 0.8;
sample-config-advanced.json
+1
@@ -186,6 +186,7 @@
186 "domains": {
187 "": {
188 "_siteStyle": 2,
189 + "_showModernUIToggle": true,
190 "title": "MyServer",
191 "title2": "Servername",
192 "_titlePicture": "title-sample.png",
views/default.handlebars
+26
@@ -164,6 +164,9 @@
164 <div id=notificationCount onclick="clickNotificationIcon()" class="unselectable" style="display: none;" title="Click to view current notifications">0</div>
165 </div>
166 <p id="logoutControl"><span id=logoutControlSpan class="logoncontrolspan"></span><span id=idleTimeoutNotify style="color:yellow"></span></p>
167 + <div class=textnewui id=textnewui onmouseup=toggleBootstrapUIMode() onkeypress="if (event.key=='Enter') { toggleBootstrapUIMode(); }">
168 + <b>Try the new MeshCentral UI</b>
169 + </div>
170 </div>
171 <div id="page_leftbar">
172 <div style="height:16px"></div>
@@ -199,11 +202,13 @@
202 <div tabindex=0 id=uiViewButton1 class=uiSelector onclick=userInterfaceSelectMenu(1) title="Left bar interface" onkeypress="if (event.key == 'Enter') userInterfaceSelectMenu(1)"><div class="uiSelector1"></div></div>
203 <div tabindex=0 id=uiViewButton2 class=uiSelector onclick=userInterfaceSelectMenu(2) title="Top bar interface" onkeypress="if (event.key == 'Enter') userInterfaceSelectMenu(2)"><div class="uiSelector2"></div></div>
204 <div tabindex=0 id=uiViewButton3 class=uiSelector onclick=userInterfaceSelectMenu(3) title="Fixed width interface" onkeypress="if (event.key == 'Enter') userInterfaceSelectMenu(3)"><div class="uiSelector3"></div></div>
205 + <div tabindex=0 id=uiViewButton7 class=uiSelector onclick=toggleBootstrapUIMode() title="Toggle Bootstrap UI" onkeypress="if (event.key == 'Enter') toggleBootstrapUIMode()"><div class="uiSelector7"></div></div>
206 </td>
207 <td>
208 <div tabindex=0 id=uiViewButton6 class=uiSelector onclick="showNotes(false)" title="Personal Notes" onkeypress="if (event.key == 'Enter') showNotes(false)"><div class="uiSelector6"></div></div>
209 <div tabindex=0 id=uiViewButton4 class=uiSelector onclick=toggleNightMode() title="Toggle night mode" onkeypress="if (event.key == 'Enter') toggleNightMode()"><div class="uiSelector4"></div></div>
210 <div tabindex=0 id=uiViewButton5 class=uiSelector onclick=toggleFooterBarMode() title="Toggle footer bar" onkeypress="if (event.key == 'Enter') toggleFooterBarMode()"><div class="uiSelector5"></div></div>
211 + <div class=uiSelector_end>&nbsp;</div>
212 </td>
213 </tr>
214 </table>
@@ -1723,6 +1728,9 @@
1728 });
1729 }
1730
1731 + // Show the modern ui switcher
1732 + QV('textnewui', ((features2 & 0x40000000) == 0) ? false : true);
1733 +
1734 // Connect to the mesh server
1735 meshserver = MeshServerCreateControl(domainUrl);
1736 meshserver.onStateChanged = onStateChanged;
@@ -2183,6 +2191,24 @@
2191 QV('body', true);
2192 }
2193
2194 + function saveUserInterfaceMode() {
2195 + var nUiViewMode = 2;
2196 + if (Q('ui1').checked) { nUiViewMode = 3; }
2197 + if (getstore('uiViewMode', 2) != nUiViewMode) {
2198 + putstore('uiViewMode', nUiViewMode);
2199 + reload();
2200 + }
2201 + }
2202 +
2203 + function toggleBootstrapUIMode() {
2204 + if (xxdialogMode) return;
2205 + var uiViewMode = getstore('uiViewMode', 2);
2206 + var x = '<input type=radio id=ui0 name=uiradio value=2 ' + ((uiViewMode == 2)?'checked':'') + '><label for=ui0>' + "Classic" + '</label><br>';
2207 + x += '<input type=radio id=ui1 name=uiradio value=3 ' + ((uiViewMode == 3)?'checked':'') + '><label for=ui1>' + "Modern" + '</label><br>';
2208 + setDialogMode(2, "User Interface", 3, saveUserInterfaceMode, x);
2209 + QV('uiMenu', false);
2210 + }
2211 +
2212 function getNodeFromId(id) { if (nodes != null) { for (var i in nodes) { if (nodes[i]._id == id) return nodes[i]; } } return null; }
2213 function reload() {
2214 var x = window.location.href;
views/default3.handlebars
+37
@@ -153,6 +153,9 @@
153 <img id="topMenuIcon" class=noselect
154 style="position:absolute;right:0;top:10px;color:#c8c8c8;font-size:44px;margin-right:8px;cursor:pointer;display:none"
155 onclick=topMenu() src="/images/3bars-30.png" width=30 height=30 />
156 + <div class=textnewui id=textnewui onmouseup=toggleBootstrapUIMode() onkeypress="if (event.key=='Enter') { toggleBootstrapUIMode(); }">
157 + <b>Try the new MeshCentral UI</b>
158 + </div>
159 </div>
160 <div class="sidebar flex-column" id="page_leftbar">
161 <div style="height:24px"></div>
@@ -190,6 +193,11 @@
193 onkeypress="if (event.key == 'Enter') userInterfaceSelectMenu(3)">
194 <div class="uiSelector3"></div>
195 </div>
196 + <div tabindex=0 id=uiViewButton7 class=uiSelector
197 + onclick=toggleBootstrapUIMode() title="Toggle Modern UI"
198 + onkeypress="if (event.key == 'Enter') toggleBootstrapUIMode()">
199 + <div class="uiSelector7"></div>
200 + </div>
201 </td>
202 <td>
203 <div tabindex=0 id=uiViewButton6 class=uiSelector onclick="showNotes(false)"
@@ -207,6 +215,7 @@
215 onkeypress="if (event.key == 'Enter') toggleFooterBarMode()">
216 <div class="uiSelector5"></div>
217 </div>
218 + <div class=uiSelector_end>&nbsp;</div>
219 </td>
220 </tr>
221 </table>
@@ -2149,6 +2158,9 @@
2158 });
2159 }
2160
2161 + // Show the modern ui switcher
2162 + QV('textnewui', ((features2 & 0x40000000) == 0) ? false : true);
2163 +
2164 // Connect to the mesh server
2165 meshserver = MeshServerCreateControl(domainUrl);
2166 meshserver.onStateChanged = onStateChanged;
@@ -2622,6 +2634,31 @@
2634 QV('body', true);
2635 }
2636
2637 + function saveUserInterfaceMode() {
2638 + var nUiViewMode = 3;
2639 + if (Q('ui0').checked) { nUiViewMode = 2; }
2640 + if (getstore('uiViewMode', 3) != nUiViewMode) {
2641 + putstore('uiViewMode', nUiViewMode);
2642 + // Check if the URL includes 'sitestyle=' and remove it
2643 + var url = new URL(window.location.href);
2644 + if (url.searchParams.has('sitestyle')) {
2645 + url.searchParams.delete('sitestyle');
2646 + window.history.replaceState({}, document.title, url.toString());
2647 + }
2648 + reload();
2649 + }
2650 + }
2651 +
2652 + function toggleBootstrapUIMode() {
2653 + if (xxdialogMode) return;
2654 + var uiViewMode = getstore('uiViewMode', 3);
2655 + var x = '<div class=form-check><input type=radio class=form-check-input id=ui0 name=uiradio value=2 ' + ((uiViewMode == 2)?'checked':'') + '><label class=form-check-label for=ui0>' + "Classic" + '</label></div>';
2656 + x += '<div class=form-check><input type=radio class=form-check-input id=ui1 name=uiradio value=3 ' + ((uiViewMode == 3)?'checked':'') + '><label class=form-check-label for=ui1>' + "Modern" + '</label></div>';
2657 + setModalContent('xxAddAgent', "User Interface", x);
2658 + showModal('xxAddAgentModal', 'idx_dlgOkButton', saveUserInterfaceMode);
2659 + QV('uiMenu', false);
2660 + }
2661 +
2662 function getNodeFromId(id) { if (nodes != null) { for (var i in nodes) { if (nodes[i]._id == id) return nodes[i]; } } return null; }
2663 function reload() {
2664 var x = window.location.href;
webserver.js
+9 -2
@@ -3184,8 +3184,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3184 if (obj.parent.config.settings && obj.parent.config.settings.webrtcconfig && (typeof obj.parent.config.settings.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(obj.parent.config.settings.webrtcconfig)).replace(/'/g, '%27'); }
3185 else if (args.webrtcconfig && (typeof args.webrtcconfig == 'object')) { webRtcConfig = encodeURIComponent(JSON.stringify(args.webrtcconfig)).replace(/'/g, '%27'); }
3186
3187 + // Load default page style or new modern ui
3188 + var uiViewMode = 'default';
3189 + var webstateJSON = JSON.parse(webstate);
3190 + if (webstateJSON && webstateJSON.uiViewMode == 3) { uiViewMode = 'default3'; }
3191 + if (domain.sitestyle == 3) { uiViewMode = 'default3'; }
3192 + if (req.query.sitestyle == 3) { uiViewMode = 'default3'; }
3193 // Refresh the session
3188 - render(dbGetFunc.req, dbGetFunc.res, getRenderPage(((domain.sitestyle == 3) || (req.query.sitestyle == 3) ? 'default3' : 'default'), dbGetFunc.req, domain), getRenderArgs({
3194 + render(dbGetFunc.req, dbGetFunc.res, getRenderPage(uiViewMode, dbGetFunc.req, domain), getRenderArgs({
3195 authCookie: authCookie,
3196 authRelayCookie: authRelayCookie,
3197 viewmode: viewmode,
@@ -3335,6 +3341,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
3341 if (domain.scrolltotop == true) { features2 += 0x08000000; } // Show the "Scroll to top" button
3342 if (domain.devicesearchbargroupname === true) { features2 += 0x10000000; } // Search bar will find by group name too
3343 if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.duo2factor != false)) && (typeof domain.duo2factor == 'object') && (typeof domain.duo2factor.integrationkey == 'string') && (typeof domain.duo2factor.secretkey == 'string') && (typeof domain.duo2factor.apihostname == 'string')) { features2 += 0x20000000; } // using Duo for 2FA is allowed
3344 + if (domain.showmodernuitoggle == true) { features2 += 0x40000000; } // Indicates that the new UI should be shown
3345 return { features: features, features2: features2 };
3346 }
3347
@@ -9045,7 +9052,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
9052 }
9053
9054 // Filter the user web site and only output state that we need to keep
9048 - const acceptableUserWebStateStrings = ['webPageStackMenu', 'notifications', 'deviceView', 'nightMode', 'webPageFullScreen', 'search', 'showRealNames', 'sort', 'deskAspectRatio', 'viewsize', 'DeskControl', 'uiMode', 'footerBar','loctag','theme','lastThemes'];
9055 + const acceptableUserWebStateStrings = ['webPageStackMenu', 'notifications', 'deviceView', 'nightMode', 'webPageFullScreen', 'search', 'showRealNames', 'sort', 'deskAspectRatio', 'viewsize', 'DeskControl', 'uiMode', 'footerBar','loctag','theme','lastThemes','uiViewMode'];
9056 const acceptableUserWebStateDesktopStrings = ['encoding', 'showfocus', 'showmouse', 'showcad', 'limitFrameRate', 'noMouseRotate', 'quality', 'scaling', 'agentencoding']
9057 obj.filterUserWebState = function (state) {
9058 if (typeof state == 'string') { try { state = JSON.parse(state); } catch (ex) { return null; } }