Ver código fonte

SONAR-9802 add information to System Info page

tags/6.6-RC1
Simon Brandhof 6 anos atrás
pai
commit
8fa40905cc
52 arquivos alterados com 1019 adições e 1115 exclusões
  1. 4
    1
      server/sonar-process/src/main/java/org/sonar/process/systeminfo/JvmStateSection.java
  2. 2
    2
      server/sonar-process/src/test/java/org/sonar/process/systeminfo/JvmStateSectionTest.java
  3. 0
    28
      server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSectionMBean.java
  4. 6
    34
      server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java
  5. 64
    0
      server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStatisticsSection.java
  6. 0
    156
      server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSection.java
  7. 112
    57
      server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSection.java
  8. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java
  9. 3
    3
      server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/WebSystemInfoModule.java
  10. 8
    7
      server/sonar-server/src/main/java/org/sonar/server/platform/ws/ClusterInfoAction.java
  11. 6
    17
      server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java
  12. 88
    0
      server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStatisticsSectionTest.java
  13. 0
    217
      server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SonarQubeSectionTest.java
  14. 180
    7
      server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SystemSectionTest.java
  15. 1
    2
      server/sonar-web/src/main/js/api/system.ts
  16. 0
    101
      server/sonar-web/src/main/js/apps/system/__tests__/system-test.js
  17. 22
    6
      server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts
  18. 21
    7
      server/sonar-web/src/main/js/apps/system/components/App.tsx
  19. 4
    4
      server/sonar-web/src/main/js/apps/system/components/ChangeLogLevelForm.tsx
  20. 4
    4
      server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx
  21. 4
    2
      server/sonar-web/src/main/js/apps/system/components/PageActions.tsx
  22. 9
    7
      server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx
  23. 62
    0
      server/sonar-web/src/main/js/apps/system/components/StandaloneSysInfos.tsx
  24. 24
    4
      server/sonar-web/src/main/js/apps/system/components/__tests__/ClusterSysInfos-test.tsx
  25. 28
    4
      server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx
  26. 18
    2
      server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx
  27. 28
    23
      server/sonar-web/src/main/js/apps/system/components/__tests__/StandaloneSysInfos-test.tsx
  28. 36
    6
      server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ChangeLogLevelForm-test.tsx.snap
  29. 3
    1
      server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap
  30. 1
    0
      server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageHeader-test.tsx.snap
  31. 66
    0
      server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/StandaloneSysInfos-test.tsx.snap
  32. 20
    9
      server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx
  33. 4
    2
      server/sonar-web/src/main/js/apps/system/components/info-items/HealthItem.tsx
  34. 6
    0
      server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx
  35. 3
    1
      server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthItem-test.tsx
  36. 14
    3
      server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap
  37. 9
    8
      server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthItem-test.tsx.snap
  38. 0
    30
      server/sonar-web/src/main/js/apps/system/item-boolean.js
  39. 0
    60
      server/sonar-web/src/main/js/apps/system/item-log-level.js
  40. 0
    41
      server/sonar-web/src/main/js/apps/system/item-object.js
  41. 0
    127
      server/sonar-web/src/main/js/apps/system/main.js
  42. 1
    7
      server/sonar-web/src/main/js/apps/system/routes.ts
  43. 0
    49
      server/sonar-web/src/main/js/apps/system/section.js
  44. 9
    29
      server/sonar-web/src/main/js/apps/system/styles.css
  45. 74
    9
      server/sonar-web/src/main/js/apps/system/utils.ts
  46. 2
    14
      server/sonar-web/src/main/js/components/common/BranchStatus.css
  47. 4
    7
      server/sonar-web/src/main/js/components/common/BranchStatus.tsx
  48. 34
    0
      server/sonar-web/src/main/js/components/common/StatusIndicator.css
  49. 21
    7
      server/sonar-web/src/main/js/components/common/StatusIndicator.tsx
  50. 9
    6
      server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap
  51. 2
    2
      server/sonar-web/src/main/js/components/icons-components/OpenCloseIcon.tsx
  52. 2
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 4
- 1
server/sonar-process/src/main/java/org/sonar/process/systeminfo/JvmStateSection.java Ver arquivo

@@ -57,8 +57,11 @@ public class JvmStateSection implements SystemInfoSection {
addAttributeInMb(protobuf, "Non Heap Init (MB)", nonHeap.getInit());
addAttributeInMb(protobuf, "Non Heap Max (MB)", nonHeap.getMax());
addAttributeInMb(protobuf, "Non Heap Used (MB)", nonHeap.getUsed());

ThreadMXBean thread = ManagementFactory.getThreadMXBean();
setAttribute(protobuf, "Thread Count", thread.getThreadCount());
setAttribute(protobuf, "Threads", thread.getThreadCount());

setAttribute(protobuf,"Processors", Runtime.getRuntime().availableProcessors());

return protobuf.build();
}

+ 2
- 2
server/sonar-process/src/test/java/org/sonar/process/systeminfo/JvmStateSectionTest.java Ver arquivo

@@ -30,7 +30,7 @@ import static org.mockito.Mockito.when;

public class JvmStateSectionTest {

public static final String PROCESS_NAME = "the process name";
private static final String PROCESS_NAME = "the process name";

@Test
public void toSystemInfoSection() {
@@ -39,7 +39,7 @@ public class JvmStateSectionTest {

assertThat(section.getName()).isEqualTo(PROCESS_NAME);
assertThat(section.getAttributesCount()).isGreaterThan(0);
assertThat(section.getAttributesList()).extracting("key").contains("Thread Count");
assertThat(section.getAttributesList()).extracting("key").contains("Threads", "Processors");
}

@Test

+ 0
- 28
server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSectionMBean.java Ver arquivo

@@ -1,28 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.platform.monitoring;

/**
* The public attributes of {@link EsSection}
* to be exported in JMX bean.
*/
public interface EsSectionMBean {
String getState();
}

server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSection.java → server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java Ver arquivo

@@ -19,44 +19,28 @@
*/
package org.sonar.server.platform.monitoring;

import java.util.Map;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.cluster.node.stats.NodeStats;
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse;
import org.elasticsearch.action.admin.cluster.stats.ClusterStatsResponse;
import org.elasticsearch.action.admin.indices.stats.IndexStats;
import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.elasticsearch.common.breaker.CircuitBreaker;
import org.sonar.api.utils.log.Loggers;
import org.sonar.process.systeminfo.SystemInfoSection;
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
import org.sonar.server.es.EsClient;

import static org.apache.commons.io.FileUtils.byteCountToDisplaySize;
import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;

public class EsSection extends BaseSectionMBean implements EsSectionMBean {
public class EsStateSection implements SystemInfoSection {

private final EsClient esClient;

public EsSection(EsClient esClient) {
public EsStateSection(EsClient esClient) {
this.esClient = esClient;
}

@Override
public String name() {
return "Elasticsearch";
}

/**
* MXBean does not allow to return enum {@link ClusterHealthStatus}, so
* returning String.
*/
@Override
public String getState() {
return getStateAsEnum().name();
}

private ClusterHealthStatus getStateAsEnum() {
return clusterStats().getStatus();
}
@@ -64,29 +48,17 @@ public class EsSection extends BaseSectionMBean implements EsSectionMBean {
@Override
public ProtobufSystemInfo.Section toProtobuf() {
ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
protobuf.setName(name());
protobuf.setName("Search State");
try {
setAttribute(protobuf, "State", getStateAsEnum().name());
completeNodeAttributes(protobuf);
completeIndexAttributes(protobuf);

} catch (Exception es) {
Loggers.get(EsSection.class).warn("Failed to retrieve ES attributes. There will be only a single \"state\" attribute.", es);
} catch (Exception es) {
Loggers.get(EsStateSection.class).warn("Failed to retrieve ES attributes. There will be only a single \"state\" attribute.", es);
setAttribute(protobuf, "State", es.getCause() instanceof ElasticsearchException ? es.getCause().getMessage() : es.getMessage());
}
return protobuf.build();
}

private void completeIndexAttributes(ProtobufSystemInfo.Section.Builder protobuf) {
IndicesStatsResponse indicesStats = esClient.prepareStats().all().get();
for (Map.Entry<String, IndexStats> indexStats : indicesStats.getIndices().entrySet()) {
String prefix = "Index " + indexStats.getKey() + " - ";
setAttribute(protobuf, prefix + "Docs", indexStats.getValue().getPrimaries().getDocs().getCount());
setAttribute(protobuf, prefix + "Shards", indexStats.getValue().getShards().length);
setAttribute(protobuf, prefix + "Store Size", byteCountToDisplaySize(indexStats.getValue().getPrimaries().getStore().getSizeInBytes()));
}
}

private void completeNodeAttributes(ProtobufSystemInfo.Section.Builder protobuf) {
NodesStatsResponse nodesStats = esClient.prepareNodesStats().all().get();
if (!nodesStats.getNodes().isEmpty()) {

+ 64
- 0
server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStatisticsSection.java Ver arquivo

@@ -0,0 +1,64 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.platform.monitoring;

import java.util.Map;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.indices.stats.IndexStats;
import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
import org.sonar.api.utils.log.Loggers;
import org.sonar.process.systeminfo.SystemInfoSection;
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
import org.sonar.server.es.EsClient;

import static org.apache.commons.io.FileUtils.byteCountToDisplaySize;
import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;

public class EsStatisticsSection implements SystemInfoSection {

private final EsClient esClient;

public EsStatisticsSection(EsClient esClient) {
this.esClient = esClient;
}

@Override
public ProtobufSystemInfo.Section toProtobuf() {
ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
protobuf.setName("Search Statistics");
try {
completeIndexAttributes(protobuf);
} catch (Exception es) {
Loggers.get(EsStatisticsSection.class).warn("Failed to retrieve ES attributes. There will be only a single \"Error\" attribute.", es);
setAttribute(protobuf, "Error", es.getCause() instanceof ElasticsearchException ? es.getCause().getMessage() : es.getMessage());
}
return protobuf.build();
}

private void completeIndexAttributes(ProtobufSystemInfo.Section.Builder protobuf) {
IndicesStatsResponse indicesStats = esClient.prepareStats().all().get();
for (Map.Entry<String, IndexStats> indexStats : indicesStats.getIndices().entrySet()) {
String prefix = "Index " + indexStats.getKey() + " - ";
setAttribute(protobuf, prefix + "Docs", indexStats.getValue().getPrimaries().getDocs().getCount());
setAttribute(protobuf, prefix + "Shards", indexStats.getValue().getShards().length);
setAttribute(protobuf, prefix + "Store Size", byteCountToDisplaySize(indexStats.getValue().getPrimaries().getStore().getSizeInBytes()));
}
}
}

+ 0
- 156
server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSection.java Ver arquivo

@@ -1,156 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.platform.monitoring;

import com.google.common.base.Joiner;
import java.io.File;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.CoreProperties;
import org.sonar.api.config.Configuration;
import org.sonar.api.platform.Server;
import org.sonar.api.security.SecurityRealm;
import org.sonar.api.server.authentication.IdentityProvider;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.process.ProcessProperties;
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
import org.sonar.server.authentication.IdentityProviderRepository;
import org.sonar.server.health.Health;
import org.sonar.server.health.HealthChecker;
import org.sonar.server.platform.ServerIdLoader;
import org.sonar.server.platform.ServerLogging;
import org.sonar.server.user.SecurityRealmFactory;

import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;

public class SonarQubeSection extends BaseSectionMBean implements SonarQubeSectionMBean {

private static final Joiner COMMA_JOINER = Joiner.on(", ");

static final String BRANDING_FILE_PATH = "web/WEB-INF/classes/com/sonarsource/branding";

private final Configuration config;
private final SecurityRealmFactory securityRealmFactory;
private final IdentityProviderRepository identityProviderRepository;
private final Server server;
private final ServerLogging serverLogging;
private final ServerIdLoader serverIdLoader;
private final HealthChecker healthChecker;

public SonarQubeSection(Configuration config, SecurityRealmFactory securityRealmFactory,
IdentityProviderRepository identityProviderRepository, Server server, ServerLogging serverLogging,
ServerIdLoader serverIdLoader, HealthChecker healthChecker) {
this.config = config;
this.securityRealmFactory = securityRealmFactory;
this.identityProviderRepository = identityProviderRepository;
this.server = server;
this.serverLogging = serverLogging;
this.serverIdLoader = serverIdLoader;
this.healthChecker = healthChecker;
}

@Override
public String getServerId() {
return serverIdLoader.getRaw().orElse(null);
}

@Override
public String getVersion() {
return server.getVersion();
}

@Override
public String getLogLevel() {
return serverLogging.getRootLoggerLevel().name();
}

@CheckForNull
private String getExternalUserAuthentication() {
SecurityRealm realm = securityRealmFactory.getRealm();
return realm == null ? null : realm.getName();
}

private List<String> getEnabledIdentityProviders() {
return identityProviderRepository.getAllEnabledAndSorted()
.stream()
.filter(IdentityProvider::isEnabled)
.map(IdentityProvider::getName)
.collect(MoreCollectors.toList());
}

private List<String> getAllowsToSignUpEnabledIdentityProviders() {
return identityProviderRepository.getAllEnabledAndSorted()
.stream()
.filter(IdentityProvider::isEnabled)
.filter(IdentityProvider::allowsUsersToSignUp)
.map(IdentityProvider::getName)
.collect(MoreCollectors.toList());
}

private boolean getForceAuthentication() {
return config.getBoolean(CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY).orElse(false);
}

private boolean isOfficialDistribution() {
// the dependency com.sonarsource:sonarsource-branding is shaded to webapp
// during release (see sonar-web pom)
File brandingFile = new File(server.getRootDir(), BRANDING_FILE_PATH);
// no need to check that the file exists. java.io.File#length() returns zero in this case.
return brandingFile.length() > 0L;
}

@Override
public String name() {
return "SonarQube";
}

@Override
public ProtobufSystemInfo.Section toProtobuf() {
ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
protobuf.setName(name());

serverIdLoader.get().ifPresent(serverId -> {
setAttribute(protobuf, "Server ID", serverId.getId());
setAttribute(protobuf, "Server ID validated", serverId.isValid());
});
Health health = healthChecker.checkNode();
setAttribute(protobuf, "Health", health.getStatus().name());
setAttribute(protobuf, "Health Causes", health.getCauses());
setAttribute(protobuf, "Version", getVersion());
setAttribute(protobuf, "External User Authentication", getExternalUserAuthentication());
addIfNotEmpty(protobuf, "Accepted external identity providers", getEnabledIdentityProviders());
addIfNotEmpty(protobuf, "External identity providers whose users are allowed to sign themselves up", getAllowsToSignUpEnabledIdentityProviders());
setAttribute(protobuf, "Force authentication", getForceAuthentication());
setAttribute(protobuf, "Official Distribution", isOfficialDistribution());
setAttribute(protobuf, "Home Dir", config.get(ProcessProperties.PATH_HOME).orElse(null));
setAttribute(protobuf, "Data Dir", config.get(ProcessProperties.PATH_DATA).orElse(null));
setAttribute(protobuf, "Temp Dir", config.get(ProcessProperties.PATH_TEMP).orElse(null));
setAttribute(protobuf, "Logs Dir", config.get(ProcessProperties.PATH_LOGS).orElse(null));
setAttribute(protobuf, "Logs Level", getLogLevel());
return protobuf.build();
}

private static void addIfNotEmpty(ProtobufSystemInfo.Section.Builder protobuf, String key, @Nullable List<String> values) {
if (values != null && !values.isEmpty()) {
setAttribute(protobuf, key, COMMA_JOINER.join(values));
}
}
}

+ 112
- 57
server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSection.java Ver arquivo

@@ -19,85 +19,140 @@
*/
package org.sonar.server.platform.monitoring;

import java.lang.management.ClassLoadingMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.RuntimeMXBean;
import java.lang.management.ThreadMXBean;
import java.util.Date;
import org.sonar.api.utils.System2;
import org.sonar.process.systeminfo.SystemInfoSection;
import com.google.common.base.Joiner;
import java.io.File;
import java.util.List;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.api.CoreProperties;
import org.sonar.api.config.Configuration;
import org.sonar.api.platform.Server;
import org.sonar.api.security.SecurityRealm;
import org.sonar.api.server.authentication.IdentityProvider;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.process.ProcessProperties;
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
import org.sonar.server.authentication.IdentityProviderRepository;
import org.sonar.server.health.Health;
import org.sonar.server.health.HealthChecker;
import org.sonar.server.platform.ServerIdLoader;
import org.sonar.server.platform.ServerLogging;
import org.sonar.server.user.SecurityRealmFactory;

import static java.lang.String.format;
import static org.sonar.api.utils.DateUtils.formatDateTime;
import static org.sonar.process.systeminfo.SystemInfoUtils.setAttribute;

/**
* JVM runtime information. Not exported as a MXBean because these information
* are natively provided.
*/
public class SystemSection implements SystemInfoSection {
private final System2 system;
public class SystemSection extends BaseSectionMBean implements SystemSectionMBean {

private static final Joiner COMMA_JOINER = Joiner.on(", ");

static final String BRANDING_FILE_PATH = "web/WEB-INF/classes/com/sonarsource/branding";

public SystemSection() {
this(System2.INSTANCE);
private final Configuration config;
private final SecurityRealmFactory securityRealmFactory;
private final IdentityProviderRepository identityProviderRepository;
private final Server server;
private final ServerLogging serverLogging;
private final ServerIdLoader serverIdLoader;
private final HealthChecker healthChecker;

public SystemSection(Configuration config, SecurityRealmFactory securityRealmFactory,
IdentityProviderRepository identityProviderRepository, Server server, ServerLogging serverLogging,
ServerIdLoader serverIdLoader, HealthChecker healthChecker) {
this.config = config;
this.securityRealmFactory = securityRealmFactory;
this.identityProviderRepository = identityProviderRepository;
this.server = server;
this.serverLogging = serverLogging;
this.serverIdLoader = serverIdLoader;
this.healthChecker = healthChecker;
}

SystemSection(System2 system) {
this.system = system;
@Override
public String getServerId() {
return serverIdLoader.getRaw().orElse(null);
}

@Override
public ProtobufSystemInfo.Section toProtobuf() {
ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
protobuf.setName("System");
public String getVersion() {
return server.getVersion();
}

setAttribute(protobuf, "System Date", formatDateTime(new Date(system.now())));
setAttribute(protobuf, "Start Time", formatDateTime(new Date(runtimeMXBean().getStartTime())));
setAttribute(protobuf, "JVM Vendor", runtimeMXBean().getVmVendor());
setAttribute(protobuf, "JVM Name", runtimeMXBean().getVmName());
setAttribute(protobuf, "JVM Version", runtimeMXBean().getVmVersion());
setAttribute(protobuf, "Processors", runtime().availableProcessors());
setAttribute(protobuf, "System Classpath", runtimeMXBean().getClassPath());
setAttribute(protobuf, "BootClassPath", runtimeMXBean().getBootClassPath());
setAttribute(protobuf, "Library Path", runtimeMXBean().getLibraryPath());
setAttribute(protobuf, "Total Memory", formatMemory(runtime().totalMemory()));
setAttribute(protobuf, "Free Memory", formatMemory(runtime().freeMemory()));
setAttribute(protobuf, "Max Memory", formatMemory(runtime().maxMemory()));
setAttribute(protobuf, "Heap", memoryMXBean().getHeapMemoryUsage().toString());
setAttribute(protobuf, "Non Heap", memoryMXBean().getNonHeapMemoryUsage().toString());
setAttribute(protobuf, "System Load Average", format("%.1f%% (last minute)", ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage() * 100.0));
setAttribute(protobuf, "Loaded Classes", classLoadingMXBean().getLoadedClassCount());
setAttribute(protobuf, "Total Loaded Classes", classLoadingMXBean().getTotalLoadedClassCount());
setAttribute(protobuf, "Unloaded Classes", classLoadingMXBean().getUnloadedClassCount());
setAttribute(protobuf, "Threads", threadMXBean().getThreadCount());
setAttribute(protobuf, "Threads Peak", threadMXBean().getPeakThreadCount());
setAttribute(protobuf, "Daemon Thread", threadMXBean().getDaemonThreadCount());
return protobuf.build();
@Override
public String getLogLevel() {
return serverLogging.getRootLoggerLevel().name();
}

private static RuntimeMXBean runtimeMXBean() {
return ManagementFactory.getRuntimeMXBean();
@CheckForNull
private String getExternalUserAuthentication() {
SecurityRealm realm = securityRealmFactory.getRealm();
return realm == null ? null : realm.getName();
}

private static Runtime runtime() {
return Runtime.getRuntime();
private List<String> getEnabledIdentityProviders() {
return identityProviderRepository.getAllEnabledAndSorted()
.stream()
.filter(IdentityProvider::isEnabled)
.map(IdentityProvider::getName)
.collect(MoreCollectors.toList());
}

private static MemoryMXBean memoryMXBean() {
return ManagementFactory.getMemoryMXBean();
private List<String> getAllowsToSignUpEnabledIdentityProviders() {
return identityProviderRepository.getAllEnabledAndSorted()
.stream()
.filter(IdentityProvider::isEnabled)
.filter(IdentityProvider::allowsUsersToSignUp)
.map(IdentityProvider::getName)
.collect(MoreCollectors.toList());
}

private static ClassLoadingMXBean classLoadingMXBean() {
return ManagementFactory.getClassLoadingMXBean();
private boolean getForceAuthentication() {
return config.getBoolean(CoreProperties.CORE_FORCE_AUTHENTICATION_PROPERTY).orElse(false);
}

private static ThreadMXBean threadMXBean() {
return ManagementFactory.getThreadMXBean();
private boolean isOfficialDistribution() {
// the dependency com.sonarsource:sonarsource-branding is shaded to webapp
// during release (see sonar-web pom)
File brandingFile = new File(server.getRootDir(), BRANDING_FILE_PATH);
// no need to check that the file exists. java.io.File#length() returns zero in this case.
return brandingFile.length() > 0L;
}

@Override
public String name() {
// JMX name
return "SonarQube";
}

@Override
public ProtobufSystemInfo.Section toProtobuf() {
ProtobufSystemInfo.Section.Builder protobuf = ProtobufSystemInfo.Section.newBuilder();
protobuf.setName("System");

serverIdLoader.get().ifPresent(serverId -> {
setAttribute(protobuf, "Server ID", serverId.getId());
setAttribute(protobuf, "Server ID validated", serverId.isValid());
});
Health health = healthChecker.checkNode();
setAttribute(protobuf, "Health", health.getStatus().name());
setAttribute(protobuf, "Health Causes", health.getCauses());
setAttribute(protobuf, "Version", getVersion());
setAttribute(protobuf, "External User Authentication", getExternalUserAuthentication());
addIfNotEmpty(protobuf, "Accepted external identity providers", getEnabledIdentityProviders());
addIfNotEmpty(protobuf, "External identity providers whose users are allowed to sign themselves up", getAllowsToSignUpEnabledIdentityProviders());
setAttribute(protobuf, "High Availability", false);
setAttribute(protobuf, "Official Distribution", isOfficialDistribution());
setAttribute(protobuf, "Force authentication", getForceAuthentication());
setAttribute(protobuf, "Home Dir", config.get(ProcessProperties.PATH_HOME).orElse(null));
setAttribute(protobuf, "Data Dir", config.get(ProcessProperties.PATH_DATA).orElse(null));
setAttribute(protobuf, "Temp Dir", config.get(ProcessProperties.PATH_TEMP).orElse(null));
setAttribute(protobuf, "Logs Dir", config.get(ProcessProperties.PATH_LOGS).orElse(null));
setAttribute(protobuf, "Logs Level", getLogLevel());
return protobuf.build();
}

private static String formatMemory(long memoryInBytes) {
return format("%d MB", memoryInBytes / 1_000_000);
private static void addIfNotEmpty(ProtobufSystemInfo.Section.Builder protobuf, String key, @Nullable List<String> values) {
if (values != null && !values.isEmpty()) {
setAttribute(protobuf, key, COMMA_JOINER.join(values));
}
}
}

server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSectionMBean.java → server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java Ver arquivo

@@ -21,7 +21,7 @@ package org.sonar.server.platform.monitoring;

import javax.annotation.CheckForNull;

public interface SonarQubeSectionMBean {
public interface SystemSectionMBean {
@CheckForNull
String getServerId();


+ 3
- 3
server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/WebSystemInfoModule.java Ver arquivo

@@ -35,10 +35,10 @@ public class WebSystemInfoModule {
new JvmPropertiesSection("Web JVM Properties"),
new JvmStateSection("Web JVM State"),
DatabaseSection.class,
EsSection.class,
EsStateSection.class,
EsStatisticsSection.class,
PluginsSection.class,
SettingsSection.class,
SonarQubeSection.class,
SystemSection.class,

StandaloneInfoAction.class
@@ -50,7 +50,7 @@ public class WebSystemInfoModule {
new JvmPropertiesSection("Web JVM Properties"),
new JvmStateSection("Web JVM State"),
DatabaseSection.class,
EsSection.class,
EsStateSection.class,
PluginsSection.class,

ClusterInfoAction.class

+ 8
- 7
server/sonar-server/src/main/java/org/sonar/server/platform/ws/ClusterInfoAction.java Ver arquivo

@@ -19,6 +19,7 @@
*/
package org.sonar.server.platform.ws;

import java.util.Arrays;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
@@ -50,14 +51,16 @@ public class ClusterInfoAction implements SystemWsAction {
try (JsonWriter json = response.newJsonWriter()) {
json.beginObject();

// global section
json.prop("Cluster", true);
json.prop("Cluster Name", "foo");
json.name("System");
json.beginObject();
json.prop("High Availability", true);
json.prop("Cluster Name", "fooo");
json.prop("Server Id", "ABC123");
json.prop("Health", "RED");
json
.name("Health Causes")
.beginArray().beginObject().prop("message", "Requires at least two search nodes").endObject().endArray();
.beginArray().values(Arrays.asList("foo", "bar")).endArray();
json.endObject();

json.name("Settings");
json.beginObject();
@@ -65,8 +68,6 @@ public class ClusterInfoAction implements SystemWsAction {
json.prop("sonar.externalIdentityProviders", "GitHub, BitBucket");
json.endObject();



json.name("Database");
json
.beginObject()
@@ -83,7 +84,7 @@ public class ClusterInfoAction implements SystemWsAction {
.prop("workersPerNode", 4)
.endObject();

json.name("Elasticsearch");
json.name("Search");
json
.beginObject()
.prop("Health", "GREEN")

server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsSectionTest.java → server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java Ver arquivo

@@ -35,21 +35,20 @@ import static org.mockito.Mockito.when;
import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;

public class EsSectionTest {
public class EsStateSectionTest {

@Rule
public EsTester esTester = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()));

private EsSection underTest = new EsSection(esTester.client());
private EsStateSection underTest = new EsStateSection(esTester.client());

@Test
public void name() {
assertThat(underTest.name()).isEqualTo("Elasticsearch");
assertThat(underTest.toProtobuf().getName()).isEqualTo("Search State");
}

@Test
public void es_state() {
assertThat(underTest.getState()).isEqualTo(ClusterHealthStatus.GREEN.name());
assertThatAttributeIs(underTest.toProtobuf(), "State", ClusterHealthStatus.GREEN.name());
}

@@ -59,20 +58,10 @@ public class EsSectionTest {
assertThat(attribute(section, "Store Size")).isNotNull();
}

@Test
public void index_attributes() {
ProtobufSystemInfo.Section section = underTest.toProtobuf();

// one index "issues"
assertThat(attribute(section, "Index issues - Docs").getLongValue()).isEqualTo(0L);
assertThat(attribute(section, "Index issues - Shards").getLongValue()).isGreaterThan(0);
assertThat(attribute(section, "Index issues - Store Size").getStringValue()).isNotNull();
}

@Test
public void attributes_displays_exception_message_when_cause_null_when_client_fails() {
EsClient esClientMock = mock(EsClient.class);
EsSection underTest = new EsSection(esClientMock);
EsStateSection underTest = new EsStateSection(esClientMock);
when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with no cause"));

ProtobufSystemInfo.Section section = underTest.toProtobuf();
@@ -82,7 +71,7 @@ public class EsSectionTest {
@Test
public void attributes_displays_exception_message_when_cause_is_not_ElasticSearchException_when_client_fails() {
EsClient esClientMock = mock(EsClient.class);
EsSection underTest = new EsSection(esClientMock);
EsStateSection underTest = new EsStateSection(esClientMock);
when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with cause not ES", new IllegalArgumentException("some cause message")));

ProtobufSystemInfo.Section section = underTest.toProtobuf();
@@ -92,7 +81,7 @@ public class EsSectionTest {
@Test
public void attributes_displays_cause_message_when_cause_is_ElasticSearchException_when_client_fails() {
EsClient esClientMock = mock(EsClient.class);
EsSection underTest = new EsSection(esClientMock);
EsStateSection underTest = new EsStateSection(esClientMock);
when(esClientMock.prepareClusterStats()).thenThrow(new RuntimeException("RuntimeException with ES cause", new ElasticsearchException("some cause message")));

ProtobufSystemInfo.Section section = underTest.toProtobuf();

+ 88
- 0
server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStatisticsSectionTest.java Ver arquivo

@@ -0,0 +1,88 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.platform.monitoring;

import org.elasticsearch.ElasticsearchException;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
import org.sonar.server.es.EsClient;
import org.sonar.server.es.EsTester;
import org.sonar.server.issue.index.IssueIndexDefinition;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;

public class EsStatisticsSectionTest {

@Rule
public EsTester esTester = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()));

private EsStatisticsSection underTest = new EsStatisticsSection(esTester.client());

@Test
public void name() {
assertThat(underTest.toProtobuf().getName()).isEqualTo("Search Statistics");
}

@Test
public void index_attributes() {
ProtobufSystemInfo.Section section = underTest.toProtobuf();

// one index "issues"
assertThat(attribute(section, "Index issues - Docs").getLongValue()).isEqualTo(0L);
assertThat(attribute(section, "Index issues - Shards").getLongValue()).isGreaterThan(0);
assertThat(attribute(section, "Index issues - Store Size").getStringValue()).isNotNull();
}

@Test
public void attributes_displays_exception_message_when_cause_null_when_client_fails() {
EsClient esClientMock = mock(EsClient.class);
EsStatisticsSection underTest = new EsStatisticsSection(esClientMock);
when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with no cause"));

ProtobufSystemInfo.Section section = underTest.toProtobuf();
assertThatAttributeIs(section, "Error", "RuntimeException with no cause");
}

@Test
public void attributes_displays_exception_message_when_cause_is_not_ElasticSearchException_when_client_fails() {
EsClient esClientMock = mock(EsClient.class);
EsStatisticsSection underTest = new EsStatisticsSection(esClientMock);
when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with cause not ES", new IllegalArgumentException("some cause message")));

ProtobufSystemInfo.Section section = underTest.toProtobuf();
assertThatAttributeIs(section, "Error", "RuntimeException with cause not ES");
}

@Test
public void attributes_displays_cause_message_when_cause_is_ElasticSearchException_when_client_fails() {
EsClient esClientMock = mock(EsClient.class);
EsStatisticsSection underTest = new EsStatisticsSection(esClientMock);
when(esClientMock.prepareStats()).thenThrow(new RuntimeException("RuntimeException with ES cause", new ElasticsearchException("some cause message")));

ProtobufSystemInfo.Section section = underTest.toProtobuf();
assertThatAttributeIs(section, "Error", "some cause message");
}
}

+ 0
- 217
server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SonarQubeSectionTest.java Ver arquivo

@@ -1,217 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.server.platform.monitoring;

import java.io.File;
import java.util.Optional;
import org.apache.commons.io.FileUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.platform.Server;
import org.sonar.api.security.SecurityRealm;
import org.sonar.api.utils.log.LoggerLevel;
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
import org.sonar.server.authentication.IdentityProviderRepositoryRule;
import org.sonar.server.authentication.TestIdentityProvider;
import org.sonar.server.health.Health;
import org.sonar.server.health.TestStandaloneHealthChecker;
import org.sonar.server.platform.ServerId;
import org.sonar.server.platform.ServerIdLoader;
import org.sonar.server.platform.ServerLogging;
import org.sonar.server.user.SecurityRealmFactory;

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;

public class SonarQubeSectionTest {

private static final String SERVER_ID_PROPERTY = "Server ID";
private static final String SERVER_ID_VALIDATED_PROPERTY = "Server ID validated";

@Rule
public TemporaryFolder temp = new TemporaryFolder();

@Rule
public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule();

private MapSettings settings = new MapSettings();
private Server server = mock(Server.class);
private ServerIdLoader serverIdLoader = mock(ServerIdLoader.class);
private ServerLogging serverLogging = mock(ServerLogging.class);
private SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class);
private TestStandaloneHealthChecker healthChecker = new TestStandaloneHealthChecker();

private SonarQubeSection underTest = new SonarQubeSection(settings.asConfig(), securityRealmFactory, identityProviderRepository, server,
serverLogging, serverIdLoader, healthChecker);

@Before
public void setUp() throws Exception {
when(serverLogging.getRootLoggerLevel()).thenReturn(LoggerLevel.DEBUG);
when(serverIdLoader.getRaw()).thenReturn(Optional.empty());
when(serverIdLoader.get()).thenReturn(Optional.empty());
}

@Test
public void name_is_not_empty() {
assertThat(underTest.name()).isNotEmpty();
}

@Test
public void test_getServerId() {
when(serverIdLoader.getRaw()).thenReturn(Optional.of("ABC"));
assertThat(underTest.getServerId()).isEqualTo("ABC");

when(serverIdLoader.getRaw()).thenReturn(Optional.empty());
assertThat(underTest.getServerId()).isNull();
}

@Test
public void attributes_contain_information_about_valid_server_id() {
when(serverIdLoader.get()).thenReturn(Optional.of(new ServerId("ABC", true)));

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, SERVER_ID_PROPERTY, "ABC");
assertThatAttributeIs(protobuf, SERVER_ID_VALIDATED_PROPERTY, true);
}

@Test
public void attributes_contain_information_about_non_valid_server_id() {
when(serverIdLoader.get()).thenReturn(Optional.of(new ServerId("ABC", false)));

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, SERVER_ID_PROPERTY, "ABC");
assertThatAttributeIs(protobuf, SERVER_ID_VALIDATED_PROPERTY, false);
}

@Test
public void attributes_do_not_contain_information_about_server_id_if_absent() {
when(serverIdLoader.get()).thenReturn(Optional.empty());

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThat(attribute(protobuf, SERVER_ID_PROPERTY)).isNull();
assertThat(attribute(protobuf, SERVER_ID_VALIDATED_PROPERTY)).isNull();
}

@Test
public void official_distribution() throws Exception {
File rootDir = temp.newFolder();
FileUtils.write(new File(rootDir, SonarQubeSection.BRANDING_FILE_PATH), "1.2");

when(server.getRootDir()).thenReturn(rootDir);

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "Official Distribution", true);
}

@Test
public void not_an_official_distribution() throws Exception {
File rootDir = temp.newFolder();
// branding file is missing
when(server.getRootDir()).thenReturn(rootDir);

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "Official Distribution", false);
}

@Test
public void get_log_level() throws Exception {
ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "Logs Level", "DEBUG");
}

@Test
public void get_realm() throws Exception {
SecurityRealm realm = mock(SecurityRealm.class);
when(realm.getName()).thenReturn("LDAP");
when(securityRealmFactory.getRealm()).thenReturn(realm);

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "External User Authentication", "LDAP");
}

@Test
public void no_realm() throws Exception {
when(securityRealmFactory.getRealm()).thenReturn(null);

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThat(attribute(protobuf, "External User Authentication")).isNull();
}

@Test
public void get_enabled_identity_providers() throws Exception {
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("github")
.setName("GitHub")
.setEnabled(true));
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("bitbucket")
.setName("Bitbucket")
.setEnabled(true));
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("disabled")
.setName("Disabled")
.setEnabled(false));

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "Accepted external identity providers", "Bitbucket, GitHub");
}

@Test
public void get_enabled_identity_providers_allowing_users_to_signup() throws Exception {
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("github")
.setName("GitHub")
.setEnabled(true)
.setAllowsUsersToSignUp(true));
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("bitbucket")
.setName("Bitbucket")
.setEnabled(true)
.setAllowsUsersToSignUp(false));
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("disabled")
.setName("Disabled")
.setEnabled(false)
.setAllowsUsersToSignUp(true));

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "External identity providers whose users are allowed to sign themselves up", "GitHub");
}

@Test
public void return_health() {
healthChecker.setHealth(Health.newHealthCheckBuilder()
.setStatus(Health.Status.YELLOW)
.addCause("foo")
.addCause("bar")
.build());

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "Health", "YELLOW");
SystemInfoTesting.assertThatAttributeHasOnlyValues(protobuf, "Health Causes", asList("foo", "bar"));
}
}

+ 180
- 7
server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SystemSectionTest.java Ver arquivo

@@ -19,26 +19,199 @@
*/
package org.sonar.server.platform.monitoring;

import java.io.File;
import java.util.Optional;
import org.apache.commons.io.FileUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.platform.Server;
import org.sonar.api.security.SecurityRealm;
import org.sonar.api.utils.log.LoggerLevel;
import org.sonar.process.systeminfo.protobuf.ProtobufSystemInfo;
import org.sonar.server.authentication.IdentityProviderRepositoryRule;
import org.sonar.server.authentication.TestIdentityProvider;
import org.sonar.server.health.Health;
import org.sonar.server.health.TestStandaloneHealthChecker;
import org.sonar.server.platform.ServerId;
import org.sonar.server.platform.ServerIdLoader;
import org.sonar.server.platform.ServerLogging;
import org.sonar.server.user.SecurityRealmFactory;

import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.process.systeminfo.SystemInfoUtils.attribute;
import static org.sonar.server.platform.monitoring.SystemInfoTesting.assertThatAttributeIs;

public class SystemSectionTest {

private SystemSection underTest = new SystemSection();
private static final String SERVER_ID_PROPERTY = "Server ID";
private static final String SERVER_ID_VALIDATED_PROPERTY = "Server ID validated";

@Rule
public TemporaryFolder temp = new TemporaryFolder();

@Rule
public IdentityProviderRepositoryRule identityProviderRepository = new IdentityProviderRepositoryRule();

private MapSettings settings = new MapSettings();
private Server server = mock(Server.class);
private ServerIdLoader serverIdLoader = mock(ServerIdLoader.class);
private ServerLogging serverLogging = mock(ServerLogging.class);
private SecurityRealmFactory securityRealmFactory = mock(SecurityRealmFactory.class);
private TestStandaloneHealthChecker healthChecker = new TestStandaloneHealthChecker();

private SystemSection underTest = new SystemSection(settings.asConfig(), securityRealmFactory, identityProviderRepository, server,
serverLogging, serverIdLoader, healthChecker);

@Before
public void setUp() throws Exception {
when(serverLogging.getRootLoggerLevel()).thenReturn(LoggerLevel.DEBUG);
when(serverIdLoader.getRaw()).thenReturn(Optional.empty());
when(serverIdLoader.get()).thenReturn(Optional.empty());
}

@Test
public void name_is_not_empty() {
assertThat(underTest.name()).isNotEmpty();
}

@Test
public void test_getServerId() {
when(serverIdLoader.getRaw()).thenReturn(Optional.of("ABC"));
assertThat(underTest.getServerId()).isEqualTo("ABC");

when(serverIdLoader.getRaw()).thenReturn(Optional.empty());
assertThat(underTest.getServerId()).isNull();
}

@Test
public void attributes_contain_information_about_valid_server_id() {
when(serverIdLoader.get()).thenReturn(Optional.of(new ServerId("ABC", true)));

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, SERVER_ID_PROPERTY, "ABC");
assertThatAttributeIs(protobuf, SERVER_ID_VALIDATED_PROPERTY, true);
}

@Test
public void attributes_contain_information_about_non_valid_server_id() {
when(serverIdLoader.get()).thenReturn(Optional.of(new ServerId("ABC", false)));

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, SERVER_ID_PROPERTY, "ABC");
assertThatAttributeIs(protobuf, SERVER_ID_VALIDATED_PROPERTY, false);
}

@Test
public void attributes_do_not_contain_information_about_server_id_if_absent() {
when(serverIdLoader.get()).thenReturn(Optional.empty());

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThat(attribute(protobuf, SERVER_ID_PROPERTY)).isNull();
assertThat(attribute(protobuf, SERVER_ID_VALIDATED_PROPERTY)).isNull();
}

@Test
public void name() {
assertThat(underTest.toProtobuf().getName()).isEqualTo("System");
public void official_distribution() throws Exception {
File rootDir = temp.newFolder();
FileUtils.write(new File(rootDir, SystemSection.BRANDING_FILE_PATH), "1.2");

when(server.getRootDir()).thenReturn(rootDir);

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "Official Distribution", true);
}

@Test
public void not_an_official_distribution() throws Exception {
File rootDir = temp.newFolder();
// branding file is missing
when(server.getRootDir()).thenReturn(rootDir);

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "Official Distribution", false);
}

@Test
public void get_log_level() throws Exception {
ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "Logs Level", "DEBUG");
}

@Test
public void get_realm() throws Exception {
SecurityRealm realm = mock(SecurityRealm.class);
when(realm.getName()).thenReturn("LDAP");
when(securityRealmFactory.getRealm()).thenReturn(realm);

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "External User Authentication", "LDAP");
}

@Test
public void no_realm() throws Exception {
when(securityRealmFactory.getRealm()).thenReturn(null);

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThat(attribute(protobuf, "External User Authentication")).isNull();
}

@Test
public void get_enabled_identity_providers() throws Exception {
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("github")
.setName("GitHub")
.setEnabled(true));
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("bitbucket")
.setName("Bitbucket")
.setEnabled(true));
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("disabled")
.setName("Disabled")
.setEnabled(false));

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "Accepted external identity providers", "Bitbucket, GitHub");
}

@Test
public void get_enabled_identity_providers_allowing_users_to_signup() throws Exception {
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("github")
.setName("GitHub")
.setEnabled(true)
.setAllowsUsersToSignUp(true));
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("bitbucket")
.setName("Bitbucket")
.setEnabled(true)
.setAllowsUsersToSignUp(false));
identityProviderRepository.addIdentityProvider(new TestIdentityProvider()
.setKey("disabled")
.setName("Disabled")
.setEnabled(false)
.setAllowsUsersToSignUp(true));

ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "External identity providers whose users are allowed to sign themselves up", "GitHub");
}

@Test
public void system_properties() {
ProtobufSystemInfo.Section section = underTest.toProtobuf();
public void return_health() {
healthChecker.setHealth(Health.newHealthCheckBuilder()
.setStatus(Health.Status.YELLOW)
.addCause("foo")
.addCause("bar")
.build());

assertThat(attribute(section, "System Date").getStringValue()).isNotEmpty();
assertThat(attribute(section, "Processors").getLongValue()).isGreaterThan(0);
ProtobufSystemInfo.Section protobuf = underTest.toProtobuf();
assertThatAttributeIs(protobuf, "Health", "YELLOW");
SystemInfoTesting.assertThatAttributeHasOnlyValues(protobuf, "Health Causes", asList("foo", "bar"));
}
}

+ 1
- 2
server/sonar-web/src/main/js/api/system.ts Ver arquivo

@@ -44,14 +44,13 @@ export interface NodeInfo extends SysValueObject {
Name: string;
Health: HealthType;
'Health Causes': HealthCause[];
'Logs Level': string;
}

export interface SysInfo extends SysValueObject {
Cluster: boolean;
Health: HealthType;
'Health Causes': HealthCause[];
'Application Nodes': NodeInfo[];
'Search Nodes': NodeInfo[];
}

export function setLogLevel(level: string): Promise<void | Response> {

+ 0
- 101
server/sonar-web/src/main/js/apps/system/__tests__/system-test.js Ver arquivo

@@ -1,101 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { shallow } from 'enzyme';
import ItemValue from '../item-value';
import ItemBoolean from '../item-boolean';
import ItemObject from '../item-object';
import ItemLogLevel from '../item-log-level';

describe('Item Value', () => {
it('should render string', () => {
const result = shallow(<ItemValue value="/some/path/as/an/example" />);
expect(result.find('code').text()).toBe('/some/path/as/an/example');
});
});

describe('ItemBoolean', () => {
it('should render `true`', () => {
const result = shallow(<ItemBoolean value={true} />);
expect(result.find('.icon-check').length).toBe(1);
});

it('should render `false`', () => {
const result = shallow(<ItemBoolean value={false} />);
expect(result.find('.icon-delete').length).toBe(1);
});
});

describe('ItemObject', () => {
it('should render object', () => {
const result = shallow(<ItemObject value={{ name: 'Java', version: '3.2' }} />);
expect(result.find('table').length).toBe(1);
expect(result.find('tr').length).toBe(2);
});

it('should render `true` inside object', () => {
const result = shallow(<ItemObject value={{ isCool: true }} />);
const itemValue = result.find(ItemValue);
expect(itemValue.length).toBe(1);
expect(itemValue.prop('value')).toBe(true);
});

it('should render object inside object', () => {
const result = shallow(
<ItemObject value={{ users: { docs: 1, shards: 5 }, tests: { docs: 68, shards: 5 } }} />
);
expect(result.find(ItemValue).length).toBe(2);
expect(
result
.find(ItemValue)
.at(0)
.prop('value')
).toEqual({ docs: 1, shards: 5 });
expect(
result
.find(ItemValue)
.at(1)
.prop('value')
).toEqual({ docs: 68, shards: 5 });
});
});

describe('Log Level', () => {
it('should render select box', () => {
const result = shallow(<ItemLogLevel value="INFO" />);
expect(result.find('select').length).toBe(1);
expect(result.find('option').length).toBe(3);
});

it('should set initial value', () => {
const result = shallow(<ItemLogLevel value="DEBUG" />);
expect(result.find('select').prop('value')).toBe('DEBUG');
});

it('should render warning', () => {
const result = shallow(<ItemLogLevel value="DEBUG" />);
expect(result.find('.alert').length).toBe(1);
});

it('should not render warning', () => {
const result = shallow(<ItemLogLevel value="INFO" />);
expect(result.find('.alert').length).toBe(0);
});
});

+ 22
- 6
server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts Ver arquivo

@@ -17,27 +17,43 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as utils from '../utils';
import * as u from '../utils';

describe('parseQuery', () => {
it('should correctly parse the expand array', () => {
expect(utils.parseQuery({})).toEqual({ expandedCards: [] });
expect(utils.parseQuery({ expand: 'foo,bar' })).toEqual({ expandedCards: ['foo', 'bar'] });
expect(u.parseQuery({})).toEqual({ expandedCards: [] });
expect(u.parseQuery({ expand: 'foo,bar' })).toEqual({ expandedCards: ['foo', 'bar'] });
});
});

describe('serializeQuery', () => {
it('should correctly serialize the expand array', () => {
expect(utils.serializeQuery({ expandedCards: [] })).toEqual({});
expect(utils.serializeQuery({ expandedCards: ['foo', 'bar'] })).toEqual({ expand: 'foo,bar' });
expect(u.serializeQuery({ expandedCards: [] })).toEqual({});
expect(u.serializeQuery({ expandedCards: ['foo', 'bar'] })).toEqual({ expand: 'foo,bar' });
});
});

describe('groupSections', () => {
it('should correctly group the root field into a main section', () => {
expect(utils.groupSections({ foo: 'Foo', bar: 3, baz: { a: 'a' } })).toEqual({
expect(u.groupSections({ foo: 'Foo', bar: 3, baz: { a: 'a' } })).toEqual({
mainSection: { foo: 'Foo', bar: 3 },
sections: { baz: { a: 'a' } }
});
});
});

describe('getSystemLogsLevel', () => {
it('should correctly return log level for standalone mode', () => {
expect(u.getSystemLogsLevel({ 'Logs Level': 'FOO' } as u.StandaloneSysInfo)).toBe('FOO');
expect(u.getSystemLogsLevel({} as u.StandaloneSysInfo)).toBe('INFO');
expect(u.getSystemLogsLevel()).toBe('INFO');
});
it('should return the worst log level for cluster mode', () => {
expect(
u.getSystemLogsLevel({
Cluster: true,
'Application Nodes': [{ 'Logs Level': 'INFO' }, { 'Logs Level': 'DEBUG' }]
} as u.ClusterSysInfo)
).toBe('DEBUG');
});
});

+ 21
- 7
server/sonar-web/src/main/js/apps/system/components/App.tsx Ver arquivo

@@ -22,10 +22,18 @@ import * as PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import ClusterSysInfos from './ClusterSysInfos';
import PageHeader from './PageHeader';
import StandAloneSysInfos from './StandAloneSysInfos';
import StandaloneSysInfos from './StandaloneSysInfos';
import { translate } from '../../../helpers/l10n';
import { getSystemInfo, SysInfo } from '../../../api/system';
import { isCluster, parseQuery, Query, serializeQuery } from '../utils';
import {
ClusterSysInfo,
getSystemLogsLevel,
isCluster,
parseQuery,
Query,
serializeQuery,
StandaloneSysInfo
} from '../utils';
import { RawQuery } from '../../../helpers/query';
import '../styles.css';

@@ -84,7 +92,7 @@ export default class App extends React.PureComponent<Props, State> {

updateQuery = (newQuery: Query) => {
const query = serializeQuery({ ...parseQuery(this.props.location.query), ...newQuery });
this.context.router.push({ pathname: this.props.location.pathname, query });
this.context.router.replace({ pathname: this.props.location.pathname, query });
};

renderSysInfo() {
@@ -97,26 +105,32 @@ export default class App extends React.PureComponent<Props, State> {
if (isCluster(sysInfoData)) {
return (
<ClusterSysInfos
sysInfoData={sysInfoData}
expandedCards={query.expandedCards}
sysInfoData={sysInfoData as ClusterSysInfo}
toggleCard={this.toggleSysInfoCards}
/>
);
}
return <StandAloneSysInfos sysInfoData={sysInfoData} />;
return (
<StandaloneSysInfos
expandedCards={query.expandedCards}
sysInfoData={sysInfoData as StandaloneSysInfo}
toggleCard={this.toggleSysInfoCards}
/>
);
}

render() {
const { loading, sysInfoData } = this.state;
// TODO Correctly get logLevel, we are not sure yet how we want to do it for cluster mode
return (
<div className="page page-limited">
<Helmet title={translate('system_info.page')} />
<PageHeader
loading={loading}
isCluster={isCluster(sysInfoData)}
logLevel="INFO"
logLevel={getSystemLogsLevel(sysInfoData)}
showActions={sysInfoData != undefined}
onLogLevelChange={this.fetchSysInfo}
/>
{this.renderSysInfo()}
</div>

+ 4
- 4
server/sonar-web/src/main/js/apps/system/components/ChangeLogLevelForm.tsx Ver arquivo

@@ -21,6 +21,7 @@ import * as React from 'react';
import Modal from 'react-modal';
import { setLogLevel } from '../../../api/system';
import { translate } from '../../../helpers/l10n';
import { LOGS_LEVELS } from '../utils';

interface Props {
infoMsg: string;
@@ -34,8 +35,6 @@ interface State {
updating: boolean;
}

const LOG_LEVELS = ['INFO', 'DEBUG', 'TRACE'];

export default class ChangeLogLevelForm extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
@@ -78,9 +77,10 @@ export default class ChangeLogLevelForm extends React.PureComponent<Props, State
<h2>{header}</h2>
</div>
<div className="modal-body">
{LOG_LEVELS.map(level => (
{LOGS_LEVELS.map(level => (
<p key={level} className="spacer-bottom">
<input
id={`loglevel-${level}`}
type="radio"
className="spacer-right text-middle"
name="system.log_levels"
@@ -88,7 +88,7 @@ export default class ChangeLogLevelForm extends React.PureComponent<Props, State
checked={level === newLevel}
onChange={this.handleLevelChange}
/>
{level}
<label htmlFor={`loglevel-${level}`}>{level}</label>
</p>
))}
<div className="alert alert-info spacer-top">{this.props.infoMsg}</div>

+ 4
- 4
server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx Ver arquivo

@@ -22,19 +22,19 @@ import { sortBy } from 'lodash';
import HealthCard from './info-items/HealthCard';
import { translate } from '../../../helpers/l10n';
import {
ClusterSysInfo,
getAppNodes,
getHealth,
getHealthCauses,
getMainCardSection,
getClusterMainCardSection,
getNodeName,
getSearchNodes,
ignoreInfoFields
} from '../utils';
import { SysInfo } from '../../../api/system';

interface Props {
expandedCards: string[];
sysInfoData: SysInfo;
sysInfoData: ClusterSysInfo;
toggleCard: (toggledCard: string) => void;
}

@@ -49,7 +49,7 @@ export default function ClusterSysInfos({ expandedCards, sysInfoData, toggleCard
name={mainCardName}
onClick={toggleCard}
open={expandedCards.includes(mainCardName)}
sysInfoData={ignoreInfoFields(getMainCardSection(sysInfoData))}
sysInfoData={ignoreInfoFields(getClusterMainCardSection(sysInfoData))}
/>
<li className="note system-info-health-title">
{translate('system.application_nodes_title')}

+ 4
- 2
server/sonar-web/src/main/js/apps/system/components/PageActions.tsx Ver arquivo

@@ -28,6 +28,7 @@ interface Props {
canRestart: boolean;
cluster: boolean;
logLevel: string;
onLogLevelChange: () => void;
}

interface State {
@@ -40,9 +41,9 @@ export default class PageActions extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
logLevel: props.logLevel,
openLogsLevelForm: false,
openRestartForm: false,
logLevel: props.logLevel
openRestartForm: false
};
}

@@ -59,6 +60,7 @@ export default class PageActions extends React.PureComponent<Props, State> {

handleLogsLevelChange = (logLevel: string) => {
this.setState({ logLevel });
this.props.onLogLevelChange();
this.handleLogsLevelClose();
};


+ 9
- 7
server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx Ver arquivo

@@ -26,21 +26,23 @@ interface Props {
loading: boolean;
logLevel: string;
showActions: boolean;
onLogLevelChange: () => void;
}

export default function PageHeader({ isCluster, loading, logLevel, showActions }: Props) {
export default function PageHeader(props: Props) {
return (
<header className="page-header">
<h1 className="page-title">{translate('system_info.page')}</h1>
{showActions && (
{props.showActions && (
<PageActions
canDownloadLogs={!isCluster}
canRestart={!isCluster}
cluster={isCluster}
logLevel={logLevel}
canDownloadLogs={!props.isCluster}
canRestart={!props.isCluster}
cluster={props.isCluster}
logLevel={props.logLevel}
onLogLevelChange={props.onLogLevelChange}
/>
)}
{loading && (
{props.loading && (
<div className="page-actions">
<i className="spinner" />
</div>

+ 62
- 0
server/sonar-web/src/main/js/apps/system/components/StandaloneSysInfos.tsx Ver arquivo

@@ -0,0 +1,62 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { map } from 'lodash';
import HealthCard from './info-items/HealthCard';
import {
getHealth,
getHealthCauses,
getStandaloneMainSections,
getStandaloneSecondarySections,
ignoreInfoFields,
StandaloneSysInfo
} from '../utils';

interface Props {
expandedCards: string[];
sysInfoData: StandaloneSysInfo;
toggleCard: (toggledCard: string) => void;
}

export default function StandAloneSysInfos({ expandedCards, sysInfoData, toggleCard }: Props) {
const mainCardName = 'System';
return (
<ul>
<HealthCard
biggerHealth={true}
health={getHealth(sysInfoData)}
healthCauses={getHealthCauses(sysInfoData)}
name={mainCardName}
onClick={toggleCard}
open={expandedCards.includes(mainCardName)}
sysInfoData={ignoreInfoFields(getStandaloneMainSections(sysInfoData))}
/>
{map(getStandaloneSecondarySections(sysInfoData), (section, name) => (
<HealthCard
key={name}
name={name}
onClick={toggleCard}
open={expandedCards.includes(name)}
sysInfoData={ignoreInfoFields(section)}
/>
))}
</ul>
);
}

+ 24
- 4
server/sonar-web/src/main/js/apps/system/components/__tests__/ClusterSysInfos-test.tsx Ver arquivo

@@ -20,18 +20,38 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import ClusterSysInfos from '../ClusterSysInfos';
import { HealthType, SysInfo } from '../../../../api/system';
import { HealthType } from '../../../../api/system';
import { ClusterSysInfo } from '../../utils';

const sysInfoData: SysInfo = {
const sysInfoData: ClusterSysInfo = {
Cluster: true,
Health: HealthType.RED,
Name: 'Foo',
'Health Causes': [{ message: 'Database down' }],
'Application Nodes': [{ Name: 'Bar', Health: HealthType.GREEN, 'Health Causes': [] }],
'Search Nodes': [{ Name: 'Baz', Health: HealthType.YELLOW, 'Health Causes': [] }]
'Application Nodes': [
{ Name: 'Bar', Health: HealthType.GREEN, 'Health Causes': [], 'Logs Level': 'INFO' }
],
'Search Nodes': [
{ Name: 'Baz', Health: HealthType.YELLOW, 'Health Causes': [], 'Logs Level': 'INFO' }
]
};

it('should render correctly', () => {
expect(
getWrapper({
sysInfoData: {
...sysInfoData,
'Application Nodes': [
{ Name: 'Foo', Health: HealthType.GREEN, 'Health Causes': [], 'Logs Level': 'INFO' },
{ Name: 'Bar', Health: HealthType.RED, 'Health Causes': [], 'Logs Level': 'DEBUG' },
{ Name: 'Baz', Health: HealthType.YELLOW, 'Health Causes': [], 'Logs Level': 'TRACE' }
]
}
}).find('HealthCard')
).toHaveLength(5);
});

it('should support more than two nodes', () => {
expect(getWrapper()).toMatchSnapshot();
});


+ 28
- 4
server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx Ver arquivo

@@ -25,7 +25,13 @@ import { click } from '../../../../helpers/testUtils';
it('should render correctly', () => {
expect(
shallow(
<PageActions canDownloadLogs={true} canRestart={true} cluster={false} logLevel="INFO" />
<PageActions
canDownloadLogs={true}
canRestart={true}
cluster={false}
logLevel="INFO"
onLogLevelChange={() => {}}
/>
)
).toMatchSnapshot();
});
@@ -33,14 +39,26 @@ it('should render correctly', () => {
it('should render without restart and log download', () => {
expect(
shallow(
<PageActions canDownloadLogs={false} canRestart={false} cluster={true} logLevel="INFO" />
<PageActions
canDownloadLogs={false}
canRestart={false}
cluster={true}
logLevel="INFO"
onLogLevelChange={() => {}}
/>
)
).toMatchSnapshot();
});

it('should open restart modal', () => {
const wrapper = shallow(
<PageActions canDownloadLogs={true} canRestart={true} cluster={false} logLevel="INFO" />
<PageActions
canDownloadLogs={true}
canRestart={true}
cluster={false}
logLevel="INFO"
onLogLevelChange={() => {}}
/>
);
click(wrapper.find('#restart-server-button'));
expect(wrapper.find('RestartForm')).toHaveLength(1);
@@ -48,7 +66,13 @@ it('should open restart modal', () => {

it('should open change log level modal', () => {
const wrapper = shallow(
<PageActions canDownloadLogs={true} canRestart={true} cluster={false} logLevel="INFO" />
<PageActions
canDownloadLogs={true}
canRestart={true}
cluster={false}
logLevel="INFO"
onLogLevelChange={() => {}}
/>
);
click(wrapper.find('#edit-logs-level-button'));
expect(wrapper.find('ChangeLogLevelForm')).toHaveLength(1);

+ 18
- 2
server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx Ver arquivo

@@ -23,12 +23,28 @@ import PageHeader from '../PageHeader';

it('should render correctly', () => {
expect(
shallow(<PageHeader isCluster={true} loading={false} logLevel="INFO" showActions={true} />)
shallow(
<PageHeader
isCluster={true}
loading={false}
logLevel="INFO"
showActions={true}
onLogLevelChange={() => {}}
/>
)
).toMatchSnapshot();
});

it('should show a loading spinner and no actions', () => {
expect(
shallow(<PageHeader isCluster={true} loading={true} logLevel="INFO" showActions={false} />)
shallow(
<PageHeader
isCluster={true}
loading={true}
logLevel="INFO"
showActions={false}
onLogLevelChange={() => {}}
/>
)
).toMatchSnapshot();
});

server/sonar-web/src/main/js/apps/system/item-value.js → server/sonar-web/src/main/js/apps/system/components/__tests__/StandaloneSysInfos-test.tsx Ver arquivo

@@ -17,29 +17,34 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import ItemBoolean from './item-boolean';
import ItemObject from './item-object';
import ItemLogLevel from './item-log-level';
import * as React from 'react';
import { shallow } from 'enzyme';
import StandaloneSysInfos from '../StandaloneSysInfos';
import { HealthType } from '../../../../api/system';
import { StandaloneSysInfo } from '../../utils';

export default class ItemValue extends React.PureComponent {
render() {
if (this.props.name === 'Logs Level') {
return <ItemLogLevel value={this.props.value} />;
}
const sysInfoData: StandaloneSysInfo = {
Cluster: true,
Health: HealthType.RED,
'Logs Level': 'DEBUG',
Name: 'Foo',
'Health Causes': [{ message: 'Database down' }],
'Web JVM': { 'Max Memory': '2Gb' },
'Compute Engine': { Pending: 4 },
Elasticsearch: { 'Number of Nodes': 1 }
};

const rawValue = this.props.value;
let formattedValue;
switch (typeof this.props.value) {
case 'boolean':
formattedValue = <ItemBoolean value={rawValue} />;
break;
case 'object':
formattedValue = <ItemObject value={rawValue} />;
break;
default:
formattedValue = <code>{rawValue}</code>;
}
return formattedValue;
}
it('should render correctly', () => {
expect(getWrapper()).toMatchSnapshot();
});

function getWrapper(props = {}) {
return shallow(
<StandaloneSysInfos
expandedCards={['Compute Engine', 'Foo']}
sysInfoData={sysInfoData}
toggleCard={() => {}}
{...props}
/>
);
}

+ 36
- 6
server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ChangeLogLevelForm-test.tsx.snap Ver arquivo

@@ -34,12 +34,17 @@ exports[`should display some warning messages for non INFO levels 1`] = `
<input
checked={false}
className="spacer-right text-middle"
id="loglevel-INFO"
name="system.log_levels"
onChange={[Function]}
type="radio"
value="INFO"
/>
INFO
<label
htmlFor="loglevel-INFO"
>
INFO
</label>
</p>
<p
className="spacer-bottom"
@@ -47,12 +52,17 @@ exports[`should display some warning messages for non INFO levels 1`] = `
<input
checked={true}
className="spacer-right text-middle"
id="loglevel-DEBUG"
name="system.log_levels"
onChange={[Function]}
type="radio"
value="DEBUG"
/>
DEBUG
<label
htmlFor="loglevel-DEBUG"
>
DEBUG
</label>
</p>
<p
className="spacer-bottom"
@@ -60,12 +70,17 @@ exports[`should display some warning messages for non INFO levels 1`] = `
<input
checked={false}
className="spacer-right text-middle"
id="loglevel-TRACE"
name="system.log_levels"
onChange={[Function]}
type="radio"
value="TRACE"
/>
TRACE
<label
htmlFor="loglevel-TRACE"
>
TRACE
</label>
</p>
<div
className="alert alert-info spacer-top"
@@ -133,12 +148,17 @@ exports[`should render correctly 1`] = `
<input
checked={true}
className="spacer-right text-middle"
id="loglevel-INFO"
name="system.log_levels"
onChange={[Function]}
type="radio"
value="INFO"
/>
INFO
<label
htmlFor="loglevel-INFO"
>
INFO
</label>
</p>
<p
className="spacer-bottom"
@@ -146,12 +166,17 @@ exports[`should render correctly 1`] = `
<input
checked={false}
className="spacer-right text-middle"
id="loglevel-DEBUG"
name="system.log_levels"
onChange={[Function]}
type="radio"
value="DEBUG"
/>
DEBUG
<label
htmlFor="loglevel-DEBUG"
>
DEBUG
</label>
</p>
<p
className="spacer-bottom"
@@ -159,12 +184,17 @@ exports[`should render correctly 1`] = `
<input
checked={false}
className="spacer-right text-middle"
id="loglevel-TRACE"
name="system.log_levels"
onChange={[Function]}
type="radio"
value="TRACE"
/>
TRACE
<label
htmlFor="loglevel-TRACE"
>
TRACE
</label>
</p>
<div
className="alert alert-info spacer-top"

+ 3
- 1
server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap Ver arquivo

@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
exports[`should support more than two nodes 1`] = `
<ul>
<HealthCard
biggerHealth={true}
@@ -34,6 +34,7 @@ exports[`should render correctly 1`] = `
open={false}
sysInfoData={
Object {
"Logs Level": "INFO",
"Name": "Bar",
}
}
@@ -51,6 +52,7 @@ exports[`should render correctly 1`] = `
open={false}
sysInfoData={
Object {
"Logs Level": "INFO",
"Name": "Baz",
}
}

+ 1
- 0
server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageHeader-test.tsx.snap Ver arquivo

@@ -14,6 +14,7 @@ exports[`should render correctly 1`] = `
canRestart={false}
cluster={true}
logLevel="INFO"
onLogLevelChange={[Function]}
/>
</header>
`;

+ 66
- 0
server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/StandaloneSysInfos-test.tsx.snap Ver arquivo

@@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<ul>
<HealthCard
biggerHealth={true}
health="RED"
healthCauses={
Array [
Object {
"message": "Database down",
},
]
}
name="System"
onClick={[Function]}
open={false}
sysInfoData={
Object {
"Logs Level": "DEBUG",
"Name": "Foo",
}
}
/>
<HealthCard
name="Web"
onClick={[Function]}
open={false}
sysInfoData={
Object {
"Web Database Connectivity": undefined,
"Web JVM": Object {
"Max Memory": "2Gb",
},
"Web JVM Properties": undefined,
}
}
/>
<HealthCard
name="Compute Engine"
onClick={[Function]}
open={true}
sysInfoData={
Object {
"Compute Engine JVM": undefined,
"Compute Engine JVM Properties": undefined,
"Pending": 4,
}
}
/>
<HealthCard
name="Search"
onClick={[Function]}
open={false}
sysInfoData={
Object {
"Elasticsearch": Object {
"Number of Nodes": 1,
},
"Search JVM": undefined,
"Search JVM Properties": undefined,
}
}
/>
</ul>
`;

+ 20
- 9
server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx Ver arquivo

@@ -24,12 +24,13 @@ import HealthItem from './HealthItem';
import OpenCloseIcon from '../../../../components/icons-components/OpenCloseIcon';
import Section from './Section';
import { HealthType, HealthCause, SysValueObject } from '../../../../api/system';
import { groupSections } from '../../utils';
import { LOGS_LEVELS, groupSections, getLogsLevel } from '../../utils';
import { translate } from '../../../../helpers/l10n';

interface Props {
biggerHealth?: boolean;
health: HealthType;
healthCauses: HealthCause[];
health?: HealthType;
healthCauses?: HealthCause[];
onClick: (toggledCard: string) => void;
open: boolean;
name: string;
@@ -48,10 +49,12 @@ export default class HealthCard extends React.PureComponent<Props, State> {
onDetailLeave = () => this.setState({ hoveringDetail: false });

render() {
const { open, sysInfoData } = this.props;
const { health, open, sysInfoData } = this.props;
const { mainSection, sections } = groupSections(sysInfoData);
const showFields = open && mainSection && Object.keys(mainSection).length > 0;
const showSections = open && sections;
const logLevel = getLogsLevel(sysInfoData);
const showLogLevelWarning = logLevel && logLevel !== LOGS_LEVELS[0];
return (
<li
className={classNames('boxed-group system-info-health-card', {
@@ -62,11 +65,19 @@ export default class HealthCard extends React.PureComponent<Props, State> {
<OpenCloseIcon className="little-spacer-right" open={open} />
{this.props.name}
</span>
<HealthItem
className={classNames('pull-right', { 'big-dot': this.props.biggerHealth })}
health={this.props.health}
healthCauses={this.props.healthCauses}
/>
{health && (
<HealthItem
biggerHealth={this.props.biggerHealth}
className="pull-right spacer-left"
health={health}
healthCauses={this.props.healthCauses}
/>
)}
{showLogLevelWarning && (
<span className="pull-right alert alert-danger">
{translate('system.log_level.warning.short')}
</span>
)}
</div>
{open && (
<div

+ 4
- 2
server/sonar-web/src/main/js/apps/system/components/info-items/HealthItem.tsx Ver arquivo

@@ -20,15 +20,17 @@
import * as React from 'react';
import * as classNames from 'classnames';
import HealthCauseItem from './HealthCauseItem';
import StatusIndicator from '../../../../components/common/StatusIndicator';
import { HealthType, HealthCause } from '../../../../api/system';

interface Props {
biggerHealth?: boolean;
className?: string;
health: HealthType;
healthCauses?: HealthCause[];
}

export default function HealthItem({ className, health, healthCauses }: Props) {
export default function HealthItem({ biggerHealth, className, health, healthCauses }: Props) {
const hasHealthCauses = healthCauses && healthCauses.length > 0 && health !== HealthType.GREEN;
return (
<div className={classNames('system-info-health-info', className)}>
@@ -36,7 +38,7 @@ export default function HealthItem({ className, health, healthCauses }: Props) {
healthCauses!.map((cause, idx) => (
<HealthCauseItem key={idx} className="spacer-right" health={health} healthCause={cause} />
))}
<span className={classNames('system-info-health-dot', health)} />
<StatusIndicator color={health.toLowerCase()} size={biggerHealth ? 'big' : undefined} />
</div>
);
}

+ 6
- 0
server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx Ver arquivo

@@ -48,6 +48,12 @@ it('should show a main section and multiple sub sections', () => {
expect(getShallowWrapper({ open: true, sysInfoData })).toMatchSnapshot();
});

it('should display the log level alert', () => {
expect(
getShallowWrapper({ sysInfoData: { 'Logs Level': 'DEBUG' } }).find('.alert')
).toMatchSnapshot();
});

function getShallowWrapper(props = {}) {
return shallow(
<HealthCard

+ 3
- 1
server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthItem-test.tsx Ver arquivo

@@ -24,7 +24,9 @@ import { HealthType } from '../../../../../api/system';

it('should render correctly', () => {
expect(
shallow(<HealthItem health={HealthType.RED} healthCauses={[{ message: 'foo' }]} />)
shallow(
<HealthItem biggerHealth={true} health={HealthType.RED} healthCauses={[{ message: 'foo' }]} />
)
).toMatchSnapshot();
});


+ 14
- 3
server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap Ver arquivo

@@ -1,5 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display the log level alert 1`] = `
<span
className="pull-right alert alert-danger"
>
system.log_level.warning.short
</span>
`;

exports[`should display the sysinfo detail 1`] = `
<li
className="boxed-group system-info-health-card"
@@ -18,7 +26,8 @@ exports[`should display the sysinfo detail 1`] = `
Foobar
</span>
<HealthItem
className="pull-right big-dot"
biggerHealth={true}
className="pull-right spacer-left"
health="RED"
healthCauses={
Array [
@@ -55,7 +64,8 @@ exports[`should render correctly 1`] = `
Foobar
</span>
<HealthItem
className="pull-right"
biggerHealth={false}
className="pull-right spacer-left"
health="RED"
healthCauses={
Array [
@@ -87,7 +97,8 @@ exports[`should show a main section and multiple sub sections 1`] = `
Foobar
</span>
<HealthItem
className="pull-right"
biggerHealth={false}
className="pull-right spacer-left"
health="RED"
healthCauses={
Array [

+ 9
- 8
server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthItem-test.tsx.snap Ver arquivo

@@ -4,8 +4,8 @@ exports[`should not render health causes 1`] = `
<div
className="system-info-health-info"
>
<span
className="system-info-health-dot GREEN"
<StatusIndicator
color="green"
/>
</div>
`;
@@ -14,8 +14,8 @@ exports[`should not render health causes 2`] = `
<div
className="system-info-health-info"
>
<span
className="system-info-health-dot YELLOW"
<StatusIndicator
color="yellow"
/>
</div>
`;
@@ -33,8 +33,9 @@ exports[`should render correctly 1`] = `
}
}
/>
<span
className="system-info-health-dot RED"
<StatusIndicator
color="red"
size="big"
/>
</div>
`;
@@ -61,8 +62,8 @@ exports[`should render multiple health causes 1`] = `
}
}
/>
<span
className="system-info-health-dot YELLOW"
<StatusIndicator
color="yellow"
/>
</div>
`;

+ 0
- 30
server/sonar-web/src/main/js/apps/system/item-boolean.js Ver arquivo

@@ -1,30 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';

export default class ItemBoolean extends React.PureComponent {
render() {
if (this.props.value) {
return <i className="icon-check" />;
} else {
return <i className="icon-delete" />;
}
}
}

+ 0
- 60
server/sonar-web/src/main/js/apps/system/item-log-level.js Ver arquivo

@@ -1,60 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { setLogLevel } from '../../api/system';
import { translate } from '../../helpers/l10n';

const LOG_LEVELS = ['INFO', 'DEBUG', 'TRACE'];

export default class ItemLogLevel extends React.PureComponent {
constructor(props) {
super(props);
this.state = { level: props.value };
}

onChange = () => {
const newValue = this.refs.select.value;
setLogLevel(newValue).then(() => {
this.setState({ level: newValue });
});
};

render() {
const options = LOG_LEVELS.map(level => (
<option key={level} value={level}>
{level}
</option>
));
const warning =
this.state.level !== 'INFO' ? (
<div className="alert alert-danger spacer-top" style={{ wordBreak: 'normal' }}>
{translate('system.log_level.warning')}
</div>
) : null;
return (
<div>
<select ref="select" onChange={this.onChange} value={this.state.level}>
{options}
</select>
{warning}
</div>
);
}
}

+ 0
- 41
server/sonar-web/src/main/js/apps/system/item-object.js Ver arquivo

@@ -1,41 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import ItemValue from './item-value';

export default class ItemObject extends React.PureComponent {
render() {
const rows = Object.keys(this.props.value).map(key => {
return (
<tr key={key}>
<td className="thin nowrap">{key}</td>
<td>
<ItemValue value={this.props.value[key]} />
</td>
</tr>
);
});
return (
<table className="data">
<tbody>{rows}</tbody>
</table>
);
}
}

+ 0
- 127
server/sonar-web/src/main/js/apps/system/main.js Ver arquivo

@@ -1,127 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import Helmet from 'react-helmet';
import { sortBy } from 'lodash';
import { getSystemInfo } from '../../api/system';
import Section from './section';
import { translate } from '../../helpers/l10n';
import RestartForm from '../../components/common/RestartForm';

const SECTIONS_ORDER = [
'SonarQube',
'Database',
'System',
'Elasticsearch State',
'Elasticsearch',
'Compute Engine Tasks',
'Compute Engine State',
'Compute Engine Database Connection',
'JvmProperties'
];

export default class Main extends React.PureComponent {
state = { openRestartForm: false };

componentDidMount() {
getSystemInfo().then(info => this.setState({ sections: this.parseSections(info) }));
}

parseSections = data => {
const sections = Object.keys(data).map(section => {
return { name: section, items: this.parseItems(data[section]) };
});
return this.orderSections(sections);
};

orderSections = sections => sortBy(sections, section => SECTIONS_ORDER.indexOf(section.name));

parseItems = data => {
const items = Object.keys(data).map(item => {
return { name: item, value: data[item] };
});
return this.orderItems(items);
};

orderItems = items => sortBy(items, 'name');

handleServerRestartOpen = () => this.setState({ openRestartForm: true });
handleServerRestartClose = () => this.setState({ openRestartForm: false });

render() {
let sections = null;
if (this.state && this.state.sections) {
sections = this.state.sections
.filter(section => SECTIONS_ORDER.indexOf(section.name) >= 0)
.map(section => (
<Section key={section.name} section={section.name} items={section.items} />
));
}

return (
<div className="page page-limited">
<Helmet title={translate('system_info.page')} />
<header className="page-header">
<h1 className="page-title">{translate('system_info.page')}</h1>
<div className="page-actions">
<a href={window.baseUrl + '/api/system/info'} id="download-link">
Download
</a>
<div className="display-inline-block dropdown big-spacer-left">
<button data-toggle="dropdown">
Logs <i className="icon-dropdown" />
</button>
<ul className="dropdown-menu">
<li>
<a href={window.baseUrl + '/api/system/logs?process=app'} id="logs-link">
Main Process
</a>
</li>
<li>
<a href={window.baseUrl + '/api/system/logs?process=ce'} id="ce-logs-link">
Compute Engine
</a>
</li>
<li>
<a href={window.baseUrl + '/api/system/logs?process=es'} id="es-logs-link">
Elasticsearch
</a>
</li>
<li>
<a href={window.baseUrl + '/api/system/logs?process=web'} id="web-logs-link">
Web Server
</a>
</li>
</ul>
</div>
<button
id="restart-server-button"
className="big-spacer-left"
onClick={this.handleServerRestartOpen}>
Restart Server
</button>
{this.state.openRestartForm && <RestartForm onClose={this.handleServerRestartClose} />}
</div>
</header>
{sections}
</div>
);
}
}

+ 1
- 7
server/sonar-web/src/main/js/apps/system/routes.ts Ver arquivo

@@ -17,19 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { RouterState, RouteComponent, IndexRouteProps } from 'react-router';
import { RouterState, IndexRouteProps } from 'react-router';

const routes = [
{
getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) {
import('./components/App').then(i => callback(null, { component: i.default }));
}
},
{
path: 'old',
getComponent(_: RouterState, callback: (err: any, component: RouteComponent) => any) {
import('./main').then(i => callback(null, (i as any).default));
}
}
];


+ 0
- 49
server/sonar-web/src/main/js/apps/system/section.js Ver arquivo

@@ -1,49 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import ItemValue from './item-value';

export default class Section extends React.PureComponent {
render() {
const items = this.props.items.map(item => {
return (
<tr key={item.name}>
<td className="thin">
<div style={{ width: '25vw', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{item.name}
</div>
</td>
<td style={{ wordBreak: 'break-all' }}>
<ItemValue name={item.name} value={item.value} />
</td>
</tr>
);
});

return (
<div className="big-spacer-bottom">
<h3 className="spacer-bottom">{this.props.section}</h3>
<table className="data zebra" id={this.props.section}>
<tbody>{items}</tbody>
</table>
</div>
);
}
}

+ 9
- 29
server/sonar-web/src/main/js/apps/system/styles.css Ver arquivo

@@ -21,6 +21,10 @@
padding-bottom: 15px;
}

.system-info-health-card .boxed-group-header > .alert {
margin-top: -6px;
}

.system-info-health-card .boxed-group-inner {
padding-top: 0;
}
@@ -30,43 +34,19 @@
}

.system-info-health-info {
margin-top: -4px;
}

.system-info-health-dot {
display: inline-block;
width: 16px;
height: 16px;
margin: 4px;
border-radius: 16px;
box-sizing: border-box;
}

.system-info-health-dot.GREEN {
background-color: #00aa00;
margin-top: -12px;
}

.system-info-health-dot.YELLOW {
background-color: #eabe06;
}
.system-info-health-dot.RED {
background-color: #d4333f;
.system-info-health-info .status-indicator {
position: relative;
top: 8px;
}

.system-info-health-info .alert {
display: inline-block;
position: relative;
top: -8px;
}

.system-info-health-info.big-dot .system-info-health-dot {
width: 24px;
height: 24px;
margin: 0;
border-radius: 24px;
}

.system-info-health-info.no-margin .system-info-health-dot {
.system-info-health-info.no-margin .status-indicator {
margin: 0;
}


+ 74
- 9
server/sonar-web/src/main/js/apps/system/utils.ts Ver arquivo

@@ -17,7 +17,7 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { each, omit, memoize } from 'lodash';
import { each, omit, memoize, sortBy } from 'lodash';
import {
cleanQuery,
parseAsArray,
@@ -38,6 +38,16 @@ export interface Query {
expandedCards: string[];
}

export interface ClusterSysInfo extends SysInfo {
'Application Nodes': NodeInfo[];
'Search Nodes': NodeInfo[];
}

export interface StandaloneSysInfo extends SysInfo {
'Logs Level': string;
}

export const LOGS_LEVELS = ['INFO', 'DEBUG', 'TRACE'];
export const HEALTH_FIELD = 'Health';
export const HEALTHCAUSES_FIELD = 'Health Causes';

@@ -45,10 +55,6 @@ export function ignoreInfoFields(sysInfoObject: SysValueObject): SysValueObject
return omit(sysInfoObject, ['Cluster', HEALTH_FIELD, HEALTHCAUSES_FIELD]);
}

export function getAppNodes(sysInfoData: SysInfo): NodeInfo[] {
return sysInfoData['Application Nodes'];
}

export function getHealth(sysInfoObject: SysValueObject): HealthType {
return sysInfoObject[HEALTH_FIELD] as HealthType;
}
@@ -57,15 +63,74 @@ export function getHealthCauses(sysInfoObject: SysValueObject): HealthCause[] {
return sysInfoObject[HEALTHCAUSES_FIELD] as HealthCause[];
}

export function getMainCardSection(sysInfoData: SysInfo): SysValueObject {
return omit(sysInfoData, ['Application Nodes', 'Search Nodes', 'Settings', 'Statistics']);
export function getLogsLevel(sysInfoObject: SysValueObject): string {
return (sysInfoObject['Logs Level'] || LOGS_LEVELS[0]) as string;
}

export function getSystemLogsLevel(sysInfoData?: SysInfo): string {
const defaultLevel = LOGS_LEVELS[0];
if (!sysInfoData) {
return defaultLevel;
}
if (isCluster(sysInfoData)) {
const nodes = sortBy(getAppNodes(sysInfoData as ClusterSysInfo), node =>
LOGS_LEVELS.indexOf(getLogsLevel(node))
);
return nodes.length > 0 ? getLogsLevel(nodes[nodes.length - 1]) : defaultLevel;
} else {
return getLogsLevel(sysInfoData);
}
}

export function getNodeName(nodeInfo: NodeInfo): string {
return nodeInfo['Name'];
}

export function getSearchNodes(sysInfoData: SysInfo): NodeInfo[] {
export function getClusterMainCardSection(sysInfoData: ClusterSysInfo): SysValueObject {
return omit(sysInfoData, ['Application Nodes', 'Search Nodes', 'Settings', 'Statistics']);
}

export function getStandaloneMainSections(sysInfoData: StandaloneSysInfo): SysValueObject {
return omit(sysInfoData, [
'Settings',
'Statistics',
'Compute Engine',
'Compute Engine JVM',
'Compute Engine JVM Properties',
'Elasticsearch',
'Search JVM',
'Search JVM Properties',
'Web Database Connectivity',
'Web JVM',
'Web JVM Properties'
]);
}

export function getStandaloneSecondarySections(sysInfoData: StandaloneSysInfo): SysInfoSection {
return {
Web: {
'Web Database Connectivity': sysInfoData['Web Database Connectivity'],
'Web JVM': sysInfoData['Web JVM'],
'Web JVM Properties': sysInfoData['Web JVM Properties']
},
'Compute Engine': {
...sysInfoData['Compute Engine'] as SysValueObject,
'Compute Engine JVM': sysInfoData['Compute Engine JVM'],
'Compute Engine JVM Properties': sysInfoData['Compute Engine JVM Properties']
},
Search: {
Elasticsearch: sysInfoData['Elasticsearch'] as SysValueObject,
'Search JVM': sysInfoData['Search JVM'],
'Search JVM Properties': sysInfoData['Search JVM Properties']
}
};
}

export function getAppNodes(sysInfoData: ClusterSysInfo): NodeInfo[] {
return sysInfoData['Application Nodes'];
}

export function getSearchNodes(sysInfoData: ClusterSysInfo): NodeInfo[] {
return sysInfoData['Search Nodes'];
}

@@ -83,7 +148,7 @@ export function groupSections(sysInfoData: SysValueObject) {
}

export function isCluster(sysInfoData?: SysInfo): boolean {
return sysInfoData != undefined && sysInfoData['Cluster'];
return sysInfoData != undefined && sysInfoData['Cluster'] === true;
}

export const parseQuery = memoize((urlQuery: RawQuery): Query => {

+ 2
- 14
server/sonar-web/src/main/js/components/common/BranchStatus.css Ver arquivo

@@ -3,18 +3,6 @@
text-align: right;
}

.branch-status-indicator {
display: block;
width: 8px;
height: 8px;
border-radius: 8px;
margin: 4px 0;
}

.branch-status-indicator.is-failed {
background-color: #d4333f;
}

.branch-status-indicator.is-passed {
background-color: #00aa00;
.branch-status .status-indicator {
margin: 0;
}

+ 4
- 7
server/sonar-web/src/main/js/components/common/BranchStatus.tsx Ver arquivo

@@ -18,11 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as classNames from 'classnames';
import { Branch } from '../../app/types';
import Level from '../ui/Level';
import BugIcon from '../icons-components/BugIcon';
import CodeSmellIcon from '../icons-components/CodeSmellIcon';
import StatusIndicator from './StatusIndicator';
import VulnerabilityIcon from '../icons-components/VulnerabilityIcon';
import { isShortLivingBranch } from '../../helpers/branches';
import './BranchStatus.css';
@@ -41,22 +41,19 @@ export default function BranchStatus({ branch, concise = false }: Props) {
const totalIssues =
branch.status.bugs + branch.status.vulnerabilities + branch.status.codeSmells;

const indicatorClassName = classNames('branch-status-indicator', {
'is-failed': totalIssues > 0,
'is-passed': totalIssues === 0
});
const indicatorColor = totalIssues > 0 ? 'red' : 'green';

return concise ? (
<ul className="list-inline branch-status">
<li>{totalIssues}</li>
<li className="spacer-left">
<i className={indicatorClassName} />
<StatusIndicator color={indicatorColor} size="small" />
</li>
</ul>
) : (
<ul className="list-inline branch-status">
<li className="spacer-right">
<i className={indicatorClassName} />
<StatusIndicator color={indicatorColor} size="small" />
</li>
<li>
{branch.status.bugs}

+ 34
- 0
server/sonar-web/src/main/js/components/common/StatusIndicator.css Ver arquivo

@@ -0,0 +1,34 @@
.status-indicator {
display: inline-block;
box-sizing: border-box;
width: 16px;
height: 16px;
border-radius: 16px;
margin: 4px;
}

.status-indicator.small-status-indicator {
width: 8px;
height: 8px;
border-radius: 8px;
margin: 8px;
}

.status-indicator.big-status-indicator {
width: 24px;
height: 24px;
border-radius: 24px;
margin: 0;
}

.status-indicator.red {
background-color: #d4333f;
}

.sstatus-indicator.yellow {
background-color: #eabe06;
}

.status-indicator.green {
background-color: #00aa00;
}

server/sonar-web/src/main/js/apps/system/components/StandAloneSysInfos.tsx → server/sonar-web/src/main/js/components/common/StatusIndicator.tsx Ver arquivo

@@ -1,7 +1,7 @@
/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
* Copyright (C) 2009-2016 SonarSource SA
* mailto:contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
@@ -18,13 +18,27 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as classNames from 'classnames';
import './StatusIndicator.css';

interface Props {
sysInfoData: object;
className?: string;
color?: string;
size?: string;
}

export default class StandAloneSysInfos extends React.PureComponent<Props> {
render() {
return <div>StandAloneSysInfos</div>;
}
export default function StatusIndicator({ className, color, size }: Props) {
return (
<i
className={classNames(
'status-indicator',
color,
{
'small-status-indicator': size === 'small',
'big-status-indicator': size === 'big'
},
className
)}
/>
);
}

+ 9
- 6
server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap Ver arquivo

@@ -23,8 +23,9 @@ exports[`renders status of short-living branches 1`] = `
<li
className="spacer-right"
>
<i
className="branch-status-indicator is-passed"
<StatusIndicator
color="green"
size="small"
/>
</li>
<li>
@@ -49,8 +50,9 @@ exports[`renders status of short-living branches 2`] = `
<li
className="spacer-right"
>
<i
className="branch-status-indicator is-failed"
<StatusIndicator
color="red"
size="small"
/>
</li>
<li>
@@ -75,8 +77,9 @@ exports[`renders status of short-living branches 3`] = `
<li
className="spacer-right"
>
<i
className="branch-status-indicator is-failed"
<StatusIndicator
color="red"
size="small"
/>
</li>
<li>

+ 2
- 2
server/sonar-web/src/main/js/components/icons-components/OpenCloseIcon.tsx Ver arquivo

@@ -35,9 +35,9 @@ export default function OpenCloseIcon({ className, open, size = 14 }: Props) {
height={size}
style={{ fill: 'currentColor' }}>
{open ? (
<path d="M13.506 8.539l-5.191 5.184q-0.133 0.133-0.315 0.133t-0.315-0.133l-5.191-5.184q-0.133-0.133-0.133-0.318t0.133-0.318l1.161-1.154q0.133-0.133 0.315-0.133t0.315 0.133l3.715 3.715 3.715-3.715q0.133-0.133 0.315-0.133t0.315 0.133l1.161 1.154q0.133 0.133 0.133 0.318t-0.133 0.318z" />
<path d="M13.506 9.289l-5.191 5.184q-0.133 0.133-0.315 0.133t-0.315-0.133l-5.191-5.184q-0.133-0.133-0.133-0.318t0.133-0.318l1.161-1.154q0.133-0.133 0.315-0.133t0.315 0.133l3.715 3.715 3.715-3.715q0.133-0.133 0.315-0.133t0.315 0.133l1.161 1.154q0.133 0.133 0.133 0.318t-0.133 0.318z" />
) : (
<path d="M13.527 8.318l-5.244 5.244q-0.134 0.134-0.318 0.134t-0.318-0.134l-1.173-1.173q-0.134-0.134-0.134-0.318t0.134-0.318l3.753-3.753-3.753-3.753q-0.134-0.134-0.134-0.318t0.134-0.318l1.173-1.173q0.134-0.134 0.318-0.134t0.318 0.134l5.244 5.244q0.134 0.134 0.134 0.318t-0.134 0.318z" />
<path d="M13.527 9.318l-5.244 5.244q-0.134 0.134-0.318 0.134t-0.318-0.134l-1.173-1.173q-0.134-0.134-0.134-0.318t0.134-0.318l3.753-3.753-3.753-3.753q-0.134-0.134-0.134-0.318t0.134-0.318l1.173-1.173q0.134-0.134 0.318-0.134t0.318 0.134l5.244 5.244q0.134 0.134 0.134 0.318t-0.134 0.318z" />
)}
</svg>
);

+ 2
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Ver arquivo

@@ -2847,7 +2847,8 @@ system.cluster_log_level.info=Changes apply to all Application nodes but not to
system.download_logs=Download Logs
system.download_system_info=Download System Info
system.is_restarting=Server is restarting. This page will be automatically refreshed.
system.log_level.warning=Current level has performance impacts, please make sure to get back to INFO level once your investigation is done. Please note that when the server is restarted, logging will revert to the level configured in sonar.properties.
system.log_level.warning=This level has performance impacts, please make sure to get back to INFO level once your investigation is done. Please note that when the server is restarted, logging will revert to the level configured in sonar.properties.
system.log_level.warning.short=Current logs level has performance impacts, get back to INFO level.
system.log_level.info=Changes don't apply to Search.
system.logs_level=Logs level
system.restart_server=Restart Server

Carregando…
Cancelar
Salvar