aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-03-06 17:41:23 +0100
committerSonarTech <sonartech@sonarsource.com>2018-03-26 20:20:57 +0200
commitcb0a23c978efcc296cf29837c9e6e1a657403404 (patch)
tree7e408a33f49254e99e47753c9d7b272c269c53c6
parentb4125add7a55db6d2dc71a1bd0b2cadbe5ff7887 (diff)
downloadsonarqube-cb0a23c978efcc296cf29837c9e6e1a657403404.tar.gz
sonarqube-cb0a23c978efcc296cf29837c9e6e1a657403404.zip
VSTS-141 Add VSTS Quality widget
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java6
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java40
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java57
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java119
-rw-r--r--server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html14
-rw-r--r--server/sonar-web/config/utils.js65
-rw-r--r--server/sonar-web/config/vsts.webpack.config.js110
-rw-r--r--server/sonar-web/config/webpack.config.js41
-rw-r--r--server/sonar-web/package.json8
-rw-r--r--server/sonar-web/public/integration/vsts/index.html26
-rw-r--r--server/sonar-web/scripts/start.js3
-rw-r--r--server/sonar-web/scripts/vsts.build.js97
-rw-r--r--server/sonar-web/scripts/vsts.start.js113
-rw-r--r--server/sonar-web/src/main/js/api/measures.ts2
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/Configuration.tsx147
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/QGWidget.tsx57
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/Widget.tsx106
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/index.js42
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/vsts.css89
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/spinner.css82
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/icons.css67
-rw-r--r--server/sonar-web/src/main/js/app/styles/sonar.css1
-rw-r--r--server/sonar-web/src/main/js/libs/third-party/VSS.SDK.min.js957
-rw-r--r--server/sonar-web/yarn.lock4
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java2
-rw-r--r--sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java15
26 files changed, 2025 insertions, 245 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java b/server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java
index f644738ddf2..23c31776d68 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java
@@ -59,7 +59,11 @@ public class SecurityServletFilter implements Filter {
// Clickjacking protection
// See https://www.owasp.org/index.php/Clickjacking_Protection_for_Java_EE
- httpResponse.addHeader("X-Frame-Options", "SAMEORIGIN");
+ // The protection is disabled on purpose for integration in external systems like VSTS (/integration/vsts/index.html).
+ String path = httpRequest.getRequestURI().replaceFirst(httpRequest.getContextPath(), "");
+ if (!path.startsWith("/integration/")) {
+ httpResponse.addHeader("X-Frame-Options", "SAMEORIGIN");
+ }
// Cross-site scripting
// See https://www.owasp.org/index.php/List_of_useful_HTTP_headers
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java b/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
index 27632c7c8b6..ab4990322f6 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/web/WebPagesFilter.java
@@ -19,7 +19,12 @@
*/
package org.sonar.server.platform.web;
+import com.google.common.collect.ImmutableSet;
import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
@@ -47,15 +52,24 @@ public class WebPagesFilter implements Filter {
private static final String CACHE_CONTROL_HEADER = "Cache-Control";
private static final String CACHE_CONTROL_VALUE = "no-cache, no-store, must-revalidate";
-
- private static final String CONTEXT_PLACEHOLDER = "%WEB_CONTEXT%";
+ private static final String WEB_CONTEXT_PLACEHOLDER = "%WEB_CONTEXT%";
+ private static final String DEFAULT_HTML_PATH = "/index.html";
+ // all the html files to be loaded from disk
+ private static final Set<String> HTML_PATHS = ImmutableSet.of(DEFAULT_HTML_PATH, "/integration/vsts/index.html");
+ private static final Map<String, String> HTML_CONTENTS_BY_PATH = new HashMap<>();
private static final ServletFilter.UrlPattern URL_PATTERN = ServletFilter.UrlPattern
.builder()
.excludes(staticResourcePatterns())
.build();
- private String indexDotHtml;
+ @Override
+ public void init(FilterConfig filterConfig) {
+ HTML_PATHS.forEach(path -> {
+ ServletContext servletContext = filterConfig.getServletContext();
+ HTML_CONTENTS_BY_PATH.put(path, loadHtmlFile(servletContext, path));
+ });
+ }
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
@@ -69,21 +83,17 @@ public class WebPagesFilter implements Filter {
httpServletResponse.setContentType(HTML);
httpServletResponse.setCharacterEncoding(UTF_8.name().toLowerCase(ENGLISH));
httpServletResponse.setHeader(CACHE_CONTROL_HEADER, CACHE_CONTROL_VALUE);
- write(indexDotHtml, httpServletResponse.getOutputStream(), UTF_8);
- }
-
- @Override
- public void init(FilterConfig filterConfig) {
- String context = filterConfig.getServletContext().getContextPath();
- String indexFile = readIndexFile(filterConfig.getServletContext());
- this.indexDotHtml = indexFile.replaceAll(CONTEXT_PLACEHOLDER, context);
+ String htmlPath = HTML_PATHS.contains(path) ? path : DEFAULT_HTML_PATH;
+ String htmlContent = requireNonNull(HTML_CONTENTS_BY_PATH.get(htmlPath));
+ write(htmlContent, httpServletResponse.getOutputStream(), UTF_8);
}
- private static String readIndexFile(ServletContext servletContext) {
- try {
- return IOUtils.toString(requireNonNull(servletContext.getResource("/index.html")), UTF_8);
+ private static String loadHtmlFile(ServletContext context, String path) {
+ try (InputStream input = context.getResourceAsStream(path)) {
+ String template = IOUtils.toString(requireNonNull(input), UTF_8);
+ return template.replaceAll(WEB_CONTEXT_PLACEHOLDER, context.getContextPath());
} catch (Exception e) {
- throw new IllegalStateException("Fail to provide index file", e);
+ throw new IllegalStateException("Fail to load file " + path, e);
}
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java
index 6c40c7d2279..4faf044eadb 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/platform/web/SecurityServletFilterTest.java
@@ -26,21 +26,19 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.junit.Test;
-import org.sonar.server.platform.web.SecurityServletFilter;
import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.startsWith;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class SecurityServletFilterTest {
- SecurityServletFilter underTest = new SecurityServletFilter();
- HttpServletResponse response = mock(HttpServletResponse.class);
- FilterChain chain = mock(FilterChain.class);
+ private SecurityServletFilter underTest = new SecurityServletFilter();
+ private HttpServletResponse response = mock(HttpServletResponse.class);
+ private FilterChain chain = mock(FilterChain.class);
@Test
public void allow_GET_method() throws IOException, ServletException {
@@ -63,7 +61,7 @@ public class SecurityServletFilterTest {
}
private void assertThatMethodIsAllowed(String httpMethod) throws IOException, ServletException {
- HttpServletRequest request = newRequest(httpMethod);
+ HttpServletRequest request = newRequest(httpMethod, "/");
underTest.doFilter(request, response, chain);
verify(response, never()).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
verify(chain).doFilter(request, response);
@@ -80,25 +78,60 @@ public class SecurityServletFilterTest {
}
private void assertThatMethodIsDenied(String httpMethod) throws IOException, ServletException {
- underTest.doFilter(newRequest(httpMethod), response, chain);
+ underTest.doFilter(newRequest(httpMethod, "/"), response, chain);
verify(response).setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
@Test
- public void set_secured_headers() throws ServletException, IOException {
+ public void set_security_headers() throws Exception {
underTest.init(mock(FilterConfig.class));
- HttpServletRequest request = newRequest("GET");
+ HttpServletRequest request = newRequest("GET", "/");
underTest.doFilter(request, response, chain);
- verify(response, times(3)).addHeader(startsWith("X-"), anyString());
+ verify(response).addHeader("X-Frame-Options", "SAMEORIGIN");
+ verify(response).addHeader("X-XSS-Protection", "1; mode=block");
+ verify(response).addHeader("X-Content-Type-Options", "nosniff");
underTest.destroy();
}
- private HttpServletRequest newRequest(String httpMethod) {
+ @Test
+ public void do_not_set_frame_protection_on_integration_resources() throws Exception {
+ underTest.init(mock(FilterConfig.class));
+ HttpServletRequest request = newRequest("GET", "/integration/vsts/index.html");
+
+ underTest.doFilter(request, response, chain);
+
+ verify(response, never()).addHeader(eq("X-Frame-Options"), anyString());
+ verify(response).addHeader("X-XSS-Protection", "1; mode=block");
+ verify(response).addHeader("X-Content-Type-Options", "nosniff");
+
+ underTest.destroy();
+ }
+
+ @Test
+ public void do_not_set_frame_protection_on_integration_resources_with_context() throws Exception {
+ underTest.init(mock(FilterConfig.class));
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getMethod()).thenReturn("GET");
+ when(request.getRequestURI()).thenReturn("/sonarqube/integration/vsts/index.html");
+ when(request.getContextPath()).thenReturn("/sonarqube");
+
+ underTest.doFilter(request, response, chain);
+
+ verify(response, never()).addHeader(eq("X-Frame-Options"), anyString());
+ verify(response).addHeader("X-XSS-Protection", "1; mode=block");
+ verify(response).addHeader("X-Content-Type-Options", "nosniff");
+
+ underTest.destroy();
+ }
+
+ private static HttpServletRequest newRequest(String httpMethod, String path) {
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getMethod()).thenReturn(httpMethod);
+ when(req.getRequestURI()).thenReturn(path);
+ when(req.getContextPath()).thenReturn("");
return req;
}
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java
index 55080c092cb..75b11a726ad 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/platform/web/WebPagesFilterTest.java
@@ -19,8 +19,7 @@
*/
package org.sonar.server.platform.web;
-import java.io.IOException;
-import java.net.MalformedURLException;
+import java.io.InputStream;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
@@ -28,131 +27,77 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
+import org.mockito.stubbing.Answer;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.RETURNS_MOCKS;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
public class WebPagesFilterTest {
+ private static final String TEST_CONTEXT = "/sonarqube";
@Rule
public ExpectedException expectedException = ExpectedException.none();
- private HttpServletRequest request = mock(HttpServletRequest.class);
- private HttpServletResponse response = mock(HttpServletResponse.class);
- private FilterChain chain = mock(FilterChain.class);
- private ServletContext servletContext = mock(ServletContext.class);
+ private ServletContext servletContext = mock(ServletContext.class, RETURNS_MOCKS);
private FilterConfig filterConfig = mock(FilterConfig.class);
- private StringOutputStream outputStream = new StringOutputStream();
-
private WebPagesFilter underTest = new WebPagesFilter();
@Before
public void setUp() throws Exception {
+ when(servletContext.getContextPath()).thenReturn(TEST_CONTEXT);
+ when(servletContext.getResourceAsStream(anyString())).thenAnswer((Answer<InputStream>) invocationOnMock -> {
+ String path = invocationOnMock.getArgument(0);
+ return IOUtils.toInputStream("Content of " + path + " with context [%WEB_CONTEXT%]", UTF_8);
+ });
when(filterConfig.getServletContext()).thenReturn(servletContext);
- when(response.getOutputStream()).thenReturn(outputStream);
- }
-
- @Test
- public void verify_paths() throws Exception {
- mockIndexFile();
- verifyPathIsHandled("/");
- verifyPathIsHandled("/issues");
- verifyPathIsHandled("/foo");
- }
-
- @Test
- public void return_index_file_content() throws Exception {
- mockIndexFile();
- mockPath("/foo", "");
underTest.init(filterConfig);
- underTest.doFilter(request, response, chain);
-
- assertThat(outputStream.toString()).contains("<head>");
- verify(response).setContentType("text/html");
- verify(response).setCharacterEncoding("utf-8");
- verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
- verify(response).getOutputStream();
- verifyNoMoreInteractions(response);
}
@Test
- public void return_index_file_content_with_default_web_context() throws Exception {
- mockIndexFile();
- mockPath("/foo", "");
- underTest.init(filterConfig);
- underTest.doFilter(request, response, chain);
-
- assertThat(outputStream.toString()).contains("href=\"/sonar.css\"");
- assertThat(outputStream.toString()).contains("<script src=\"/sonar.js\"></script>");
- assertThat(outputStream.toString()).doesNotContain("%WEB_CONTEXT%");
+ public void return_base_index_dot_html_if_not_static_resource() throws Exception {
+ verifyDefaultHtml("/index.html");
+ verifyDefaultHtml("/foo");
+ verifyDefaultHtml("/foo.html");
}
@Test
- public void return_index_file_content_with_web_context() throws Exception {
- mockIndexFile();
- mockPath("/foo", "/web");
- underTest.init(filterConfig);
- underTest.doFilter(request, response, chain);
-
- assertThat(outputStream.toString()).contains("href=\"/web/sonar.css\"");
- assertThat(outputStream.toString()).contains("<script src=\"/web/sonar.js\"></script>");
- assertThat(outputStream.toString()).doesNotContain("%WEB_CONTEXT%");
+ public void return_integration_html() throws Exception {
+ verifyHtml("/integration/vsts/index.html", "Content of /integration/vsts/index.html with context [" + TEST_CONTEXT + "]");
}
- @Test
- public void fail_when_index_is_not_found() throws Exception {
- mockPath("/foo", "");
- when(servletContext.getResource("/index.html")).thenReturn(null);
-
- expectedException.expect(IllegalStateException.class);
- underTest.init(filterConfig);
- }
-
- private void mockIndexFile() throws MalformedURLException {
- when(servletContext.getResource("/index.html")).thenReturn(getClass().getResource("WebPagesFilterTest/index.html"));
+ private void verifyDefaultHtml(String path) throws Exception {
+ verifyHtml(path, "Content of /index.html with context [" + TEST_CONTEXT + "]");
}
- private void mockPath(String path, String context) {
+ private void verifyHtml(String path, String expectedContent) throws Exception {
+ HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getRequestURI()).thenReturn(path);
- when(request.getContextPath()).thenReturn(context);
- when(servletContext.getContextPath()).thenReturn(context);
- }
-
- private void verifyPathIsHandled(String path) throws Exception {
- mockPath(path, "");
- underTest.init(filterConfig);
-
- underTest.doFilter(request, response, chain);
-
- verify(response).getOutputStream();
- verify(response).setContentType(anyString());
- reset(response);
+ when(request.getContextPath()).thenReturn(TEST_CONTEXT);
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ StringOutputStream outputStream = new StringOutputStream();
when(response.getOutputStream()).thenReturn(outputStream);
- }
-
- private void verifyPthIsIgnored(String path) throws Exception {
- mockPath(path, "");
- underTest.init(filterConfig);
+ FilterChain chain = mock(FilterChain.class);
underTest.doFilter(request, response, chain);
- verifyZeroInteractions(response);
- reset(response);
- when(response.getOutputStream()).thenReturn(outputStream);
+ verify(response).setContentType("text/html");
+ verify(response).setCharacterEncoding("utf-8");
+ verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ assertThat(outputStream.toString()).isEqualTo(expectedContent);
}
class StringOutputStream extends ServletOutputStream {
- private StringBuffer buf = new StringBuffer();
+ private final StringBuilder buf = new StringBuilder();
StringOutputStream() {
}
@@ -176,7 +121,7 @@ public class WebPagesFilterTest {
}
public void write(int b) {
- byte[] bytes = new byte[] {(byte) b};
+ byte[] bytes = new byte[]{(byte) b};
this.buf.append(new String(bytes));
}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html b/server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html
deleted file mode 100644
index 379f83f53c7..00000000000
--- a/server/sonar-server/src/test/resources/org/sonar/server/platform/web/WebPagesFilterTest/index.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <link href="%WEB_CONTEXT%/favicon.ico" rel="shortcut icon" type="image/x-icon">
- <link href="%WEB_CONTEXT%/sonar.css" rel="stylesheet">
- <title>SonarQube</title>
-</head>
-<body>
-<div id="content"></div>
-<script>window.baseUrl = '%WEB_CONTEXT%';</script>
-<script src="%WEB_CONTEXT%/sonar.js"></script>
-</body>
-</html>
diff --git a/server/sonar-web/config/utils.js b/server/sonar-web/config/utils.js
new file mode 100644
index 00000000000..e2fef069675
--- /dev/null
+++ b/server/sonar-web/config/utils.js
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * 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
+ * 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.
+ */
+const cssMinimizeOptions = {
+ discardComments: { removeAll: true }
+};
+
+const cssLoader = ({ production }) => ({
+ loader: 'css-loader',
+ options: {
+ importLoaders: 1,
+ minimize: production && cssMinimizeOptions,
+ url: false
+ }
+});
+
+const postcssLoader = () => ({
+ loader: 'postcss-loader',
+ options: {
+ ident: 'postcss',
+ plugins: () => [
+ require('autoprefixer'),
+ require('postcss-custom-properties')({
+ variables: require('../src/main/js/app/theme')
+ }),
+ require('postcss-calc')
+ ]
+ }
+});
+
+const minifyParams = ({ production }) =>
+ production && {
+ removeComments: true,
+ collapseWhitespace: true,
+ removeRedundantAttributes: true,
+ useShortDoctype: true,
+ removeEmptyAttributes: true,
+ removeStyleLinkTypeAttributes: true,
+ keepClosingSlash: true,
+ minifyJS: true,
+ minifyCSS: true,
+ minifyURLs: true
+ };
+
+module.exports = {
+ cssLoader,
+ postcssLoader,
+ minifyParams
+};
diff --git a/server/sonar-web/config/vsts.webpack.config.js b/server/sonar-web/config/vsts.webpack.config.js
new file mode 100644
index 00000000000..dfab0102c08
--- /dev/null
+++ b/server/sonar-web/config/vsts.webpack.config.js
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+/* eslint-disable import/no-extraneous-dependencies */
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
+const webpack = require('webpack');
+const paths = require('./paths');
+const utils = require('./utils');
+
+module.exports = ({ production = true, fast = false }) => ({
+ bail: production,
+
+ devtool: production ? (fast ? false : 'source-map') : 'cheap-module-source-map',
+ resolve: {
+ // Add '.ts' and '.tsx' as resolvable extensions.
+ extensions: ['.ts', '.tsx', '.js', '.json']
+ },
+ entry: {
+ vsts: [
+ !production && require.resolve('react-dev-utils/webpackHotDevClient'),
+ !production && require.resolve('react-error-overlay'),
+ 'react',
+ 'react-dom',
+ './src/main/js/app/integration/vsts/index.js'
+ ].filter(Boolean)
+ },
+ output: {
+ path: paths.vstsBuild,
+ pathinfo: !production,
+ publicPath: '/integration/vsts/',
+ filename: production ? 'js/[name].[chunkhash:8].js' : 'js/[name].js',
+ chunkFilename: production ? 'js/[name].[chunkhash:8].chunk.js' : 'js/[name].chunk.js'
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ loader: 'babel-loader',
+ exclude: /(node_modules|libs)/
+ },
+ {
+ test: /\.tsx?$/,
+ use: [
+ {
+ loader: 'awesome-typescript-loader',
+ options: {
+ transpileOnly: true,
+ useBabel: true,
+ useCache: true
+ }
+ }
+ ]
+ },
+ {
+ test: /\.css$/,
+ use: ['style-loader', utils.cssLoader({ production, fast }), utils.postcssLoader()]
+ }
+ ].filter(Boolean)
+ },
+ plugins: [
+ !production && new InterpolateHtmlPlugin({ WEB_CONTEXT: '' }),
+
+ new HtmlWebpackPlugin({
+ inject: false,
+ template: paths.vstsHtml,
+ minify: utils.minifyParams({ production, fast })
+ }),
+
+ new webpack.DefinePlugin({
+ 'process.env.NODE_ENV': JSON.stringify(production ? 'production' : 'development')
+ }),
+
+ new CopyWebpackPlugin([
+ {
+ from: './src/main/js/libs/third-party/VSS.SDK.min.js',
+ to: 'js/'
+ }
+ ]),
+
+ production &&
+ !fast &&
+ new webpack.optimize.UglifyJsPlugin({
+ sourceMap: true,
+ compress: { screw_ie8: true, warnings: false },
+ mangle: { screw_ie8: true },
+ output: { comments: false, screw_ie8: true }
+ }),
+
+ !production && new webpack.HotModuleReplacementPlugin()
+ ].filter(Boolean)
+});
diff --git a/server/sonar-web/config/webpack.config.js b/server/sonar-web/config/webpack.config.js
index 3461d359ecb..04139c2acf0 100644
--- a/server/sonar-web/config/webpack.config.js
+++ b/server/sonar-web/config/webpack.config.js
@@ -26,33 +26,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const InterpolateHtmlPlugin = require('./InterpolateHtmlPlugin');
const paths = require('./paths');
-
-const cssMinimizeOptions = {
- discardComments: { removeAll: true }
-};
-
-const cssLoader = ({ production }) => ({
- loader: 'css-loader',
- options: {
- importLoaders: 1,
- minimize: production && cssMinimizeOptions,
- url: false
- }
-});
-
-const postcssLoader = () => ({
- loader: 'postcss-loader',
- options: {
- ident: 'postcss',
- plugins: () => [
- require('autoprefixer'),
- require('postcss-custom-properties')({
- variables: require('../src/main/js/app/theme')
- }),
- require('postcss-calc')
- ]
- }
-});
+const utils = require('./utils');
module.exports = ({ production = true }) => ({
mode: production ? 'production' : 'development',
@@ -122,18 +96,7 @@ module.exports = ({ production = true }) => ({
new HtmlWebpackPlugin({
inject: false,
template: paths.appHtml,
- minify: production && {
- removeComments: true,
- collapseWhitespace: true,
- removeRedundantAttributes: true,
- useShortDoctype: true,
- removeEmptyAttributes: true,
- removeStyleLinkTypeAttributes: true,
- keepClosingSlash: true,
- minifyJS: true,
- minifyCSS: true,
- minifyURLs: true
- }
+ minify: utils.minifyParams({ production })
}),
// keep `InterpolateHtmlPlugin` after `HtmlWebpackPlugin`
diff --git a/server/sonar-web/package.json b/server/sonar-web/package.json
index 0bc51ddff1b..7bfa1b7d2dc 100644
--- a/server/sonar-web/package.json
+++ b/server/sonar-web/package.json
@@ -106,7 +106,13 @@
},
"scripts": {
"start": "node scripts/start.js",
- "build": "node scripts/build.js",
+ "start-vsts": "node scripts/vsts.start.js",
+ "build": "yarn build-core && yarn build-vsts",
+ "build-fast": "yarn build-core-fast && yarn build-vsts-fast",
+ "build-core": "node scripts/build.js",
+ "build-core-fast": "node scripts/build.js --fast",
+ "build-vsts": "node scripts/vsts.build.js",
+ "build-vsts-fast": "node scripts/vsts.build.js --fast",
"test": "node scripts/test.js",
"coverage": "npm test -- --coverage",
"format": "prettier --write --list-different 'src/main/js/!(libs)/**/*.{js,ts,tsx,css}'",
diff --git a/server/sonar-web/public/integration/vsts/index.html b/server/sonar-web/public/integration/vsts/index.html
new file mode 100644
index 00000000000..90f0799d8ad
--- /dev/null
+++ b/server/sonar-web/public/integration/vsts/index.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8" charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+
+ <meta name="application-name" content="SonarQube VSTS Widget" />
+
+ <script src="%WEB_CONTEXT%/integration/vsts/js/VSS.SDK.min.js"></script>
+ <title>SonarQube VSTS Widget</title>
+</head>
+
+<body>
+ <div id="content">
+ <div class="vsts-loading">
+ <i class="spinner global-loading-spinner"></i>
+ </div>
+ </div>
+ <script>window.baseUrl = '%WEB_CONTEXT%';</script>
+ <% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
+ <script src="%WEB_CONTEXT%<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
+ <% } %>
+</body>
+
+</html>
diff --git a/server/sonar-web/scripts/start.js b/server/sonar-web/scripts/start.js
index 53969869f33..96ceaa8ed9d 100644
--- a/server/sonar-web/scripts/start.js
+++ b/server/sonar-web/scripts/start.js
@@ -108,7 +108,8 @@ function runDevServer(compiler, host, port, protocol) {
'/api': proxy,
'/fonts': proxy,
'/images': proxy,
- '/static': proxy
+ '/static': proxy,
+ '/integration': proxy
}
});
diff --git a/server/sonar-web/scripts/vsts.build.js b/server/sonar-web/scripts/vsts.build.js
new file mode 100644
index 00000000000..45a7a8f1f79
--- /dev/null
+++ b/server/sonar-web/scripts/vsts.build.js
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * 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
+ * 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.
+ */
+/* eslint-disable no-console*/
+process.env.NODE_ENV = 'production';
+
+const chalk = require('chalk');
+const fs = require('fs-extra');
+const rimrafSync = require('rimraf').sync;
+const webpack = require('webpack');
+const paths = require('../config/paths');
+const formatSize = require('./utils/formatSize');
+const getConfig = require('../config/vsts.webpack.config');
+
+const fast = process.argv.some(arg => arg.indexOf('--fast') > -1);
+
+const config = getConfig({ fast, production: true });
+
+function clean() {
+ // Remove all content but keep the directory so that
+ // if you're in it, you don't end up in Trash
+ console.log(chalk.cyan.bold('Cleaning output directories and files...'));
+
+ console.log(paths.vstsBuild + '/*');
+ rimrafSync(paths.vstsBuild + '/*');
+
+ console.log();
+}
+
+function build() {
+ if (fast) {
+ console.log(chalk.magenta.bold('Running fast build...'));
+ } else {
+ console.log(chalk.cyan.bold('Creating optimized production build...'));
+ }
+ console.log();
+
+ webpack(config, (err, stats) => {
+ if (err) {
+ console.log(chalk.red.bold('Failed to create a production build!'));
+ console.log(chalk.red(err.message || err));
+ process.exit(1);
+ }
+
+ if (stats.compilation.errors && stats.compilation.errors.length) {
+ console.log(chalk.red.bold('Failed to create a production build!'));
+ stats.compilation.errors.forEach(err => console.log(chalk.red(err.message || err)));
+ process.exit(1);
+ }
+
+ const jsonStats = stats.toJson();
+
+ console.log('Assets:');
+ const assets = jsonStats.assets.slice();
+ assets.sort((a, b) => b.size - a.size);
+ assets.forEach(asset => {
+ let sizeLabel = formatSize(asset.size);
+ const leftPadding = ' '.repeat(Math.max(0, 8 - sizeLabel.length));
+ sizeLabel = leftPadding + sizeLabel;
+ console.log('', chalk.yellow(sizeLabel), asset.name);
+ });
+ console.log();
+
+ const seconds = jsonStats.time / 1000;
+ console.log('Duration: ' + seconds.toFixed(2) + 's');
+ console.log();
+
+ console.log(chalk.green.bold('Compiled successfully!'));
+ });
+}
+
+function copyPublicFolder() {
+ fs.copySync(paths.appPublic, paths.appBuild, {
+ dereference: true,
+ filter: file => file !== paths.appHtml
+ });
+}
+
+clean();
+build();
+copyPublicFolder();
diff --git a/server/sonar-web/scripts/vsts.start.js b/server/sonar-web/scripts/vsts.start.js
new file mode 100644
index 00000000000..a228c3d4a44
--- /dev/null
+++ b/server/sonar-web/scripts/vsts.start.js
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * 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
+ * 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.
+ */
+/* eslint-disable no-console */
+process.env.NODE_ENV = 'development';
+
+const chalk = require('chalk');
+const webpack = require('webpack');
+const WebpackDevServer = require('webpack-dev-server');
+const clearConsole = require('react-dev-utils/clearConsole');
+const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
+const errorOverlayMiddleware = require('react-error-overlay/middleware');
+const getConfig = require('../config/vsts.webpack.config');
+const paths = require('../config/paths');
+
+const config = getConfig({ production: false });
+
+const port = process.env.PORT || 3000;
+const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
+const host = process.env.HOST || 'localhost';
+const proxy = process.env.PROXY || 'http://localhost:9000';
+
+const compiler = setupCompiler(host, port, protocol);
+
+runDevServer(compiler, host, port, protocol);
+
+function setupCompiler(host, port, protocol) {
+ const compiler = webpack(config);
+
+ compiler.plugin('invalid', () => {
+ clearConsole();
+ console.log('Compiling...');
+ });
+
+ compiler.plugin('done', stats => {
+ clearConsole();
+
+ const jsonStats = stats.toJson({}, true);
+ const messages = formatWebpackMessages(jsonStats);
+ const seconds = jsonStats.time / 1000;
+ if (!messages.errors.length && !messages.warnings.length) {
+ console.log(chalk.green('Compiled successfully!'));
+ console.log('Duration: ' + seconds.toFixed(2) + 's');
+ console.log();
+ console.log('The app is running at:');
+ console.log();
+ console.log(' ' + chalk.cyan(protocol + '://' + host + ':' + port + '/'));
+ console.log();
+ }
+
+ if (messages.errors.length) {
+ console.log(chalk.red('Failed to compile.'));
+ console.log();
+ messages.errors.forEach(message => {
+ console.log(message);
+ console.log();
+ });
+ }
+ });
+
+ return compiler;
+}
+
+function runDevServer(compiler, host, port, protocol) {
+ const devServer = new WebpackDevServer(compiler, {
+ before(app) {
+ app.use(errorOverlayMiddleware());
+ },
+ compress: true,
+ clientLogLevel: 'none',
+ contentBase: paths.appPublic,
+ disableHostCheck: true,
+ hot: true,
+ publicPath: config.output.publicPath,
+ quiet: true,
+ watchOptions: {
+ ignored: /node_modules/
+ },
+ https: protocol === 'https',
+ host,
+ overlay: false,
+ proxy: {
+ '/': proxy
+ }
+ });
+
+ devServer.listen(port, err => {
+ if (err) {
+ console.log(err);
+ return;
+ }
+
+ clearConsole();
+ console.log(chalk.cyan('Starting the development server...'));
+ console.log();
+ });
+}
diff --git a/server/sonar-web/src/main/js/api/measures.ts b/server/sonar-web/src/main/js/api/measures.ts
index 40d219fcfa3..b5d1e649525 100644
--- a/server/sonar-web/src/main/js/api/measures.ts
+++ b/server/sonar-web/src/main/js/api/measures.ts
@@ -29,7 +29,7 @@ export function getMeasures(
return getJSON('/api/measures/component', data).then(r => r.component.measures, throwGlobalError);
}
-interface MeasureComponent {
+export interface MeasureComponent {
key: string;
description?: string;
measures: Measure[];
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/Configuration.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/Configuration.tsx
new file mode 100644
index 00000000000..838114c4cbd
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/components/Configuration.tsx
@@ -0,0 +1,147 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { searchProjects } from '../../../../api/components';
+
+interface Settings {
+ project: string;
+}
+
+interface Props {
+ widgetHelpers: any;
+}
+
+interface State {
+ loading: boolean;
+ organizations?: Array<{ key: string; name: string }>;
+ projects?: Array<{ label: string; value: string }>;
+ settings: Settings;
+ widgetConfigurationContext?: any;
+}
+
+declare const VSS: any;
+
+export default class Configuration extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: true, settings: { project: '' } };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.props.widgetHelpers.IncludeWidgetConfigurationStyles();
+ VSS.register('e56c6ff0-c6f9-43d0-bdef-b3f1aa0dc6dd', () => {
+ return { load: this.load, onSave: this.onSave };
+ });
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ load = (widgetSettings: any, widgetConfigurationContext: any) => {
+ const settings: Settings = JSON.parse(widgetSettings.customSettings.data);
+ if (this.mounted) {
+ this.setState({ settings: settings || {}, widgetConfigurationContext });
+ this.fetchProjects();
+ }
+ return this.props.widgetHelpers.WidgetStatusHelper.Success();
+ };
+
+ onSave = () => {
+ if (!this.state.settings || !this.state.settings.project) {
+ return this.props.widgetHelpers.WidgetConfigurationSave.Invalid();
+ }
+ return this.props.widgetHelpers.WidgetConfigurationSave.Valid({
+ data: JSON.stringify(this.state.settings)
+ });
+ };
+
+ fetchProjects = (organization?: string) => {
+ this.setState({ loading: true });
+ searchProjects({ organization, ps: 100 }).then(
+ ({ components }) => {
+ if (this.mounted) {
+ this.setState({
+ projects: components.map(c => ({ label: c.name, value: c.key })),
+ loading: false
+ });
+ }
+ },
+ () => {
+ this.setState({
+ projects: [],
+ loading: false
+ });
+ }
+ );
+ };
+
+ handleProjectChange = (
+ event: React.ChangeEvent<HTMLSelectElement> | React.FocusEvent<HTMLSelectElement>
+ ) => {
+ const { value } = event.currentTarget;
+ this.setState(
+ ({ settings }) => ({ settings: { ...settings, project: value } }),
+ this.notifyChange
+ );
+ };
+
+ notifyChange = ({ settings, widgetConfigurationContext } = this.state) => {
+ const { widgetHelpers } = this.props;
+ if (widgetConfigurationContext && widgetConfigurationContext.notify) {
+ const eventName = widgetHelpers.WidgetEvent.ConfigurationChange;
+ const eventArgs = widgetHelpers.WidgetEvent.Args({ data: JSON.stringify(settings) });
+ widgetConfigurationContext.notify(eventName, eventArgs);
+ }
+ };
+
+ render() {
+ const { projects, loading, settings } = this.state;
+ if (loading) {
+ return (
+ <div className="vsts-loading">
+ <i className="spinner global-loading-spinner" />
+ </div>
+ );
+ }
+ return (
+ <div className="widget-configuration">
+ <div className="dropdown" id="project">
+ <label>SonarCloud project</label>
+ <div className="wrapper">
+ <select
+ onBlur={this.handleProjectChange}
+ onChange={this.handleProjectChange}
+ value={settings.project}>
+ <option disabled={true} hidden={true} value="">
+ Select a project...
+ </option>
+ {projects &&
+ projects.map(project => (
+ <option key={project.value} value={project.value}>
+ {project.label}
+ </option>
+ ))}
+ </select>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/QGWidget.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/QGWidget.tsx
new file mode 100644
index 00000000000..d7e3910f9e1
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/components/QGWidget.tsx
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as classNames from 'classnames';
+import { MeasureComponent } from '../../../../api/measures';
+import { Metric } from '../../../types';
+import { getPathUrlAsString, getProjectUrl } from '../../../../helpers/urls';
+
+interface Props {
+ component: MeasureComponent;
+ metrics: Metric[];
+}
+
+const QG_LEVELS: { [level: string]: string } = {
+ ERROR: 'Failed',
+ WARN: 'Warning',
+ OK: 'Passed',
+ NONE: 'None'
+};
+
+export default function QGWidget({ component, metrics }: Props) {
+ const qgMetric = metrics && metrics.find(m => m.key === 'alert_status');
+ const qgMeasure = component && component.measures.find(m => m.metric === 'alert_status');
+
+ if (!qgMeasure || !qgMeasure.value) {
+ return <p>Project Quality Gate not computed.</p>;
+ }
+
+ return (
+ <div className={classNames('widget dark-widget clickable', 'level-' + qgMeasure.value)}>
+ <a href={getPathUrlAsString(getProjectUrl(component.key))} target="_blank">
+ <h2 className="title truncated-text-ellipsis">{component.name}</h2>
+ <div className="big-value truncated-text-ellipsis">{QG_LEVELS[qgMeasure.value]}</div>
+ <div className="footer truncated-text-ellipsis">
+ {qgMetric ? qgMetric.name : 'Quality Gate'}
+ </div>
+ </a>
+ </div>
+ );
+}
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/Widget.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/Widget.tsx
new file mode 100644
index 00000000000..98ac6c85aa7
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/components/Widget.tsx
@@ -0,0 +1,106 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 QGWidget from './QGWidget';
+import { getMeasuresAndMeta, MeasureComponent } from '../../../../api/measures';
+import { Metric } from '../../../types';
+
+interface Props {
+ widgetHelpers: any;
+}
+
+interface State {
+ component?: MeasureComponent;
+ loading: boolean;
+ metrics?: Metric[];
+}
+
+declare const VSS: any;
+
+export default class Widget extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: true };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.props.widgetHelpers.IncludeWidgetStyles();
+ VSS.register('3c598f25-01c1-4c09-97c6-926476882688', () => {
+ return { load: this.load, reload: this.load };
+ });
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ load = (widgetSettings: any) => {
+ const settings = JSON.parse(widgetSettings.customSettings.data);
+ if (this.mounted) {
+ if (settings && settings.project) {
+ this.fetchProjectMeasures(settings.project);
+ } else {
+ this.setState({ loading: false });
+ }
+ }
+ return this.props.widgetHelpers.WidgetStatusHelper.Success();
+ };
+
+ fetchProjectMeasures = (project: string) => {
+ this.setState({ loading: true });
+ getMeasuresAndMeta(project, ['alert_status'], { additionalFields: 'metrics' }).then(
+ ({ component, metrics }) => {
+ if (this.mounted) {
+ this.setState({ component, loading: false, metrics });
+ }
+ },
+ () => {
+ this.setState({ loading: false });
+ }
+ );
+ };
+
+ render() {
+ const { component, loading, metrics } = this.state;
+ if (loading) {
+ return (
+ <div className="vsts-loading">
+ <i className="spinner global-loading-spinner" />
+ </div>
+ );
+ }
+
+ if (!component || !metrics) {
+ return (
+ <div className="vsts-widget-configure widget">
+ <h2 className="title">Quality Widget</h2>
+ <div className="content">
+ <div>Configure widget</div>
+ <img
+ alt=""
+ src="https://cdn.vsassets.io/v/20180301T143409/_content/Dashboards/unconfigured-small.png"
+ />
+ </div>
+ </div>
+ );
+ }
+
+ return <QGWidget component={component} metrics={metrics} />;
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/index.js b/server/sonar-web/src/main/js/app/integration/vsts/index.js
new file mode 100644
index 00000000000..433334447b6
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/index.js
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 { parse } from 'querystring';
+import React from 'react';
+import { render } from 'react-dom';
+import Configuration from './components/Configuration';
+import Widget from './components/Widget';
+import './vsts.css';
+
+VSS.init({
+ explicitNotifyLoaded: true,
+ usePlatformStyles: true
+});
+
+VSS.require('TFS/Dashboards/WidgetHelpers', widgetHelpers => {
+ const container = document.getElementById('content');
+ const query = parse(window.location.search.replace('?', ''));
+
+ if (query.type === 'configuration') {
+ render(<Configuration widgetHelpers={widgetHelpers} />, container);
+ } else {
+ render(<Widget widgetHelpers={widgetHelpers} />, container);
+ }
+ VSS.notifyLoadSucceeded();
+});
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/vsts.css b/server/sonar-web/src/main/js/app/integration/vsts/vsts.css
new file mode 100644
index 00000000000..8c0b5a62242
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/vsts.css
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 '../../styles/components/spinner.css';
+@import '../../styles/components/global-loading.css';
+
+#content {
+ height: 100%;
+}
+
+.vsts-loading {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.vsts-widget-configure {
+ display: block;
+ position: relative;
+ width: 100%;
+ height: 100%;
+ padding: 10px 14px;
+ font-size: 16px;
+}
+
+.vsts-widget-configure .title {
+ color: #333;
+ font-weight: normal;
+}
+
+.vsts-widget-configure .content {
+ padding-top: 10%;
+ text-align: center;
+ color: #666;
+}
+
+.vsts-widget-configure img {
+ height: 40px;
+ margin-top: 10px;
+}
+
+.widget.dark-widget.clickable > a {
+ color: white;
+}
+
+.big-value {
+ font-size: 36px;
+ line-height: 68px;
+ margin: 20px 0 10px 0;
+ font-weight: 300;
+}
+
+.level-OK {
+ background-color: var(--green);
+}
+
+.level-WARN {
+ background-color: var(--orange);
+}
+
+.level-ERROR {
+ background-color: var(--red);
+}
+
+.level-NONE {
+ background-color: var(--gray71);
+}
+
+.Select {
+ width: 100%;
+}
diff --git a/server/sonar-web/src/main/js/app/styles/components/spinner.css b/server/sonar-web/src/main/js/app/styles/components/spinner.css
new file mode 100644
index 00000000000..4482584983f
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/styles/components/spinner.css
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+.spinner {
+ position: relative;
+ vertical-align: middle;
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--blue);
+ border-radius: 50%;
+ animation: spin 0.75s infinite linear;
+}
+
+.spinner-placeholder {
+ position: relative;
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ height: 16px;
+ visibility: hidden;
+}
+
+.spinner:before,
+.spinner:after {
+ left: -2px;
+ top: -2px;
+ display: none;
+ position: absolute;
+ content: '';
+ width: inherit;
+ height: inherit;
+ border: inherit;
+ border-radius: inherit;
+}
+
+.spinner,
+.spinner:before,
+.spinner:after {
+ display: inline-block;
+ box-sizing: border-box;
+ border-color: transparent;
+ border-top-color: var(--blue);
+ animation-duration: 1.2s;
+}
+
+.spinner:before {
+ transform: rotate(120deg);
+}
+
+.spinner:after {
+ transform: rotate(240deg);
+}
+
+.spinner-margin {
+ margin: 10px;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/styles/init/icons.css b/server/sonar-web/src/main/js/app/styles/init/icons.css
index b6c9cdb078b..25b007c1ed6 100644
--- a/server/sonar-web/src/main/js/app/styles/init/icons.css
+++ b/server/sonar-web/src/main/js/app/styles/init/icons.css
@@ -783,70 +783,3 @@ a:hover > .icon-radio {
background-image: url(data:image/svg+xml,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cpath%20d%3D%22M2.977%2012.656c0%20.417-.142.745-.426.985-.283.24-.636.36-1.058.36-.552%200-1-.172-1.344-.516l.446-.687c.255.234.53.35.828.35.15%200%20.282-.036.394-.112.112-.075.168-.186.168-.332%200-.333-.273-.48-.82-.437l-.203-.438c.043-.052.127-.165.255-.34.127-.174.238-.315.332-.422.094-.106.19-.207.29-.3v-.008c-.084%200-.21.002-.38.008-.17.005-.296.007-.38.007v.415H.25V10h2.602v.688l-.743.898c.265.062.476.19.632.383.156.19.235.42.235.686zm.015-4.898V9H.164c-.03-.188-.047-.328-.047-.422%200-.265.06-.508.184-.726.123-.22.27-.396.442-.532.172-.135.344-.26.516-.37.172-.113.32-.226.44-.34.124-.115.185-.232.185-.352%200-.13-.038-.23-.113-.3-.076-.07-.18-.106-.31-.106-.24%200-.45.15-.632.453l-.664-.46c.125-.267.31-.474.56-.622.246-.15.52-.223.823-.223.38%200%20.7.108.96.324.26.216.39.51.39.88%200%20.26-.087.498-.264.714-.177.216-.373.384-.586.504-.214.12-.41.25-.59.394-.18.144-.272.28-.277.41h.992V7.76h.82zM14%2010.25v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074-.05-.05-.074-.108-.074-.176v-1.5c0-.073.023-.133.07-.18.047-.047.107-.07.18-.07h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176zM3%203.227V4H.383v-.773h.836c0-.214%200-.532.003-.954l.004-.945v-.094H1.21c-.04.09-.17.23-.39.422l-.554-.593L1.328.07h.828v3.157H3zM14%206.25v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074C4.024%207.876%204%207.818%204%207.75v-1.5c0-.073.023-.133.07-.18.047-.047.107-.07.18-.07h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176zm0-4v1.5c0%20.068-.025.126-.074.176-.05.05-.108.074-.176.074h-9.5c-.068%200-.126-.025-.176-.074C4.024%203.876%204%203.818%204%203.75v-1.5c0-.068.025-.126.074-.176.05-.05.108-.074.176-.074h9.5c.068%200%20.126.025.176.074.05.05.074.108.074.176z%22%20fill%3D%22%23236A97%22%20fill-rule%3D%22nonzero%22%2F%3E%3C%2Fsvg%3E);
background-repeat: no-repeat;
}
-
-/*
- * Spinner
- */
-.spinner {
- position: relative;
- vertical-align: middle;
- width: 16px;
- height: 16px;
- border: 2px solid var(--blue);
- border-radius: 50%;
- animation: spin 0.75s infinite linear;
-}
-
-.spinner-placeholder {
- position: relative;
- display: inline-block;
- vertical-align: middle;
- width: 16px;
- height: 16px;
- visibility: hidden;
-}
-
-.spinner:before,
-.spinner:after {
- left: -2px;
- top: -2px;
- display: none;
- position: absolute;
- content: '';
- width: inherit;
- height: inherit;
- border: inherit;
- border-radius: inherit;
-}
-
-.spinner,
-.spinner:before,
-.spinner:after {
- display: inline-block;
- box-sizing: border-box;
- border-color: transparent;
- border-top-color: var(--blue);
- animation-duration: 1.2s;
-}
-
-.spinner:before {
- transform: rotate(120deg);
-}
-
-.spinner:after {
- transform: rotate(240deg);
-}
-
-.spinner-margin {
- margin: 10px;
-}
-
-@keyframes spin {
- from {
- transform: rotate(0deg);
- }
-
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/server/sonar-web/src/main/js/app/styles/sonar.css b/server/sonar-web/src/main/js/app/styles/sonar.css
index 714ed9d74b2..5b6284ebeed 100644
--- a/server/sonar-web/src/main/js/app/styles/sonar.css
+++ b/server/sonar-web/src/main/js/app/styles/sonar.css
@@ -27,6 +27,7 @@
@import './init/misc.css';
@import './components/ui.css';
+@import './components/spinner.css';
@import './components/global-loading.css';
@import './components/bubble-popup.css';
@import './components/modals.css';
diff --git a/server/sonar-web/src/main/js/libs/third-party/VSS.SDK.min.js b/server/sonar-web/src/main/js/libs/third-party/VSS.SDK.min.js
new file mode 100644
index 00000000000..5f6a76663bd
--- /dev/null
+++ b/server/sonar-web/src/main/js/libs/third-party/VSS.SDK.min.js
@@ -0,0 +1,957 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+// Copyright (C) Microsoft Corporation. All rights reserved.
+var XDM, VSS;
+(function(n) {
+ function u() {
+ return new o();
+ }
+ function s() {
+ return (
+ Math.floor(Math.random() * (f - t) + t).toString(36) +
+ Math.floor(Math.random() * (f - t) + t).toString(36)
+ );
+ }
+ var i, r, e;
+ n.createDeferred = u;
+ var o = (function() {
+ function n() {
+ var n = this;
+ this._resolveCallbacks = [];
+ this._rejectCallbacks = [];
+ this._isResolved = !1;
+ this._isRejected = !1;
+ this.resolve = function(t) {
+ n._resolve(t);
+ };
+ this.reject = function(t) {
+ n._reject(t);
+ };
+ this.promise = {};
+ this.promise.then = function(t, i) {
+ return n._then(t, i);
+ };
+ }
+ return (
+ (n.prototype._then = function(t, i) {
+ var u = this,
+ r;
+ return (!t && !i) || (this._isResolved && !t) || (this._isRejected && !i)
+ ? this.promise
+ : ((r = new n()),
+ this._resolveCallbacks.push(function(n) {
+ u._wrapCallback(t, n, r, !1);
+ }),
+ this._rejectCallbacks.push(function(n) {
+ u._wrapCallback(i, n, r, !0);
+ }),
+ this._isResolved
+ ? this._resolve(this._resolvedValue)
+ : this._isRejected && this._reject(this._rejectValue),
+ r.promise);
+ }),
+ (n.prototype._wrapCallback = function(n, t, i, r) {
+ if (!n) {
+ r ? i.reject(t) : i.resolve(t);
+ return;
+ }
+ var u;
+ try {
+ u = n(t);
+ } catch (f) {
+ i.reject(f);
+ return;
+ }
+ u === undefined
+ ? i.resolve(t)
+ : u && typeof u.then == 'function'
+ ? u.then(
+ function(n) {
+ i.resolve(n);
+ },
+ function(n) {
+ i.reject(n);
+ }
+ )
+ : i.resolve(u);
+ }),
+ (n.prototype._resolve = function(n) {
+ if (
+ (this._isRejected ||
+ this._isResolved ||
+ ((this._isResolved = !0), (this._resolvedValue = n)),
+ this._isResolved && this._resolveCallbacks.length > 0)
+ ) {
+ var t = this._resolveCallbacks.splice(0);
+ window.setTimeout(function() {
+ for (var i = 0, r = t.length; i < r; i++) t[i](n);
+ });
+ }
+ }),
+ (n.prototype._reject = function(n) {
+ if (
+ (this._isRejected ||
+ this._isResolved ||
+ ((this._isRejected = !0),
+ (this._rejectValue = n),
+ this._rejectCallbacks.length === 0 &&
+ window.console &&
+ window.console.warn &&
+ (console.warn('Rejected XDM promise with no reject callbacks'),
+ n && console.warn(n))),
+ this._isRejected && this._rejectCallbacks.length > 0)
+ ) {
+ var t = this._rejectCallbacks.splice(0);
+ window.setTimeout(function() {
+ for (var i = 0, r = t.length; i < r; i++) t[i](n);
+ });
+ }
+ }),
+ n
+ );
+ })(),
+ t = parseInt('10000000000', 36),
+ f = Number.MAX_SAFE_INTEGER || 9007199254740991;
+ i = (function() {
+ function n() {
+ this._registeredObjects = {};
+ }
+ return (
+ (n.prototype.register = function(n, t) {
+ this._registeredObjects[n] = t;
+ }),
+ (n.prototype.unregister = function(n) {
+ delete this._registeredObjects[n];
+ }),
+ (n.prototype.getInstance = function(n, t) {
+ var i = this._registeredObjects[n];
+ return i ? (typeof i == 'function' ? i(t) : i) : null;
+ }),
+ n
+ );
+ })();
+ n.XDMObjectRegistry = i;
+ n.globalObjectRegistry = new i();
+ r = (function() {
+ function t(n, r) {
+ r === void 0 && (r = null);
+ this._nextMessageId = 1;
+ this._deferreds = {};
+ this._nextProxyFunctionId = 1;
+ this._proxyFunctions = {};
+ this._postToWindow = n;
+ this._targetOrigin = r;
+ this._channelObjectRegistry = new i();
+ this._channelId = t._nextChannelId++;
+ this._targetOrigin || (this._handshakeToken = s());
+ }
+ return (
+ (t.prototype.getObjectRegistry = function() {
+ return this._channelObjectRegistry;
+ }),
+ (t.prototype.invokeRemoteMethod = function(n, t, i, r, f) {
+ var e = {
+ id: this._nextMessageId++,
+ methodName: n,
+ instanceId: t,
+ instanceContext: r,
+ params: this._customSerializeObject(i, f),
+ jsonrpc: '2.0',
+ serializationSettings: f
+ },
+ o;
+ return (
+ this._targetOrigin || (e.handshakeToken = this._handshakeToken),
+ (o = u()),
+ (this._deferreds[e.id] = o),
+ this._sendRpcMessage(e),
+ o.promise
+ );
+ }),
+ (t.prototype.getRemoteObjectProxy = function(n, t) {
+ return this.invokeRemoteMethod(null, n, null, t);
+ }),
+ (t.prototype.invokeMethod = function(n, t) {
+ var f = this,
+ r,
+ u,
+ i;
+ if (!t.methodName) {
+ this._success(t, n, t.handshakeToken);
+ return;
+ }
+ if (((r = n[t.methodName]), typeof r != 'function')) {
+ this._error(t, new Error('RPC method not found: ' + t.methodName), t.handshakeToken);
+ return;
+ }
+ try {
+ u = [];
+ t.params && (u = this._customDeserializeObject(t.params));
+ i = r.apply(n, u);
+ i && i.then && typeof i.then == 'function'
+ ? i.then(
+ function(n) {
+ f._success(t, n, t.handshakeToken);
+ },
+ function(n) {
+ f._error(t, n, t.handshakeToken);
+ }
+ )
+ : this._success(t, i, t.handshakeToken);
+ } catch (e) {
+ this._error(t, e, t.handshakeToken);
+ }
+ }),
+ (t.prototype.getRegisteredObject = function(t, i) {
+ if (t === '__proxyFunctions') return this._proxyFunctions;
+ var r = this._channelObjectRegistry.getInstance(t, i);
+ return r || (r = n.globalObjectRegistry.getInstance(t, i)), r;
+ }),
+ (t.prototype.onMessage = function(n) {
+ var u = this,
+ t = n,
+ i,
+ r;
+ if (t.instanceId) {
+ if (((i = this.getRegisteredObject(t.instanceId, t.instanceContext)), !i)) return !1;
+ typeof i.then == 'function'
+ ? i.then(
+ function(n) {
+ u.invokeMethod(n, t);
+ },
+ function(n) {
+ u._error(t, n, t.handshakeToken);
+ }
+ )
+ : this.invokeMethod(i, t);
+ } else {
+ if (((r = this._deferreds[t.id]), !r)) return !1;
+ t.error
+ ? r.reject(this._customDeserializeObject([t.error])[0])
+ : r.resolve(this._customDeserializeObject([t.result])[0]);
+ delete this._deferreds[t.id];
+ }
+ return !0;
+ }),
+ (t.prototype.owns = function(n, t, i) {
+ var r = i;
+ if (this._postToWindow === n) {
+ if (this._targetOrigin)
+ return t
+ ? t.toLowerCase() === 'null' ||
+ this._targetOrigin.toLowerCase().indexOf(t.toLowerCase()) === 0
+ : !1;
+ if (r.handshakeToken && r.handshakeToken === this._handshakeToken)
+ return (this._targetOrigin = t), !0;
+ }
+ return !1;
+ }),
+ (t.prototype.error = function(n, t) {
+ var i = n;
+ this._error(i, t, i.handshakeToken);
+ }),
+ (t.prototype._error = function(n, t, i) {
+ var r = {
+ id: n.id,
+ error: this._customSerializeObject([t], n.serializationSettings)[0],
+ jsonrpc: '2.0',
+ handshakeToken: i
+ };
+ this._sendRpcMessage(r);
+ }),
+ (t.prototype._success = function(n, t, i) {
+ var r = {
+ id: n.id,
+ result: this._customSerializeObject([t], n.serializationSettings)[0],
+ jsonrpc: '2.0',
+ handshakeToken: i
+ };
+ this._sendRpcMessage(r);
+ }),
+ (t.prototype._sendRpcMessage = function(n) {
+ var t = JSON.stringify(n);
+ this._postToWindow.postMessage(t, '*');
+ }),
+ (t.prototype._shouldSkipSerialization = function(n) {
+ for (var r, i = 0, u = t.WINDOW_TYPES_TO_SKIP_SERIALIZATION.length; i < u; i++)
+ if (((r = t.WINDOW_TYPES_TO_SKIP_SERIALIZATION[i]), window[r] && n instanceof window[r]))
+ return !0;
+ if (window.jQuery)
+ for (i = 0, u = t.JQUERY_TYPES_TO_SKIP_SERIALIZATION.length; i < u; i++)
+ if (
+ ((r = t.JQUERY_TYPES_TO_SKIP_SERIALIZATION[i]),
+ window.jQuery[r] && n instanceof window.jQuery[r])
+ )
+ return !0;
+ return !1;
+ }),
+ (t.prototype._customSerializeObject = function(n, i, r, u, f) {
+ var h = this,
+ a,
+ o,
+ l,
+ v,
+ e,
+ c,
+ s;
+ if (
+ (r === void 0 && (r = null),
+ u === void 0 && (u = 1),
+ f === void 0 && (f = 1),
+ !n || f > t.MAX_XDM_DEPTH) ||
+ this._shouldSkipSerialization(n)
+ )
+ return null;
+ if (
+ ((a = function(t, e, o) {
+ var s, c, l, a, v;
+ try {
+ s = t[o];
+ } catch (y) {}
+ ((c = typeof s), c !== 'undefined') &&
+ ((l = -1),
+ c === 'object' && (l = r.originalObjects.indexOf(s)),
+ l >= 0
+ ? ((a = r.newObjects[l]),
+ a.__circularReferenceId || (a.__circularReferenceId = u++),
+ (e[o] = { __circularReference: a.__circularReferenceId }))
+ : c === 'function'
+ ? ((v = h._nextProxyFunctionId++),
+ (e[o] = {
+ __proxyFunctionId: h._registerProxyFunction(s, n),
+ __channelId: h._channelId
+ }))
+ : c === 'object'
+ ? (e[o] =
+ s && s instanceof Date
+ ? { __proxyDate: s.getTime() }
+ : h._customSerializeObject(s, i, r, u, f + 1))
+ : o !== '__proxyFunctionId' && (e[o] = s));
+ }),
+ r || (r = { newObjects: [], originalObjects: [] }),
+ r.originalObjects.push(n),
+ n instanceof Array)
+ )
+ for (o = [], r.newObjects.push(o), e = 0, c = n.length; e < c; e++) a(n, o, e);
+ else {
+ o = {};
+ r.newObjects.push(o);
+ l = {};
+ try {
+ for (s in n) l[s] = !0;
+ for (v = Object.getOwnPropertyNames(n), e = 0, c = v.length; e < c; e++) l[v[e]] = !0;
+ } catch (y) {}
+ for (s in l) ((s && s[0] !== '_') || (i && i.includeUnderscoreProperties)) && a(n, o, s);
+ }
+ return r.originalObjects.pop(), r.newObjects.pop(), o;
+ }),
+ (t.prototype._registerProxyFunction = function(n, t) {
+ var i = this._nextProxyFunctionId++;
+ return (
+ (this._proxyFunctions['proxy' + i] = function() {
+ return n.apply(t, Array.prototype.slice.call(arguments, 0));
+ }),
+ i
+ );
+ }),
+ (t.prototype._customDeserializeObject = function(n, t) {
+ var e = this,
+ o = this,
+ r,
+ i,
+ u,
+ f;
+ if (!n) return null;
+ if (
+ (t || (t = {}),
+ (r = function(n, i) {
+ var r = n[i],
+ u = typeof r;
+ i === '__circularReferenceId' && u === 'number'
+ ? ((t[r] = n), delete n[i])
+ : u === 'object' &&
+ r &&
+ (r.__proxyFunctionId
+ ? (n[i] = function() {
+ return o.invokeRemoteMethod(
+ 'proxy' + r.__proxyFunctionId,
+ '__proxyFunctions',
+ Array.prototype.slice.call(arguments, 0),
+ null,
+ { includeUnderscoreProperties: !0 }
+ );
+ })
+ : r.__proxyDate
+ ? (n[i] = new Date(r.__proxyDate))
+ : r.__circularReference
+ ? (n[i] = t[r.__circularReference])
+ : e._customDeserializeObject(r, t));
+ }),
+ n instanceof Array)
+ )
+ for (i = 0, u = n.length; i < u; i++) r(n, i);
+ else if (typeof n == 'object') for (f in n) r(n, f);
+ return n;
+ }),
+ (t._nextChannelId = 1),
+ (t.MAX_XDM_DEPTH = 100),
+ (t.WINDOW_TYPES_TO_SKIP_SERIALIZATION = ['Node', 'Window', 'Event']),
+ (t.JQUERY_TYPES_TO_SKIP_SERIALIZATION = ['jQuery']),
+ t
+ );
+ })();
+ n.XDMChannel = r;
+ e = (function() {
+ function n() {
+ this._channels = [];
+ this._subscribe(window);
+ }
+ return (
+ (n.get = function() {
+ return this._default || (this._default = new n()), this._default;
+ }),
+ (n.prototype.addChannel = function(n, t) {
+ var i = new r(n, t);
+ return this._channels.push(i), i;
+ }),
+ (n.prototype.removeChannel = function(n) {
+ this._channels = this._channels.filter(function(t) {
+ return t !== n;
+ });
+ }),
+ (n.prototype._handleMessageReceived = function(n) {
+ var i, e, r, t, u, f;
+ if (typeof n.data == 'string')
+ try {
+ t = JSON.parse(n.data);
+ } catch (o) {}
+ if (t) {
+ for (u = !1, i = 0, e = this._channels.length; i < e; i++)
+ (r = this._channels[i]),
+ r.owns(n.source, n.origin, t) && ((f = r), (u = r.onMessage(t, n.origin) || u));
+ !f ||
+ u ||
+ (window.console &&
+ console.error('No handler found on any channel for message: ' + JSON.stringify(t)),
+ t.instanceId &&
+ f.error(t, 'The registered object ' + t.instanceId + ' could not be found.'));
+ }
+ }),
+ (n.prototype._subscribe = function(n) {
+ var t = this;
+ n.addEventListener
+ ? n.addEventListener('message', function(n) {
+ t._handleMessageReceived(n);
+ })
+ : n.attachEvent('onmessage', function(n) {
+ t._handleMessageReceived(n);
+ });
+ }),
+ n
+ );
+ })();
+ n.XDMChannelManager = e;
+})(XDM || (XDM = {})),
+ (function(n) {
+ function at() {
+ function r() {
+ n ||
+ (n = setTimeout(function() {
+ n = 0;
+ tt();
+ }, 50));
+ }
+ var n,
+ i = !1,
+ t;
+ try {
+ i = typeof document.cookie == 'string';
+ } catch (f) {}
+ i ||
+ Object.defineProperty(Document.prototype, 'cookie', {
+ get: function() {
+ return '';
+ },
+ set: function() {}
+ });
+ t = !1;
+ try {
+ t = !!window.localStorage;
+ } catch (f) {}
+ t ||
+ (delete window.localStorage,
+ (u = new g(r)),
+ Object.defineProperty(window, 'localStorage', { value: u }),
+ delete window.sessionStorage,
+ Object.defineProperty(window, 'sessionStorage', { value: new g() }));
+ }
+ function nt(f) {
+ r = f || {};
+ e = r.usePlatformScripts;
+ a = r.usePlatformStyles;
+ window.setTimeout(function() {
+ var f = {
+ notifyLoadSucceeded: !r.explicitNotifyLoaded,
+ extensionReusedCallback: r.extensionReusedCallback,
+ vssSDKVersion: n.VssSDKVersion
+ };
+ i.invokeRemoteMethod('initialHandshake', 'VSS.HostControl', [f]).then(function(n) {
+ var f, r, o, h, l, s, v, i;
+ if (
+ ((t = n.pageContext),
+ (b = t.webContext),
+ (k = n.initialConfig || {}),
+ (d = n.contribution),
+ (c = n.extensionContext),
+ n.sandboxedStorage)
+ ) {
+ if (((f = !1), u))
+ if (n.sandboxedStorage.localStorage) {
+ for (
+ r = n.sandboxedStorage.localStorage, o = 0, h = Object.keys(u);
+ o < h.length;
+ o++
+ )
+ (i = h[o]), (l = u.getItem(i)), l !== r[i] && ((r[i] = l), (f = !0));
+ for (s = 0, v = Object.keys(r); s < v.length; s++) (i = v[s]), u.setItem(i, r[i]);
+ } else u.length > 0 && (f = !0);
+ lt = !0;
+ f && tt();
+ }
+ e || a ? ht() : w();
+ });
+ }, 0);
+ }
+ function tt() {
+ var n = { localStorage: JSON.stringify(u || {}) };
+ i.invokeRemoteMethod('updateSandboxedStorage', 'VSS.HostControl', [n]);
+ }
+ function pt(n, t) {
+ var i;
+ i = typeof n == 'string' ? [n] : n;
+ t || (t = function() {});
+ l
+ ? it(i, t)
+ : (r ? e || ((e = !0), s && ((s = !1), ht())) : nt({ usePlatformScripts: !0 }),
+ rt(function() {
+ it(i, t);
+ }));
+ }
+ function it(n, i) {
+ t.diagnostics.bundlingEnabled
+ ? window.require(['VSS/Bundling'], function(t) {
+ t.requireModules(n).spread(function() {
+ i.apply(this, arguments);
+ });
+ })
+ : window.require(n, i);
+ }
+ function rt(n) {
+ s ? window.setTimeout(n, 0) : (f || (f = []), f.push(n));
+ }
+ function wt() {
+ i.invokeRemoteMethod('notifyLoadSucceeded', 'VSS.HostControl');
+ }
+ function ut(n) {
+ i.invokeRemoteMethod('notifyLoadFailed', 'VSS.HostControl', [n]);
+ }
+ function ft() {
+ return b;
+ }
+ function bt() {
+ return k;
+ }
+ function et() {
+ return c;
+ }
+ function kt() {
+ return d;
+ }
+ function dt(n, t) {
+ return ot(n).then(function(n) {
+ return (
+ t || (t = {}),
+ t.webContext || (t.webContext = ft()),
+ t.extensionContext || (t.extensionContext = et()),
+ n.getInstance(n.id, t)
+ );
+ });
+ }
+ function ot(t) {
+ var r = XDM.createDeferred();
+ return (
+ n.ready(function() {
+ i
+ .invokeRemoteMethod('getServiceContribution', 'vss.hostManagement', [t])
+ .then(function(n) {
+ var t = n;
+ t.getInstance = function(t, i) {
+ return st(n, t, i);
+ };
+ r.resolve(t);
+ }, r.reject);
+ }),
+ r.promise
+ );
+ }
+ function gt(t) {
+ var r = XDM.createDeferred();
+ return (
+ n.ready(function() {
+ i
+ .invokeRemoteMethod('getContributionsForTarget', 'vss.hostManagement', [t])
+ .then(function(n) {
+ var t = [];
+ n.forEach(function(n) {
+ var i = n;
+ i.getInstance = function(t, i) {
+ return st(n, t, i);
+ };
+ t.push(i);
+ });
+ r.resolve(t);
+ }, r.reject);
+ }),
+ r.promise
+ );
+ }
+ function st(t, r, u) {
+ var f = XDM.createDeferred();
+ return (
+ n.ready(function() {
+ i
+ .invokeRemoteMethod('getBackgroundContributionInstance', 'vss.hostManagement', [
+ t,
+ r,
+ u
+ ])
+ .then(f.resolve, f.reject);
+ }),
+ f.promise
+ );
+ }
+ function ni(n, t) {
+ i.getObjectRegistry().register(n, t);
+ }
+ function ti(n) {
+ i.getObjectRegistry().unregister(n);
+ }
+ function ii(n, t) {
+ return i.getObjectRegistry().getInstance(n, t);
+ }
+ function ri() {
+ return i.invokeRemoteMethod('getAccessToken', 'VSS.HostControl');
+ }
+ function ui() {
+ return i.invokeRemoteMethod('getAppToken', 'VSS.HostControl');
+ }
+ function fi(n, t) {
+ o || (o = document.getElementsByTagName('body').item(0));
+ var r = typeof n == 'number' ? n : o.scrollWidth,
+ u = typeof t == 'number' ? t : o.scrollHeight;
+ i.invokeRemoteMethod('resize', 'VSS.HostControl', [r, u]);
+ }
+ function ht() {
+ var i = si(t.webContext),
+ f,
+ g,
+ n,
+ s,
+ o,
+ b,
+ k,
+ nt,
+ tt,
+ d,
+ u;
+ if (
+ ((window.__vssPageContext = t),
+ (window.__cultureInfo = t.microsoftAjaxConfig.cultureInfo),
+ a !== !1 &&
+ t.coreReferences.stylesheets &&
+ t.coreReferences.stylesheets.forEach(function(n) {
+ if (n.isCoreStylesheet) {
+ var t = document.createElement('link');
+ t.href = h(n.url, i);
+ t.rel = 'stylesheet';
+ p(t, 'head');
+ }
+ }),
+ !e)
+ ) {
+ l = !0;
+ w();
+ return;
+ }
+ if (
+ ((f = []),
+ (g = !1),
+ t.coreReferences.scripts &&
+ (t.coreReferences.scripts.forEach(function(n) {
+ if (n.isCoreModule) {
+ var r = !1,
+ t = window;
+ n.identifier === 'JQuery'
+ ? (r = !!t.jQuery)
+ : n.identifier === 'JQueryUI'
+ ? (r = !!(t.jQuery && t.jQuery.ui && t.jQuery.ui.version))
+ : n.identifier === 'AMDLoader' &&
+ (r = typeof t.define == 'function' && !!t.define.amd);
+ r ? (g = !0) : f.push({ source: h(n.url, i) });
+ }
+ }),
+ t.coreReferences.coreScriptsBundle &&
+ !g &&
+ (f = [{ source: h(t.coreReferences.coreScriptsBundle.url, i) }]),
+ t.coreReferences.extensionCoreReferences &&
+ f.push({ source: h(t.coreReferences.extensionCoreReferences.url, i) })),
+ (n = { baseUrl: c.baseUri, contributionPaths: null, paths: {}, shim: {} }),
+ r.moduleLoaderConfig &&
+ (r.moduleLoaderConfig.baseUrl && (n.baseUrl = r.moduleLoaderConfig.baseUrl),
+ oi(r.moduleLoaderConfig, n),
+ ct(r.moduleLoaderConfig, n)),
+ t.moduleLoaderConfig &&
+ (ct(t.moduleLoaderConfig, n), (s = t.moduleLoaderConfig.contributionPaths), s))
+ )
+ for (o in s)
+ if (
+ s.hasOwnProperty(o) &&
+ !n.paths[o] &&
+ ((b = s[o].value),
+ (n.paths[o] = b.match('^https?://') ? b : i + b),
+ (k = t.moduleLoaderConfig.paths),
+ k)
+ ) {
+ nt = o + '/';
+ tt = v(i, t.moduleLoaderConfig.baseUrl);
+ for (d in k)
+ ei(d, nt) &&
+ ((u = k[d]),
+ u.match('^https?://') || (u = u[0] === '/' ? v(i, u) : v(tt, u)),
+ (n.paths[d] = u));
+ }
+ window.__vssModuleLoaderConfig = n;
+ f.push({ content: 'require.config(' + JSON.stringify(n) + ');' });
+ y(f, 0, function() {
+ l = !0;
+ w();
+ });
+ }
+ function ei(n, t) {
+ return n && n.length >= t.length ? n.substr(0, t.length).localeCompare(t) === 0 : !1;
+ }
+ function v(n, t) {
+ var i = n || '';
+ return i[i.length - 1] !== '/' && (i += '/'), t && (i += t[0] === '/' ? t.substr(1) : t), i;
+ }
+ function oi(n, t, i) {
+ var r, u;
+ if (n.paths) {
+ t.paths || (t.paths = {});
+ for (r in n.paths)
+ n.paths.hasOwnProperty(r) &&
+ ((u = n.paths[r]), i && (u = i(r, n.paths[r])), u && (t.paths[r] = u));
+ }
+ }
+ function ct(n, t) {
+ if (n.shim) {
+ t.shim || (t.shim = {});
+ for (var i in n.shim) n.shim.hasOwnProperty(i) && (t.shim[i] = n.shim[i]);
+ }
+ }
+ function si(n) {
+ var r = n.account || n.host,
+ t = r.uri,
+ i = r.relativeUri;
+ return (
+ t &&
+ i &&
+ (t[t.length - 1] !== '/' && (t += '/'),
+ i[i.length - 1] !== '/' && (i += '/'),
+ (t = t.substr(0, t.length - i.length))),
+ t
+ );
+ }
+ function y(n, t, i) {
+ var f = this,
+ r,
+ u;
+ if (t >= n.length) {
+ i.call(this);
+ return;
+ }
+ r = document.createElement('script');
+ r.type = 'text/javascript';
+ n[t].source
+ ? ((u = n[t].source),
+ (r.src = u),
+ r.addEventListener('load', function() {
+ y.call(f, n, t + 1, i);
+ }),
+ r.addEventListener('error', function() {
+ ut('Failed to load script: ' + u);
+ }),
+ p(r, 'head'))
+ : n[t].content && ((r.textContent = n[t].content), p(r, 'head'), y.call(this, n, t + 1, i));
+ }
+ function p(n, t) {
+ var i = document.getElementsByTagName(t)[0];
+ i || ((i = document.createElement(t)), document.appendChild(i));
+ i.appendChild(n);
+ }
+ function h(n, t) {
+ var i = (n || '').toLowerCase();
+ return (
+ i.substr(0, 2) !== '//' &&
+ i.substr(0, 5) !== 'http:' &&
+ i.substr(0, 6) !== 'https:' &&
+ (n = t + (i[0] === '/' ? '' : '/') + n),
+ n
+ );
+ }
+ function w() {
+ var t = this,
+ n;
+ s = !0;
+ f &&
+ ((n = f),
+ (f = null),
+ n.forEach(function(n) {
+ n.call(t);
+ }));
+ }
+ var yt;
+ n.VssSDKVersion = 2;
+ n.VssSDKRestVersion = '4.0';
+ var o,
+ b,
+ t,
+ c,
+ k,
+ d,
+ r,
+ l = !1,
+ e,
+ a,
+ s = !1,
+ f,
+ i = XDM.XDMChannelManager.get().addChannel(window.parent),
+ u,
+ lt = !1,
+ g = (function() {
+ function n() {
+ t && t.call(this);
+ }
+ function i() {}
+ var t;
+ return (
+ Object.defineProperties(i.prototype, {
+ getItem: {
+ get: function() {
+ return function(n) {
+ var t = this['' + n];
+ return typeof t == 'undefined' ? null : t;
+ };
+ }
+ },
+ setItem: {
+ get: function() {
+ return function(t, i) {
+ t = '' + t;
+ var u = this[t],
+ r = '' + i;
+ u !== r && ((this[t] = r), n());
+ };
+ }
+ },
+ removeItem: {
+ get: function() {
+ return function(t) {
+ t = '' + t;
+ typeof this[t] != 'undefined' && (delete this[t], n());
+ };
+ }
+ },
+ clear: {
+ get: function() {
+ return function() {
+ var r = Object.keys(this),
+ t,
+ i,
+ u;
+ if (r.length > 0) {
+ for (t = 0, i = r; t < i.length; t++) (u = i[t]), delete this[u];
+ n();
+ }
+ };
+ }
+ },
+ key: {
+ get: function() {
+ return function(n) {
+ return Object.keys(this)[n];
+ };
+ }
+ },
+ length: {
+ get: function() {
+ return Object.keys(this).length;
+ }
+ }
+ }),
+ i
+ );
+ })();
+ if (!window.__vssNoSandboxShim)
+ try {
+ at();
+ } catch (vt) {
+ window.console &&
+ window.console.warn &&
+ window.console.warn(
+ 'Failed to shim support for sandboxed properties: ' +
+ vt.message +
+ '. Set "window.__vssNoSandboxShim = true" in order to bypass the shim of sandboxed properties.'
+ );
+ }
+ (function(n) {
+ n.Dialog = 'ms.vss-web.dialog-service';
+ n.Navigation = 'ms.vss-web.navigation-service';
+ n.ExtensionData = 'ms.vss-web.data-service';
+ })((yt = n.ServiceIds || (n.ServiceIds = {})));
+ n.init = nt;
+ n.require = pt;
+ n.ready = rt;
+ n.notifyLoadSucceeded = wt;
+ n.notifyLoadFailed = ut;
+ n.getWebContext = ft;
+ n.getConfiguration = bt;
+ n.getExtensionContext = et;
+ n.getContribution = kt;
+ n.getService = dt;
+ n.getServiceContribution = ot;
+ n.getServiceContributions = gt;
+ n.register = ni;
+ n.unregister = ti;
+ n.getRegisteredObject = ii;
+ n.getAccessToken = ri;
+ n.getAppToken = ui;
+ n.resize = fi;
+ })(VSS || (VSS = {}));
diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock
index a10da4a267b..906f3b75881 100644
--- a/server/sonar-web/yarn.lock
+++ b/server/sonar-web/yarn.lock
@@ -7496,8 +7496,8 @@ sshpk@^1.7.0:
tweetnacl "~0.14.0"
ssri@^5.2.4:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06"
+ version "5.2.4"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.2.4.tgz#9985e14041e65fc397af96542be35724ac11da52"
dependencies:
safe-buffer "^5.1.1"
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java b/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java
index 69e3050a698..5da7fdb1c12 100644
--- a/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java
+++ b/sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java
@@ -140,7 +140,7 @@ public abstract class ServletFilter implements Filter {
*/
public static class Builder {
private static final String WILDCARD_CHAR = "*";
- private static final Collection<String> STATIC_RESOURCES = unmodifiableList(asList("/css/*", "/fonts/*", "/images/*", "/js/*", "/static/*",
+ private static final Collection<String> STATIC_RESOURCES = unmodifiableList(asList("*.css", "*.css.map", "*.ico", "*.png", "*.gif", "*.svg", "*.js", "*.js.map", "*.eot", "*.ttf", "*.woff", "/static/*",
"/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*"));
private final Set<String> inclusions = new LinkedHashSet<>();
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java
index 6bc34cdaeb5..6d98e78e2b0 100644
--- a/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java
+++ b/sonar-plugin-api/src/test/java/org/sonar/api/web/ServletFilterTest.java
@@ -217,10 +217,17 @@ public class ServletFilterTest {
@Test
public void test_staticResourcePatterns() {
assertThat(ServletFilter.UrlPattern.Builder.staticResourcePatterns()).containsOnly(
- "/css/*",
- "/fonts/*",
- "/images/*",
- "/js/*",
+ "*.css",
+ "*.css.map",
+ "*.ico",
+ "*.png",
+ "*.gif",
+ "*.svg",
+ "*.js",
+ "*.js.map",
+ "*.eot",
+ "*.ttf",
+ "*.woff",
"/static/*",
"/robots.txt",
"/favicon.ico",