diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2017-09-19 16:13:55 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2017-09-26 23:49:38 +0200 |
commit | 8fa40905cc8afa06f384ed455e06871126804751 (patch) | |
tree | b3bc5deab119ce93cc9be327ad7a59e6bb42243d /server | |
parent | 9c2a9a75fff56e56799273e4a61364ed51870b7e (diff) | |
download | sonarqube-8fa40905cc8afa06f384ed455e06871126804751.tar.gz sonarqube-8fa40905cc8afa06f384ed455e06871126804751.zip |
SONAR-9802 add information to System Info page
Diffstat (limited to 'server')
51 files changed, 1017 insertions, 1114 deletions
diff --git a/server/sonar-process/src/main/java/org/sonar/process/systeminfo/JvmStateSection.java b/server/sonar-process/src/main/java/org/sonar/process/systeminfo/JvmStateSection.java index 575083280b1..656fba7e75b 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/systeminfo/JvmStateSection.java +++ b/server/sonar-process/src/main/java/org/sonar/process/systeminfo/JvmStateSection.java @@ -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(); } diff --git a/server/sonar-process/src/test/java/org/sonar/process/systeminfo/JvmStateSectionTest.java b/server/sonar-process/src/test/java/org/sonar/process/systeminfo/JvmStateSectionTest.java index 5276a524353..3057e9933e9 100644 --- a/server/sonar-process/src/test/java/org/sonar/process/systeminfo/JvmStateSectionTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/systeminfo/JvmStateSectionTest.java @@ -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 diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSectionMBean.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSectionMBean.java deleted file mode 100644 index 6358f68593f..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSectionMBean.java +++ /dev/null @@ -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(); -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSection.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java index 2e86e6ef854..d6f5f0df07d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsSection.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStateSection.java @@ -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()) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStatisticsSection.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStatisticsSection.java new file mode 100644 index 00000000000..12656ae4f3e --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/EsStatisticsSection.java @@ -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())); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSection.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSection.java deleted file mode 100644 index ead5a7475fd..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSection.java +++ /dev/null @@ -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)); - } - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSection.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSection.java index bbd623f52b9..d19f7162ae4 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSection.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSection.java @@ -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)); + } } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSectionMBean.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java index 1a562a6bc30..7c3f8f01aa9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SonarQubeSectionMBean.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/SystemSectionMBean.java @@ -21,7 +21,7 @@ package org.sonar.server.platform.monitoring; import javax.annotation.CheckForNull; -public interface SonarQubeSectionMBean { +public interface SystemSectionMBean { @CheckForNull String getServerId(); diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/WebSystemInfoModule.java b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/WebSystemInfoModule.java index c565219240f..09c40749fc8 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/WebSystemInfoModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/monitoring/WebSystemInfoModule.java @@ -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 diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ClusterInfoAction.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ClusterInfoAction.java index 6f9125f40cc..d298d99c03a 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ClusterInfoAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ws/ClusterInfoAction.java @@ -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") diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsSectionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java index a55ae819df3..af248d63640 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsSectionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStateSectionTest.java @@ -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()); } @@ -60,19 +59,9 @@ public class EsSectionTest { } @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(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStatisticsSectionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStatisticsSectionTest.java new file mode 100644 index 00000000000..44c583ebb2e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/EsStatisticsSectionTest.java @@ -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"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SonarQubeSectionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SonarQubeSectionTest.java deleted file mode 100644 index fac46e81c31..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SonarQubeSectionTest.java +++ /dev/null @@ -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")); - } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SystemSectionTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SystemSectionTest.java index 94378667a59..c1ae9b3305d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SystemSectionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/monitoring/SystemSectionTest.java @@ -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")); } } diff --git a/server/sonar-web/src/main/js/api/system.ts b/server/sonar-web/src/main/js/api/system.ts index 1b4fd125a97..df9da694e40 100644 --- a/server/sonar-web/src/main/js/api/system.ts +++ b/server/sonar-web/src/main/js/api/system.ts @@ -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> { diff --git a/server/sonar-web/src/main/js/apps/system/__tests__/system-test.js b/server/sonar-web/src/main/js/apps/system/__tests__/system-test.js deleted file mode 100644 index 0ad07b2794a..00000000000 --- a/server/sonar-web/src/main/js/apps/system/__tests__/system-test.js +++ /dev/null @@ -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); - }); -}); diff --git a/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts index cd10c605418..0c550fd943f 100644 --- a/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/system/__tests__/utils-test.ts @@ -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'); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/system/components/App.tsx b/server/sonar-web/src/main/js/apps/system/components/App.tsx index 440b117929a..1cd3752de88 100644 --- a/server/sonar-web/src/main/js/apps/system/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/App.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/system/components/ChangeLogLevelForm.tsx b/server/sonar-web/src/main/js/apps/system/components/ChangeLogLevelForm.tsx index 3065a0be66d..6118fe2d956 100644 --- a/server/sonar-web/src/main/js/apps/system/components/ChangeLogLevelForm.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/ChangeLogLevelForm.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx b/server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx index 7f68dc6771a..cb3000dea32 100644 --- a/server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/ClusterSysInfos.tsx @@ -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')} diff --git a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx index 1b2ef92d34a..4e26c343f02 100644 --- a/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/PageActions.tsx @@ -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(); }; diff --git a/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx b/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx index c9d84569e30..97c4f1077c0 100644 --- a/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/PageHeader.tsx @@ -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> diff --git a/server/sonar-web/src/main/js/apps/system/components/StandaloneSysInfos.tsx b/server/sonar-web/src/main/js/apps/system/components/StandaloneSysInfos.tsx new file mode 100644 index 00000000000..4ed444cc66d --- /dev/null +++ b/server/sonar-web/src/main/js/apps/system/components/StandaloneSysInfos.tsx @@ -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> + ); +} diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/ClusterSysInfos-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/ClusterSysInfos-test.tsx index 5b703a1a214..21c60cbd6fc 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/ClusterSysInfos-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/ClusterSysInfos-test.tsx @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx index 098e897ec92..627347f8886 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageActions-test.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx index f5255c7228e..a101f213406 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/PageHeader-test.tsx @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/system/item-value.js b/server/sonar-web/src/main/js/apps/system/components/__tests__/StandaloneSysInfos-test.tsx index 5461aa95e8b..c70202e3219 100644 --- a/server/sonar-web/src/main/js/apps/system/item-value.js +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/StandaloneSysInfos-test.tsx @@ -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} + /> + ); } diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ChangeLogLevelForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ChangeLogLevelForm-test.tsx.snap index 04cfc11d49b..eeaa775462d 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ChangeLogLevelForm-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ChangeLogLevelForm-test.tsx.snap @@ -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" diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap index 20ed8d541f6..abd7d3a63ac 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/ClusterSysInfos-test.tsx.snap @@ -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", } } diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageHeader-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageHeader-test.tsx.snap index 2945b6df932..39f93da4322 100644 --- a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageHeader-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/PageHeader-test.tsx.snap @@ -14,6 +14,7 @@ exports[`should render correctly 1`] = ` canRestart={false} cluster={true} logLevel="INFO" + onLogLevelChange={[Function]} /> </header> `; diff --git a/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/StandaloneSysInfos-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/StandaloneSysInfos-test.tsx.snap new file mode 100644 index 00000000000..f4961aeac1e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/system/components/__tests__/__snapshots__/StandaloneSysInfos-test.tsx.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx b/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx index b45205dccba..459f26ee721 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/HealthCard.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/HealthItem.tsx b/server/sonar-web/src/main/js/apps/system/components/info-items/HealthItem.tsx index a0793a81831..86618c34c8a 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/HealthItem.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/HealthItem.tsx @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx index c080f444dec..98b33bd31ee 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthCard-test.tsx @@ -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 diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthItem-test.tsx b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthItem-test.tsx index f77622b9e92..9f1bd198352 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/HealthItem-test.tsx @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap index 724001990fa..7390ef8020e 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthCard-test.tsx.snap @@ -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 [ diff --git a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthItem-test.tsx.snap index d9fa53a2956..f962437404c 100644 --- a/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/system/components/info-items/__tests__/__snapshots__/HealthItem-test.tsx.snap @@ -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> `; diff --git a/server/sonar-web/src/main/js/apps/system/item-boolean.js b/server/sonar-web/src/main/js/apps/system/item-boolean.js deleted file mode 100644 index aeac6e88390..00000000000 --- a/server/sonar-web/src/main/js/apps/system/item-boolean.js +++ /dev/null @@ -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" />; - } - } -} diff --git a/server/sonar-web/src/main/js/apps/system/item-log-level.js b/server/sonar-web/src/main/js/apps/system/item-log-level.js deleted file mode 100644 index 89364f164d8..00000000000 --- a/server/sonar-web/src/main/js/apps/system/item-log-level.js +++ /dev/null @@ -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> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/system/item-object.js b/server/sonar-web/src/main/js/apps/system/item-object.js deleted file mode 100644 index d9386cbce8d..00000000000 --- a/server/sonar-web/src/main/js/apps/system/item-object.js +++ /dev/null @@ -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> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/system/main.js b/server/sonar-web/src/main/js/apps/system/main.js deleted file mode 100644 index 150bd042adf..00000000000 --- a/server/sonar-web/src/main/js/apps/system/main.js +++ /dev/null @@ -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> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/system/routes.ts b/server/sonar-web/src/main/js/apps/system/routes.ts index 1a8d6a5ab38..fcffeff191c 100644 --- a/server/sonar-web/src/main/js/apps/system/routes.ts +++ b/server/sonar-web/src/main/js/apps/system/routes.ts @@ -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)); - } } ]; diff --git a/server/sonar-web/src/main/js/apps/system/section.js b/server/sonar-web/src/main/js/apps/system/section.js deleted file mode 100644 index 89b90e99c5d..00000000000 --- a/server/sonar-web/src/main/js/apps/system/section.js +++ /dev/null @@ -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> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/system/styles.css b/server/sonar-web/src/main/js/apps/system/styles.css index f1156609add..f34dc3fa394 100644 --- a/server/sonar-web/src/main/js/apps/system/styles.css +++ b/server/sonar-web/src/main/js/apps/system/styles.css @@ -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; } diff --git a/server/sonar-web/src/main/js/apps/system/utils.ts b/server/sonar-web/src/main/js/apps/system/utils.ts index 41ad5223544..ae8818edf9d 100644 --- a/server/sonar-web/src/main/js/apps/system/utils.ts +++ b/server/sonar-web/src/main/js/apps/system/utils.ts @@ -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 => { diff --git a/server/sonar-web/src/main/js/components/common/BranchStatus.css b/server/sonar-web/src/main/js/components/common/BranchStatus.css index 74278d67573..ccfccb351f1 100644 --- a/server/sonar-web/src/main/js/components/common/BranchStatus.css +++ b/server/sonar-web/src/main/js/components/common/BranchStatus.css @@ -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; } diff --git a/server/sonar-web/src/main/js/components/common/BranchStatus.tsx b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx index 40688087cf0..bb9069bc234 100644 --- a/server/sonar-web/src/main/js/components/common/BranchStatus.tsx +++ b/server/sonar-web/src/main/js/components/common/BranchStatus.tsx @@ -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} diff --git a/server/sonar-web/src/main/js/components/common/StatusIndicator.css b/server/sonar-web/src/main/js/components/common/StatusIndicator.css new file mode 100644 index 00000000000..26008838cd2 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/StatusIndicator.css @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/system/components/StandAloneSysInfos.tsx b/server/sonar-web/src/main/js/components/common/StatusIndicator.tsx index 2a142c58a0e..6fc96354033 100644 --- a/server/sonar-web/src/main/js/apps/system/components/StandAloneSysInfos.tsx +++ b/server/sonar-web/src/main/js/components/common/StatusIndicator.tsx @@ -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 + )} + /> + ); } diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap index b106929fbe4..32c0d0a2afa 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/BranchStatus-test.tsx.snap @@ -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> diff --git a/server/sonar-web/src/main/js/components/icons-components/OpenCloseIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/OpenCloseIcon.tsx index 739bc6027e7..ba583b3de7c 100644 --- a/server/sonar-web/src/main/js/components/icons-components/OpenCloseIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons-components/OpenCloseIcon.tsx @@ -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> ); |