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> </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> </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; } }