Merge pull request #11230 from umbraco/v9/feature/debug-dashboard

v9: Created new system information section in help panel
This commit is contained in:
Nikolaj Geisle
2021-10-07 15:14:47 +02:00
committed by GitHub
15 changed files with 957 additions and 544 deletions

View File

@@ -180,6 +180,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<UriUtility>();
Services.AddUnique<IDashboardService, DashboardService>();
Services.AddUnique<IUserDataService, UserDataService>();
// will be injected in controllers when needed to invoke rest endpoints on Our
Services.AddUnique<IInstallationService, InstallationService>();

View File

@@ -0,0 +1,19 @@
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models
{
[DataContract]
public class UserData
{
[DataMember(Name = "name")]
public string Name { get; }
[DataMember(Name = "data")]
public string Data { get; }
public UserData(string name, string data)
{
Name = name;
Data = data;
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services
{
public interface IUserDataService
{
IEnumerable<UserData> GetUserData();
}
}

View File

@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services
{
public class UserDataService : IUserDataService
{
private readonly IUmbracoVersion _version;
private readonly ILocalizationService _localizationService;
public UserDataService(IUmbracoVersion version, ILocalizationService localizationService)
{
_version = version;
_localizationService = localizationService;
}
public IEnumerable<UserData> GetUserData() =>
new List<UserData>
{
new("Server OS", RuntimeInformation.OSDescription),
new("Server Framework", RuntimeInformation.FrameworkDescription),
new("Default Language", _localizationService.GetDefaultLanguageIsoCode()),
new("Umbraco Version", _version.SemanticVersion.ToSemanticStringWithoutBuild()),
new("Current Culture", Thread.CurrentThread.CurrentCulture.ToString()),
new("Current UI Culture", Thread.CurrentThread.CurrentUICulture.ToString()),
new("Current Webserver", GetCurrentWebServer())
};
private string GetCurrentWebServer() => IsRunningInProcessIIS() ? "IIS" : "Kestrel";
public bool IsRunningInProcessIIS()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return false;
}
string processName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName);
return (processName.Contains("w3wp") || processName.Contains("iisexpress"));
}
}
}

View File

@@ -0,0 +1,47 @@
/// <reference types="Cypress" />
function openSystemInformation(){
//We have to wait for page to load, if the site is slow
cy.get('[data-element="global-help"]').should('be.visible').click();
cy.get('.umb-help-list-item').last().should('be.visible').click();
cy.get('.umb-drawer-content').scrollTo('bottom', {ensureScrollable : false});
}
context('System Information', () => {
beforeEach(() => {
//arrange
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'));
cy.umbracoSetCurrentUserLanguage('en-US');
});
afterEach(() => {
cy.umbracoSetCurrentUserLanguage('en-US');
});
it('Check System Info Displays', () => {
openSystemInformation();
cy.get('.table').find('tr').should('have.length', 10);
cy.contains('Current Culture').parent().should('contain', 'en-US');
cy.contains('Current UI Culture').parent().should('contain', 'en-US');
});
it('Checks language displays correctly after switching', () => {
//Navigate to edit user and change language
cy.umbracoGlobalUser().click();
cy.get('[alias="editUser"]').click();
cy.get('[name="culture"]').select('string:da-DK', { force: true});
cy.umbracoButtonByLabelKey('buttons_save').click({force: true});
//Refresh site to display new language
cy.reload();
cy.get('.umb-tour-step', { timeout: 60000 }).should('be.visible'); // We now due to the api calls this will be shown, but slow computers can take a while
cy.get('.umb-tour-step__close').click();
openSystemInformation();
//Assert
cy.contains('Current Culture').parent().should('contain', 'da-DK');
cy.contains('Current UI Culture').parent().should('contain', 'da-DK');
cy.get('.umb-button__content').last().click();
//Clean
cy.umbracoSetCurrentUserLanguage('en-US');
});
});

View File

@@ -100,4 +100,4 @@ function runBackOfficeIntroTour(percentageComplete, buttonText, timeout) {
cy.get('.umb-tour-step__footer .umb-button').should('be.visible').click();
cy.umbracoGlobalHelp().should("be.visible");
}
}

View File

@@ -11,7 +11,7 @@
"del": "^6.0.0",
"ncp": "^2.0.0",
"prompt": "^1.0.0",
"umbraco-cypress-testhelpers": "^1.0.0-beta-57"
"umbraco-cypress-testhelpers": "^1.0.0-beta-58"
},
"dependencies": {
"typescript": "^3.9.2"

View File

@@ -0,0 +1,110 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Semver;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services
{
[TestFixture]
public class UserDataServiceTests
{
private IUmbracoVersion _umbracoVersion;
[OneTimeSetUp]
public void CreateMocks() => CreateUmbracoVersion(9, 0, 0);
[Test]
[TestCase("en-US")]
[TestCase("de-DE")]
[TestCase("en-NZ")]
[TestCase("sv-SE")]
public void GetCorrectDefaultLanguageTest(string culture)
{
var userDataService = CreateUserDataService(culture);
var defaultLanguage = userDataService.GetUserData().FirstOrDefault(x => x.Name == "Default Language");
Assert.Multiple(() =>
{
Assert.IsNotNull(defaultLanguage);
Assert.AreEqual(culture, defaultLanguage.Data);
});
}
[Test]
[TestCase("en-US")]
[TestCase("de-DE")]
[TestCase("en-NZ")]
[TestCase("sv-SE")]
public void GetCorrectCultureTest(string culture)
{
Thread.CurrentThread.CurrentCulture = new CultureInfo(culture);
var userDataService = CreateUserDataService(culture);
var currentCulture = userDataService.GetUserData().FirstOrDefault(x => x.Name == "Current Culture");
Assert.Multiple(() =>
{
Assert.IsNotNull(currentCulture);
Assert.AreEqual(culture, currentCulture.Data);
});
}
[Test]
[TestCase("en-US")]
[TestCase("de-DE")]
[TestCase("en-NZ")]
[TestCase("sv-SE")]
public void GetCorrectUICultureTest(string culture)
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
var userDataService = CreateUserDataService(culture);
var currentCulture = userDataService.GetUserData().FirstOrDefault(x => x.Name == "Current UI Culture");
Assert.Multiple(() =>
{
Assert.IsNotNull(currentCulture);
Assert.AreEqual(culture, currentCulture.Data);
});
}
[Test]
[TestCase("en-US")]
[TestCase("de-DE")]
[TestCase("en-NZ")]
[TestCase("sv-SE")]
public void RunTimeInformationNotNullTest(string culture)
{
var userDataService = CreateUserDataService(culture);
IEnumerable<UserData> userData = userDataService.GetUserData().ToList();
Assert.Multiple(() =>
{
Assert.IsNotNull(userData.Select(x => x.Name == "Server OS"));
Assert.IsNotNull(userData.Select(x => x.Name == "Server Framework"));
Assert.IsNotNull(userData.Select(x => x.Name == "Current Webserver"));
});
}
private UserDataService CreateUserDataService(string culture)
{
var localizationService = CreateILocalizationService(culture);
return new UserDataService(_umbracoVersion, localizationService);
}
private ILocalizationService CreateILocalizationService(string culture)
{
var localizationService = new Mock<ILocalizationService>();
localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns(culture);
return localizationService.Object;
}
private void CreateUmbracoVersion(int major, int minor, int patch)
{
var umbracoVersion = new Mock<IUmbracoVersion>();
var semVersion = new SemVersion(major, minor, patch);
umbracoVersion.Setup(x => x.SemanticVersion).Returns(semVersion);
_umbracoVersion = umbracoVersion.Object;
}
}
}

View File

@@ -6,7 +6,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Umbraco.Cms.Core;
@@ -22,11 +22,10 @@ using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Web.BackOffice.Extensions;
using Umbraco.Cms.Web.BackOffice.Filters;
using Umbraco.Cms.Web.Common.ActionsResults;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;
@@ -47,12 +46,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
private readonly IUserService _userService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly IBackOfficeUserManager _backOfficeUserManager;
private readonly ILoggerFactory _loggerFactory;
private readonly ILocalizedTextService _localizedTextService;
private readonly AppCaches _appCaches;
private readonly IShortStringHelper _shortStringHelper;
private readonly IPasswordChanger<BackOfficeIdentityUser> _passwordChanger;
private readonly IUserDataService _userDataService;
[ActivatorUtilitiesConstructor]
public CurrentUserController(
MediaFileManager mediaFileManager,
IOptions<ContentSettings> contentSettings,
@@ -62,11 +62,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
IUserService userService,
IUmbracoMapper umbracoMapper,
IBackOfficeUserManager backOfficeUserManager,
ILoggerFactory loggerFactory,
ILocalizedTextService localizedTextService,
AppCaches appCaches,
IShortStringHelper shortStringHelper,
IPasswordChanger<BackOfficeIdentityUser> passwordChanger)
IPasswordChanger<BackOfficeIdentityUser> passwordChanger,
IUserDataService userDataService)
{
_mediaFileManager = mediaFileManager;
_contentSettings = contentSettings.Value;
@@ -76,11 +76,42 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
_userService = userService;
_umbracoMapper = umbracoMapper;
_backOfficeUserManager = backOfficeUserManager;
_loggerFactory = loggerFactory;
_localizedTextService = localizedTextService;
_appCaches = appCaches;
_shortStringHelper = shortStringHelper;
_passwordChanger = passwordChanger;
_userDataService = userDataService;
}
[Obsolete("This constructor is obsolete and will be removed in v11, use constructor with all values")]
public CurrentUserController(
MediaFileManager mediaFileManager,
IOptions<ContentSettings> contentSettings,
IHostingEnvironment hostingEnvironment,
IImageUrlGenerator imageUrlGenerator,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IUserService userService,
IUmbracoMapper umbracoMapper,
IBackOfficeUserManager backOfficeUserManager,
ILocalizedTextService localizedTextService,
AppCaches appCaches,
IShortStringHelper shortStringHelper,
IPasswordChanger<BackOfficeIdentityUser> passwordChanger) : this(
mediaFileManager,
contentSettings,
hostingEnvironment,
imageUrlGenerator,
backofficeSecurityAccessor,
userService,
umbracoMapper,
backOfficeUserManager,
localizedTextService,
appCaches,
shortStringHelper,
passwordChanger,
StaticServiceProvider.Instance.GetRequiredService<IUserDataService>())
{
}
@@ -167,6 +198,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return userTours;
}
public IEnumerable<UserData> GetUserData() => _userDataService.GetUserData();
/// <summary>
/// When a user is invited and they click on the invitation link, they will be partially logged in
/// where they can set their username/password

View File

@@ -51,7 +51,6 @@ function currentUserResource($q, $http, umbRequestHelper, umbDataFormatter) {
[{ permissionToCheck: permission }, { nodeId: id }])),
'Failed to check permission for item ' + id);
},
getCurrentUserLinkedLogins: function () {
return umbRequestHelper.resourcePromise(
@@ -61,6 +60,14 @@ function currentUserResource($q, $http, umbRequestHelper, umbDataFormatter) {
"GetCurrentUserLinkedLogins")),
'Server call failed for getting current users linked logins');
},
getUserData: function () {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"currentUserApiBaseUrl",
"GetUserData")),
'Server call failed for getting current user data');
},
saveTourStatus: function (tourStatus) {

View File

@@ -1,23 +1,84 @@
(function() {
'use strict';
(function () {
'use strict';
function platformService() {
function platformService() {
const userAgentRules = [
['Aol', /AOLShield\/([0-9\._]+)/],
['Edge', /Edge\/([0-9\._]+)/],
['Edge-ios', /EdgiOS\/([0-9\._]+)/],
['Yandexbrowser', /YaBrowser\/([0-9\._]+)/],
['Kakaotalk', /KAKAOTALK\s([0-9\.]+)/],
['Samsung', /SamsungBrowser\/([0-9\.]+)/],
['Silk', /\bSilk\/([0-9._-]+)\b/],
['MiUI', /MiuiBrowser\/([0-9\.]+)$/],
['Beaker', /BeakerBrowser\/([0-9\.]+)/],
['Edge-chromium', /EdgA?\/([0-9\.]+)/],
['chromium-webview', /(?!Chrom.*OPR)wv\).*Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],
['Chrome', /(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],
['PhantomJS', /PhantomJS\/([0-9\.]+)(:?\s|$)/],
['Crios', /CriOS\/([0-9\.]+)(:?\s|$)/],
['Firefox', /Firefox\/([0-9\.]+)(?:\s|$)/],
['FxiOS', /FxiOS\/([0-9\.]+)/],
['Opera-mini', /Opera Mini.*Version\/([0-9\.]+)/],
['Opera', /Opera\/([0-9\.]+)(?:\s|$)/],
['Opera', /OPR\/([0-9\.]+)(:?\s|$)/],
['IE', /Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/],
['IE', /MSIE\s([0-9\.]+);.*Trident\/[4-7].0/],
['IE', /MSIE\s(7\.0)/],
['BB10', /BB10;\sTouch.*Version\/([0-9\.]+)/],
['Android', /Android\s([0-9\.]+)/],
['iOS', /Version\/([0-9\._]+).*Mobile.*Safari.*/],
['Safari', /Version\/([0-9\._]+).*Safari/],
['Facebook', /FB[AS]V\/([0-9\.]+)/],
['Instagram', /Instagram\s([0-9\.]+)/],
['iOS-webview', /AppleWebKit\/([0-9\.]+).*Mobile/],
['iOS-webview', /AppleWebKit\/([0-9\.]+).*Gecko\)$/],
['Curl', /^curl\/([0-9\.]+)$/]
];
function isMac() {
return navigator.platform.toUpperCase().indexOf('MAC')>=0;
}
function isMac() {
return navigator.platform.toUpperCase().indexOf('MAC') >= 0;
}
////////////
var service = {
isMac: isMac
function getBrowserInfo(){
let data = matchUserAgent(navigator.userAgent);
console.log(data);
if(data){
const test = data[1];
return {
name : data[0],
version : test[1]
};
}
return null;
}
return service;
function matchUserAgent(ua) {
return (ua !== '' && userAgentRules.reduce (
(matched, [browser, regex]) => {
if (matched) {
return matched;
}
const uaMatch = regex.exec(ua);
return !!uaMatch && [browser, uaMatch];
},
false
)
);
}
}
////////////
angular.module('umbraco.services').factory('platformService', platformService);
var service = {
isMac: isMac,
getBrowserInfo : getBrowserInfo
};
return service;
}
angular.module('umbraco.services').factory('platformService', platformService);
})();

View File

@@ -101,9 +101,9 @@
}
/* Make sure typography looks good */
.umb-help-article h1,
.umb-help-article h2,
.umb-help-article h3,
.umb-help-article h1,
.umb-help-article h2,
.umb-help-article h3,
.umb-help-article h4 {
line-height: 1.3em;
font-weight: bold;
@@ -138,7 +138,7 @@
}
.umb-help-section__title {
margin:0 0 10px;
margin:0 0 10px;
}
/* Help list */
@@ -147,10 +147,10 @@
list-style: none;
margin-left: 0;
margin-bottom: 0;
background: @white;
background: @white;
border-radius: 3px;
box-shadow: 0 1px 1px 0 rgba(0,0,0,0.16);
[data-element*="help-tours"] & {
margin-bottom:5px;
}
@@ -166,9 +166,20 @@
border: 0 none;
}
.umb-help-list-item:last-child {
border-bottom: none;
}
.umb-help-list-item__title-wrapper {
display:flex;
justify-content: space-between;
align-items: center;
.umb-help-list-item {
flex: 1 0 auto;
width: auto;
}
}
.umb-help-list-item__group-title i {
margin-right:2px;
@@ -185,7 +196,7 @@
.umb-help-list-item:hover,
.umb-help-list-item:focus,
.umb-help-list-item:active,
.umb-help-list-item > a:hover,
.umb-help-list-item > a:hover,
.umb-help-list-item > a:focus,
.umb-help-list-item > a:active {
text-decoration: none;

View File

@@ -1,11 +1,10 @@
(function () {
"use strict";
function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter, editorState) {
function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter, editorState, notificationsService, currentUserResource, platformService) {
var vm = this;
var evts = [];
vm.title = "";
vm.subtitle = "Umbraco version" + " " + Umbraco.Sys.ServerVariables.application.version;
vm.section = $routeParams.section;
@@ -13,16 +12,19 @@
vm.sectionName = "";
vm.customDashboard = null;
vm.tours = [];
vm.systemInfoDisplay = false;
vm.closeDrawer = closeDrawer;
vm.startTour = startTour;
vm.getTourGroupCompletedPercentage = getTourGroupCompletedPercentage;
vm.showTourButton = showTourButton;
vm.copyInformation = copyInformation;
vm.showDocTypeTour = false;
vm.docTypeTours = [];
vm.systemInfo = [];
vm.nodeName = '';
function startTour(tour) {
tourService.startTour(tour);
closeDrawer();
@@ -34,7 +36,14 @@
localizationService.localize("general_help").then(function(data){
vm.title = data;
});
currentUserResource.getUserData().then(function(systemInfo){
vm.systemInfo = systemInfo;
let browserInfo = platformService.getBrowserInfo();
if(browserInfo != null){
vm.systemInfo.push({name :"Browser", data: browserInfo.name + " " + browserInfo.version});
}
vm.systemInfo.push({name :"Browser OS", data: getPlatform()});
});
tourService.getGroupedTours().then(function(groupedTours) {
vm.tours = groupedTours;
getTourGroupCompletedPercentage();
@@ -52,11 +61,11 @@
setSectionName();
userService.getCurrentUser().then(function (user) {
vm.userType = user.userType;
vm.userLang = user.locale;
vm.hasAccessToSettings = _.contains(user.allowedSections, 'settings');
vm.hasAccessToSettings = _.contains(user.allowedSections, 'settings');
evts.push(eventsService.on("appState.treeState.changed", function (e, args) {
handleSectionChange();
@@ -72,7 +81,7 @@
});
setDocTypeTour(editorState.getCurrent());
// check if a tour is running - if it is open the matching group
var currentTour = tourService.getCurrentTour();
@@ -89,7 +98,7 @@
function handleSectionChange() {
$timeout(function () {
if (vm.section !== $routeParams.section || vm.tree !== $routeParams.tree) {
vm.section = $routeParams.section;
vm.tree = $routeParams.tree;
@@ -107,7 +116,7 @@
vm.topics = topics;
});
}
var rq = {};
rq.section = vm.section;
@@ -134,7 +143,7 @@
helpService.findVideos(rq).then(function (videos) {
vm.videos = videos;
});
}
}
}
function setSectionName() {
@@ -190,7 +199,7 @@
tourService.getToursForDoctype(node.contentTypeAlias).then(function (data) {
if (data && data.length > 0) {
vm.docTypeTours = data;
var currentVariant = _.find(node.variants, (x) => x.active);
var currentVariant = _.find(node.variants, (x) => x.active);
vm.nodeName = currentVariant.name;
vm.showDocTypeTour = true;
}
@@ -198,7 +207,23 @@
}
}
}
function copyInformation(){
let copyText = "<html>\n<body>\n<!--StartFragment-->\n\nCategory | Data\n-- | --\n";
vm.systemInfo.forEach(function (info){
copyText += info.name + " | " + info.data + "\n";
});
copyText += "\n<!--EndFragment-->\n</body>\n</html>"
navigator.clipboard.writeText(copyText);
if(copyText != null){
notificationsService.success("Copied!", "Your system information is now in your clipboard");
}
else{
notificationsService.error("Error", "Could not copy system information");
}
}
function getPlatform() {
return window.navigator.platform;
}
evts.push(eventsService.on("appState.tour.complete", function (event, tour) {
tourService.getGroupedTours().then(function(groupedTours) {
vm.tours = groupedTours;
@@ -206,7 +231,7 @@
getTourGroupCompletedPercentage();
});
}));
$scope.$on('$destroy', function () {
for (var e in evts) {
eventsService.unsubscribe(evts[e]);

View File

@@ -149,6 +149,47 @@
</a>
</div>
<!-- System info -->
<div class="umb-help-section">
<h5 class="umb-help-section__title">System Information</h5>
<div class="umb-help-list">
<div class="umb-help-list-item__title-wrapper">
<button
type="button"
class="umb-help-list-item umb-help-list-item__content"
ng-click="vm.systemInfoDisplay = !vm.systemInfoDisplay"
aria-expanded="{{vm.systemInfoDisplay === true}}">
<umb-icon class="mr1" icon="{{vm.systemInfoDisplay ? 'icon-navigation-down' : 'icon-navigation-right'}}"></umb-icon>
<span class="flex-column items-start flex">
<span class="umb-help-list-item__group-title bold">System Information</span>
<span class="umb-help-list-item__description mt0">View your system information</span>
</span>
</button>
<button
ng-click="vm.copyInformation()"
class="btn-reset"
type="button"
size="30">
<span class="">
<umb-icon icon="icon-documents" class="umb-help-list-item__icon"></umb-icon>
</span>
</button>
</div>
<div ng-if="vm.systemInfoDisplay === true">
<table class="table table-striped">
<tr>
<th>Category</th>
<th>Data</th>
</tr>
<tr ng-repeat="info in vm.systemInfo">
<td>{{info.name}}</td>
<td>{{info.data}}</td>
</tr>
</table>
</div>
</div>
</div>
</umb-drawer-content>
<umb-drawer-footer>