aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2018-03-07 17:47:48 +0100
committerSonarTech <sonartech@sonarsource.com>2018-03-26 20:20:57 +0200
commit9ac78350022bfadca4845a42cb600dfa762fb66b (patch)
tree1fb5e6366cb0e439be7c12ac9d8dbdbaf0c8b637
parentcb0a23c978efcc296cf29837c9e6e1a657403404 (diff)
downloadsonarqube-9ac78350022bfadca4845a42cb600dfa762fb66b.tar.gz
sonarqube-9ac78350022bfadca4845a42cb600dfa762fb66b.zip
VSTS-142 Handle authentication to display private project in VSTS widgets
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/Configuration.tsx157
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/LoginForm.tsx100
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/LoginLink.tsx60
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/ProjectSelector.tsx264
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/ProjectSelectorItem.tsx55
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/QGWidget.tsx8
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/SonarCloudIcon.tsx39
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/components/Widget.tsx74
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/index.js57
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/utils.ts47
-rw-r--r--server/sonar-web/src/main/js/app/integration/vsts/vsts.css205
-rw-r--r--sonar-plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java6
12 files changed, 969 insertions, 103 deletions
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
index 838114c4cbd..6afeb86b3a4 100644
--- 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
@@ -18,78 +18,97 @@
* 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;
-}
+import LoginForm from './LoginForm';
+import ProjectSelector from './ProjectSelector';
+import { Component, searchProjects } from '../../../../api/components';
+import {
+ Settings,
+ VSTSWidgetSettings,
+ VSTSConfigurationContext,
+ serializeWidgetSettings,
+ parseWidgetSettings
+} from '../utils';
+import { getCurrentUser } from '../../../../api/users';
+import { CurrentUser } from '../../../types';
interface Props {
+ contribution: string;
widgetHelpers: any;
}
interface State {
+ currentUser?: CurrentUser;
loading: boolean;
- organizations?: Array<{ key: string; name: string }>;
- projects?: Array<{ label: string; value: string }>;
+ projects: Component[];
settings: Settings;
- widgetConfigurationContext?: any;
+ selectedProject?: Component;
+ widgetConfigurationContext?: VSTSConfigurationContext;
}
-declare const VSS: any;
+declare const VSS: {
+ register: (contributionId: string, callback: Function) => void;
+ resize: Function;
+};
+
+const PAGE_SIZE = 10;
export default class Configuration extends React.PureComponent<Props, State> {
mounted = false;
- state: State = { loading: true, settings: { project: '' } };
+ state: State = { loading: true, projects: [], settings: { project: '' } };
componentDidMount() {
this.mounted = true;
- this.props.widgetHelpers.IncludeWidgetConfigurationStyles();
- VSS.register('e56c6ff0-c6f9-43d0-bdef-b3f1aa0dc6dd', () => {
+ VSS.register(this.props.contribution, () => {
return { load: this.load, onSave: this.onSave };
});
}
+ componentDidUpdate() {
+ VSS.resize();
+ }
+
componentWillUnmount() {
this.mounted = false;
}
- load = (widgetSettings: any, widgetConfigurationContext: any) => {
- const settings: Settings = JSON.parse(widgetSettings.customSettings.data);
+ load = (
+ widgetSettings: VSTSWidgetSettings,
+ widgetConfigurationContext: VSTSConfigurationContext
+ ) => {
+ const settings = parseWidgetSettings(widgetSettings);
if (this.mounted) {
this.setState({ settings: settings || {}, widgetConfigurationContext });
- this.fetchProjects();
+ this.fetchInitialData();
}
return this.props.widgetHelpers.WidgetStatusHelper.Success();
};
onSave = () => {
- if (!this.state.settings || !this.state.settings.project) {
+ const { settings } = this.state;
+ if (!settings.project) {
return this.props.widgetHelpers.WidgetConfigurationSave.Invalid();
}
- return this.props.widgetHelpers.WidgetConfigurationSave.Valid({
- data: JSON.stringify(this.state.settings)
- });
+ return this.props.widgetHelpers.WidgetConfigurationSave.Valid(
+ serializeWidgetSettings(settings)
+ );
};
- fetchProjects = (organization?: string) => {
+ fetchInitialData = () => {
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
- });
+ getCurrentUser()
+ .then(currentUser => {
+ this.setState({ currentUser });
+ const params: { ps: number; filter?: string } = { ps: PAGE_SIZE };
+ if (currentUser.isLoggedIn) {
+ params.filter = 'isFavorite';
}
- },
- () => {
- this.setState({
- projects: [],
- loading: false
- });
- }
- );
+ return searchProjects(params);
+ })
+ .then(this.handleSearchProjectsResult, this.stopLoading);
+ };
+
+ handleReload = () => {
+ this.fetchInitialData();
};
handleProjectChange = (
@@ -106,13 +125,41 @@ export default class Configuration extends React.PureComponent<Props, State> {
const { widgetHelpers } = this.props;
if (widgetConfigurationContext && widgetConfigurationContext.notify) {
const eventName = widgetHelpers.WidgetEvent.ConfigurationChange;
- const eventArgs = widgetHelpers.WidgetEvent.Args({ data: JSON.stringify(settings) });
+ const eventArgs = widgetHelpers.WidgetEvent.Args(serializeWidgetSettings(settings));
widgetConfigurationContext.notify(eventName, eventArgs);
}
};
+ handleProjectSearch = (query: string) => {
+ const searchParams: { ps: number; filter?: string } = { ps: PAGE_SIZE };
+ if (query) {
+ searchParams.filter = query;
+ }
+ return searchProjects(searchParams).then(this.handleSearchProjectsResult, this.stopLoading);
+ };
+
+ handleProjectSelect = (project: Component) => {
+ this.setState(
+ ({ settings }) => ({
+ selectedProject: project,
+ settings: { ...settings, project: project.key }
+ }),
+ this.notifyChange
+ );
+ };
+
+ handleSearchProjectsResult = ({ components }: { components: Component[] }) => {
+ if (this.mounted) {
+ this.setState({ loading: false, projects: components });
+ }
+ };
+
+ stopLoading = () => {
+ this.setState({ loading: false });
+ };
+
render() {
- const { projects, loading, settings } = this.state;
+ const { currentUser, projects, loading, selectedProject, settings } = this.state;
if (loading) {
return (
<div className="vsts-loading">
@@ -120,27 +167,27 @@ export default class Configuration extends React.PureComponent<Props, State> {
</div>
);
}
+
+ const isLoggedIn = Boolean(currentUser && currentUser.isLoggedIn);
+ const selected = selectedProject || projects.find(project => project.key === settings.project);
return (
- <div className="widget-configuration">
- <div className="dropdown" id="project">
+ <div className="widget-configuration vsts-configuration bowtie">
+ <div className="dropdown config-settings-field" id="sonarcloud-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>
+ <ProjectSelector
+ isLoggedIn={isLoggedIn}
+ onQueryChange={this.handleProjectSearch}
+ onSelect={this.handleProjectSelect}
+ projects={projects}
+ selected={selected}
+ />
</div>
+ {!isLoggedIn && (
+ <div className="config-settings-field">
+ <label>You must be logged in to see your private projects :</label>
+ <LoginForm onReload={this.handleReload} />
+ </div>
+ )}
</div>
);
}
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/LoginForm.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/LoginForm.tsx
new file mode 100644
index 00000000000..10367598a92
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/components/LoginForm.tsx
@@ -0,0 +1,100 @@
+/*
+ * 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 LoginLink from './LoginLink';
+import SonarCloudIcon from './SonarCloudIcon';
+import * as theme from '../../../../app/theme';
+import { IdentityProvider } from '../../../types';
+import { getIdentityProviders } from '../../../../api/users';
+import { getTextColor } from '../../../../helpers/colors';
+import { getBaseUrl } from '../../../../helpers/urls';
+
+interface Props {
+ onReload: () => void;
+ title?: string;
+}
+
+interface State {
+ identityProviders?: IdentityProvider[];
+}
+
+export default class LoginForm extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {};
+
+ componentDidMount() {
+ this.mounted = true;
+ getIdentityProviders().then(
+ identityProvidersResponse => {
+ if (this.mounted) {
+ this.setState({
+ identityProviders: identityProvidersResponse.identityProviders
+ });
+ }
+ },
+ () => {}
+ );
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ render() {
+ const { onReload, title } = this.props;
+ const { identityProviders } = this.state;
+ const vstsProvider =
+ identityProviders && identityProviders.find(provider => provider.key === 'microsoft');
+
+ return (
+ <div className="vsts-widget-login">
+ {title && <SonarCloudIcon size={32} />}
+ {title && <p className="login-message-text">{title}</p>}
+ {identityProviders && (
+ <section className="oauth-providers">
+ {vstsProvider && (
+ <LoginLink
+ onReload={onReload}
+ sessionUrl={`sessions/init/${vstsProvider.key}`}
+ style={{
+ backgroundColor: vstsProvider.backgroundColor,
+ color: getTextColor(vstsProvider.backgroundColor, theme.secondFontColor)
+ }}>
+ <img
+ alt={vstsProvider.name}
+ height="20"
+ src={getBaseUrl() + vstsProvider.iconPath}
+ width="20"
+ />
+ <span>{vstsProvider.name} log in</span>
+ </LoginLink>
+ )}
+ </section>
+ )}
+
+ <div className="text-center">
+ <LoginLink onReload={onReload} sessionUrl={'sessions/new'}>
+ {vstsProvider ? 'More options' : 'Log in on SonarCloud'}
+ </LoginLink>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/LoginLink.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/LoginLink.tsx
new file mode 100644
index 00000000000..a4dc36adc92
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/components/LoginLink.tsx
@@ -0,0 +1,60 @@
+/*
+ * 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 { getBaseUrl } from '../../../../helpers/urls';
+
+interface Props {
+ className?: string;
+ children: React.ReactNode;
+ onReload: () => void;
+ style?: React.CSSProperties;
+ sessionUrl: string;
+}
+
+export default class LoginLink extends React.PureComponent<Props> {
+ handleLoginClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.stopPropagation();
+ event.currentTarget.blur();
+
+ (window as any).authenticationDone = () => {
+ this.props.onReload();
+ };
+
+ const returnTo = encodeURIComponent(window.location.pathname + '?type=authenticated');
+ window.open(
+ `${getBaseUrl()}/${this.props.sessionUrl}?return_to=${returnTo}`,
+ 'Login on SonarCloud',
+ 'toolbar=0,status=0,width=377,height=380'
+ );
+ };
+
+ render() {
+ return (
+ <a
+ className={this.props.className}
+ href="#"
+ onClick={this.handleLoginClick}
+ style={this.props.style}>
+ {this.props.children}
+ </a>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/ProjectSelector.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/ProjectSelector.tsx
new file mode 100644
index 00000000000..d28fa4dc504
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/components/ProjectSelector.tsx
@@ -0,0 +1,264 @@
+/*
+ * 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 { debounce } from 'lodash';
+import ProjectSelectorItem from './ProjectSelectorItem';
+import { Component } from '../../../../api/components';
+
+interface Props {
+ isLoggedIn: boolean;
+ onQueryChange: (query: string) => Promise<void>;
+ onSelect: (component: Component) => void;
+ projects: Component[];
+ selected?: Component;
+}
+
+interface State {
+ activeIdx: number;
+ activeKey?: string;
+ favorite: boolean;
+ open: boolean;
+ search: string;
+ searching: boolean;
+}
+
+export default class ProjectSelector extends React.PureComponent<Props, State> {
+ node?: HTMLElement | null;
+ debouncedHandleSearch: () => void;
+
+ constructor(props: Props) {
+ super(props);
+ const firstProject = props.projects[0];
+ this.state = {
+ activeIdx: firstProject ? 0 : -1,
+ activeKey: firstProject && firstProject.key,
+ favorite: props.isLoggedIn,
+ open: false,
+ search: '',
+ searching: false
+ };
+ this.debouncedHandleSearch = debounce(this.handleSearch, 250);
+ }
+
+ componentDidMount() {
+ window.addEventListener('click', this.handleClickOutside);
+ if (this.node) {
+ this.node.addEventListener('keydown', this.handleKeyDown, true);
+ }
+ }
+
+ componentWillReceiveProps(nextProps: Props) {
+ if (this.props.projects !== nextProps.projects) {
+ let activeIdx = nextProps.projects.findIndex(project => project.key === this.state.activeKey);
+ activeIdx = activeIdx >= 0 ? activeIdx : 0;
+ this.setState({ activeIdx, activeKey: this.getActiveKey(activeIdx) });
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('click', this.handleClickOutside);
+ if (this.node) {
+ this.node.removeEventListener('keydown', this.handleKeyDown);
+ }
+ }
+
+ getActiveKey = (idx: number) => {
+ const { projects } = this.props;
+ return projects[idx] && projects[idx].key;
+ };
+
+ getEmptyMessage = () => {
+ const { favorite, search } = this.state;
+ if (search) {
+ return 'No project matching your search.';
+ } else if (favorite) {
+ return "You don't have any favorite projects yet.";
+ }
+ return 'No project have been found';
+ };
+
+ handleClickOutside = (event: Event) => {
+ if (!this.node || !this.node.contains(event.target as HTMLElement)) {
+ this.setState({ open: false });
+ }
+ };
+
+ handleFilterAll = () => {
+ this.setState({ favorite: false, searching: true }, this.handleSearch);
+ };
+
+ handleFilterFavorite = () => {
+ this.setState({ favorite: true, searching: true }, this.handleSearch);
+ };
+
+ handleItemHover = (item: Component) => {
+ let activeIdx = this.props.projects.findIndex(project => project.key === item.key);
+ activeIdx = activeIdx >= 0 ? activeIdx : 0;
+ this.setState({ activeIdx, activeKey: this.getActiveKey(activeIdx) });
+ };
+
+ handleKeyDown = (evt: KeyboardEvent) => {
+ switch (evt.keyCode) {
+ case 40: // down
+ evt.stopPropagation();
+ evt.preventDefault();
+ this.setState(this.selectNextItem);
+ break;
+ case 38: // up
+ evt.stopPropagation();
+ evt.preventDefault();
+ this.setState(this.selectPreviousItem);
+ break;
+ case 37: // left
+ case 39: // right
+ evt.stopPropagation();
+ break;
+ case 13: // enter
+ if (this.state.activeIdx >= 0) {
+ this.handleSelect(this.props.projects[this.state.activeIdx]);
+ }
+ break;
+ case 27: // escape
+ this.setState({ open: false });
+ break;
+ }
+ };
+
+ handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.setState(
+ { search: event.currentTarget.value, searching: true },
+ this.debouncedHandleSearch
+ );
+ };
+
+ handleSearch = () => {
+ const filter = [];
+ if (this.state.favorite) {
+ filter.push('isFavorite');
+ }
+ if (this.state.search) {
+ filter.push(`query = "${this.state.search}"`);
+ }
+ this.props.onQueryChange(filter.join(' and ')).then(this.stopSearching, this.stopSearching);
+ };
+
+ handleSelect = (project: Component) => {
+ this.props.onSelect(project);
+ this.setState({ open: false });
+ };
+
+ selectNextItem = ({ activeIdx }: State, { projects }: Props) => {
+ let newActiveIdx = activeIdx + 1;
+ if (activeIdx < 0 || activeIdx >= projects.length - 1) {
+ newActiveIdx = 0;
+ }
+ return { activeIdx: newActiveIdx, activeKey: this.getActiveKey(newActiveIdx) };
+ };
+
+ selectPreviousItem = ({ activeIdx }: State, { projects }: Props) => {
+ let newActiveIdx = activeIdx - 1;
+ if (activeIdx <= 0) {
+ newActiveIdx = projects.length - 1;
+ }
+ return { activeIdx: newActiveIdx, activeKey: this.getActiveKey(newActiveIdx) };
+ };
+
+ stopSearching = () => {
+ this.setState({ searching: false });
+ };
+
+ toggleOpen = () => {
+ this.setState(({ open }) => ({ open: !open }));
+ };
+
+ render() {
+ const { isLoggedIn, projects, selected } = this.props;
+ const { activeIdx, favorite, open, search, searching } = this.state;
+ return (
+ <div className="project-picker" ref={node => (this.node = node)}>
+ <div
+ className="filtered-list-dropdown-menu"
+ onClick={this.toggleOpen}
+ role="button"
+ tabIndex={0}>
+ <span className="selected-item-text">
+ {selected ? selected.name : 'Select a project...'}
+ </span>
+ <span className="drop-icon bowtie-icon bowtie-chevron-down-light" />
+ </div>
+ {open && (
+ <div className="filtered-list-popup" role="dialog">
+ <div className="filtered-list-control bowtie-filtered-list">
+ <div className="filter-container">
+ {isLoggedIn && (
+ <div className="views">
+ <ul className="pivot-view" role="tablist">
+ <li
+ className={classNames('filtered-list-tab', { selected: favorite })}
+ role="presentation">
+ <a onClick={this.handleFilterFavorite} role="tab" tabIndex={0}>
+ My Projects
+ </a>
+ </li>
+ <li
+ className={classNames('filtered-list-tab', { selected: !favorite })}
+ role="presentation">
+ <a onClick={this.handleFilterAll} role="tab" tabIndex={-1}>
+ All
+ </a>
+ </li>
+ </ul>
+ </div>
+ )}
+ <div className="filtered-list-search-container bowtie-style">
+ <input
+ autoFocus={true}
+ className="filtered-list-search"
+ onChange={this.handleSearchChange}
+ placeholder="Search by project name"
+ type="text"
+ value={search}
+ />
+ {searching && <i className="spinner" />}
+ </div>
+ </div>
+ <ul className="filtered-list">
+ {projects.map((project, idx) => (
+ <ProjectSelectorItem
+ isActive={activeIdx === idx}
+ isSelected={Boolean(selected && selected.key === project.key)}
+ key={project.key}
+ onHover={this.handleItemHover}
+ onSelect={this.handleSelect}
+ project={project}
+ />
+ ))}
+ {projects.length <= 0 && (
+ <li className="filtered-list-message">{this.getEmptyMessage()}</li>
+ )}
+ </ul>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/ProjectSelectorItem.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/ProjectSelectorItem.tsx
new file mode 100644
index 00000000000..6dd56d58056
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/components/ProjectSelectorItem.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 { Component } from '../../../../api/components';
+
+interface Props {
+ isActive: boolean;
+ isSelected: boolean;
+ onHover: (project: Component) => void;
+ onSelect: (project: Component) => void;
+ project: Component;
+}
+
+export default class ProjectSelectorItem extends React.PureComponent<Props> {
+ handleClick = () => {
+ this.props.onSelect(this.props.project);
+ };
+
+ handleHover = () => {
+ this.props.onHover(this.props.project);
+ };
+
+ render() {
+ return (
+ <li
+ className={classNames('filtered-list-item', {
+ 'current-item': this.props.isSelected,
+ 'active-item': this.props.isActive
+ })}
+ onClick={this.handleClick}
+ onFocus={this.handleHover}
+ onMouseOver={this.handleHover}>
+ {this.props.project.name}
+ </li>
+ );
+ }
+}
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
index d7e3910f9e1..51a08eec4ce 100644
--- 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
@@ -19,13 +19,12 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
+import SonarCloudIcon from './SonarCloudIcon';
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 } = {
@@ -35,8 +34,7 @@ const QG_LEVELS: { [level: string]: string } = {
NONE: 'None'
};
-export default function QGWidget({ component, metrics }: Props) {
- const qgMetric = metrics && metrics.find(m => m.key === 'alert_status');
+export default function QGWidget({ component }: Props) {
const qgMeasure = component && component.measures.find(m => m.metric === 'alert_status');
if (!qgMeasure || !qgMeasure.value) {
@@ -49,7 +47,7 @@ export default function QGWidget({ component, metrics }: Props) {
<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'}
+ <SonarCloudIcon fill="#FFF" /> Quality Gate
</div>
</a>
</div>
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/components/SonarCloudIcon.tsx b/server/sonar-web/src/main/js/app/integration/vsts/components/SonarCloudIcon.tsx
new file mode 100644
index 00000000000..17cbb789252
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/components/SonarCloudIcon.tsx
@@ -0,0 +1,39 @@
+/*
+ * 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 { IconProps } from '../../../../components/icons-components/types';
+
+export default function SonarCloudIcon({ className, fill = '#f3702a', size = 22 }: IconProps) {
+ return (
+ <svg
+ className={className}
+ height={size}
+ version="1.1"
+ viewBox="0 0 22 22"
+ width={size}
+ xmlSpace="preserve"
+ xmlnsXlink="http://www.w3.org/1999/xlink">
+ <path
+ d="M20.24 10.65l.15.29.14.3.12.3.1.32.09.32.07.33.04.34.03.34.02.35-.06.8-.16.76-.25.73-.34.68-.42.62-.5.57-.56.5-.63.42-.68.34-.72.25-.77.16-.8.05-.39-.01-.39-.04-.38-.06-.37-.09-.37-.11-.35-.13-.34-.16-.33-.17-.31-.2-.31-.21-.28-.23-.28-.25-.27.25-.29.24-.3.22-.32.2-.33.17-.35.16-.35.14-.37.11-.37.09-.39.06-.39.04-.4.02-.8-.06-.76-.15-.73-.26-.68-.34-.63-.42-.56-.5-.5-.56-.42-.63-.34-.68-.25-.72-.16-.77-.05-.8.03-.64.1-.63.17-.6.23-.57.28-.55.34-.51.39-.47.43-.42.48-.38.52-.34.55-.28.59-.22v-.08l.05-.8.16-.76.25-.73.34-.68.43-.63.49-.56.57-.5.63-.42.67-.34.73-.25.77-.16L11 2l.79.05.77.16.73.25.68.34.62.42.57.5.49.56.43.63.34.68.25.73.16.76.05.8v.07l.04.01.27.09.27.11.26.12.25.13.24.14.24.15.23.17.22.17.21.19.21.19.19.21.19.21v.01l-.01-.01.19.26.19.27.17.28zm-3.77-.64l-.11.27-.12.25-.13.25-.15.24-.16.24-.17.23-.18.21-.19.21-.2.2-.21.2-.22.18-.22.17-.03.02-.03.02-.02.01-.03.02-.03.01-.03.02-.04.01-.03.01-.03.01h-.04l-.03.01h-.03l-.04.01h-.03l-.09-.01-.08-.01-.08-.03-.08-.03-.07-.04-.07-.05-.06-.05-.05-.06-.05-.07-.04-.07-.03-.07-.02-.08-.02-.09v-.19l.01-.05.02-.05.01-.05.02-.05.02-.04.03-.05.03-.04.03-.04.03-.04.04-.03.04-.03.04-.03.01-.01.23-.18.22-.2.21-.2.19-.22.18-.24.16-.24.15-.26.12-.27.12-.27.09-.29.07-.29.05-.3.04-.31.01-.31-.03-.51-.09-.5-.14-.48-.19-.45-.24-.42-.28-.39-.32-.36-.36-.33-.39-.28-.43-.23-.45-.2-.48-.14-.49-.08-.51-.03-.5.03-.49.08-.46.13-.44.18-.42.23-.39.27-.35.3-.32.35-.29.37-.24.41-.2.43-.15.46-.1.48-.05.49H7.04l.16.01.15.01.15.01.15.02.15.02.14.03.15.02.14.03.14.04.14.04.14.04.13.04.13.05.02.01.08.02.07.03.07.03.07.03.07.03.07.04.07.03.06.04.07.04.06.04.06.04.07.04.06.04.06.05.03.03.03.03.03.03.03.04.02.04.03.03.02.04.01.05.02.04.01.04.01.05.01.04v.05l.01.05-.01.08-.01.09-.03.08-.03.07-.04.08-.05.06-.05.06-.06.06-.07.04-.07.04-.07.04-.08.02-.09.01-.08.01h-.04l-.04-.01h-.04l-.04-.01-.04-.01-.03-.01-.04-.01-.03-.01-.03-.02-.04-.02-.03-.01-.03-.02-.03-.03-.03-.02.01.01-.03-.02-.03-.02-.03-.02-.04-.02-.03-.02-.03-.02-.04-.02-.03-.01-.04-.02-.04-.01-.03-.02-.04-.01-.04-.02-.04-.01h-.01l.03.01-.03-.01-.08-.03-.1-.03-.11-.03-.11-.03-.11-.03-.1-.03-.12-.02-.11-.02-.11-.01-.11-.02-.12-.01h-.11L7 9.17h-.11l-.52.03-.49.09-.48.14-.45.19-.42.23-.4.29-.36.32-.32.36-.28.39-.24.42-.19.46-.14.47-.09.5-.02.51.02.51.09.5.14.47.19.45.24.43.28.39.32.36.36.32.4.28.42.24.45.19.48.14.49.09.52.03.26-.01.27-.02.26-.04.25-.06.25-.06.24-.09.24-.09.23-.11.22-.12.22-.14.2-.14.2-.16.18-.17.18-.18-.1-.16-.1-.2-.1-.2-.09-.2-.08-.21-.07-.22-.07-.21-.06-.22-.04-.22-.05-.23-.03-.23-.02-.23-.01-.23-.01-.24v-.01l.01-.08.01-.09.02-.08.04-.07.04-.07.04-.07.06-.06.06-.06.06-.04.08-.04.07-.03.08-.03.09-.01.08-.01.09.01.08.01.08.03.08.03.07.04.06.04.07.06.05.06.05.07.04.07.03.07.02.08.02.09v.08l.03.52.09.49.14.48.19.45.24.42.28.39.32.36.36.33.4.28.42.24.45.19.48.14.49.08.52.03-.01-.01.52-.03.49-.09.48-.14.45-.19.42-.24.4-.28.36-.32.32-.36.28-.39.24-.43.19-.45.14-.47.08-.5.03-.51-.01-.4-.06-.39-.08-.38-.12-.36-.15-.35-.18-.34-.2-.32-.23-.29-.25-.28-.28-.26-.3-.23-.32-.2-.34-.18-.35-.15-.01.04-.09.27-.1.27zm-6.36 6.6l-.02-.03.02.03z"
+ style={{ fill }}
+ />
+ </svg>
+ );
+}
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
index 98ac6c85aa7..276633320a8 100644
--- 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
@@ -19,65 +19,77 @@
*/
import * as React from 'react';
import QGWidget from './QGWidget';
+import LoginForm from './LoginForm';
import { getMeasuresAndMeta, MeasureComponent } from '../../../../api/measures';
import { Metric } from '../../../types';
+import { Settings } from '../utils';
interface Props {
- widgetHelpers: any;
+ settings: Settings;
}
interface State {
component?: MeasureComponent;
loading: boolean;
metrics?: Metric[];
+ unauthorized: boolean;
}
-
-declare const VSS: any;
-
export default class Widget extends React.PureComponent<Props, State> {
mounted = false;
- state: State = { loading: true };
+ state: State = { loading: true, unauthorized: false };
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;
+ const { settings } = this.props;
+ if (settings.project) {
+ this.fetchProjectMeasures(settings.project);
+ } else {
+ this.setState({ loading: false });
+ }
}
- load = (widgetSettings: any) => {
- const settings = JSON.parse(widgetSettings.customSettings.data);
- if (this.mounted) {
- if (settings && settings.project) {
- this.fetchProjectMeasures(settings.project);
+ componentWillReceiveProps(nextProps: Props) {
+ const { project } = nextProps.settings;
+ if (project !== this.props.settings.project) {
+ if (project) {
+ this.fetchProjectMeasures(project);
} else {
- this.setState({ loading: false });
+ this.setState({ component: undefined });
}
}
- return this.props.widgetHelpers.WidgetStatusHelper.Success();
- };
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
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({ component, loading: false, metrics, unauthorized: false });
}
},
- () => {
- this.setState({ loading: false });
+ response => {
+ if (response && response.response.status === 403) {
+ this.setState({ loading: false, unauthorized: true });
+ } else {
+ this.setState({ loading: false });
+ }
}
);
};
+ handleReload = () => {
+ const { settings } = this.props;
+ if (settings.project) {
+ this.fetchProjectMeasures(settings.project);
+ }
+ };
+
render() {
- const { component, loading, metrics } = this.state;
+ const { component, loading, metrics, unauthorized } = this.state;
if (loading) {
return (
<div className="vsts-loading">
@@ -86,10 +98,18 @@ export default class Widget extends React.PureComponent<Props, State> {
);
}
+ if (unauthorized) {
+ return (
+ <div className="widget">
+ <LoginForm onReload={this.handleReload} title="Authentication on SonarCloud required" />
+ </div>
+ );
+ }
+
if (!component || !metrics) {
return (
<div className="vsts-widget-configure widget">
- <h2 className="title">Quality Widget</h2>
+ <h2 className="title">Code Quality</h2>
<div className="content">
<div>Configure widget</div>
<img
@@ -101,6 +121,6 @@ export default class Widget extends React.PureComponent<Props, State> {
);
}
- return <QGWidget component={component} metrics={metrics} />;
+ return <QGWidget component={component} />;
}
}
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
index 433334447b6..468f5190698 100644
--- a/server/sonar-web/src/main/js/app/integration/vsts/index.js
+++ b/server/sonar-web/src/main/js/app/integration/vsts/index.js
@@ -22,21 +22,50 @@ import React from 'react';
import { render } from 'react-dom';
import Configuration from './components/Configuration';
import Widget from './components/Widget';
+import { parseWidgetSettings } from './utils';
import './vsts.css';
-VSS.init({
- explicitNotifyLoaded: true,
- usePlatformStyles: true
-});
+const container = document.getElementById('content');
+const query = parse(window.location.search.replace('?', ''));
-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);
+if (query.type === 'authenticated') {
+ if (window.opener && window.opener.authenticationDone) {
+ window.opener.authenticationDone();
}
- VSS.notifyLoadSucceeded();
-});
+ window.close();
+} else if (VSS && query.contribution && VSS.init && VSS.require) {
+ VSS.init({
+ explicitNotifyLoaded: true,
+ usePlatformStyles: true
+ });
+
+ VSS.require('TFS/Dashboards/WidgetHelpers', WidgetHelpers => {
+ WidgetHelpers.IncludeWidgetStyles();
+ WidgetHelpers.IncludeWidgetConfigurationStyles();
+
+ if (query.type === 'configuration') {
+ render(
+ <Configuration contribution={query.contribution} widgetHelpers={WidgetHelpers} />,
+ container
+ );
+ } else {
+ VSS.register(query.contribution, () => {
+ const loadFunction = loadVSTSWidget(WidgetHelpers);
+ return { load: loadFunction, reload: loadFunction };
+ });
+ }
+ VSS.notifyLoadSucceeded();
+ });
+}
+
+function loadVSTSWidget(WidgetHelpers) {
+ return widgetSettings => {
+ try {
+ render(<Widget settings={parseWidgetSettings(widgetSettings)} />, container);
+ } catch (error) {
+ return WidgetHelpers.WidgetStatusHelper.Failure(error);
+ }
+
+ return WidgetHelpers.WidgetStatusHelper.Success();
+ };
+}
diff --git a/server/sonar-web/src/main/js/app/integration/vsts/utils.ts b/server/sonar-web/src/main/js/app/integration/vsts/utils.ts
new file mode 100644
index 00000000000..ec9bc4688b8
--- /dev/null
+++ b/server/sonar-web/src/main/js/app/integration/vsts/utils.ts
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+export interface VSTSConfigurationContext {
+ notify: Function;
+}
+
+export interface VSTSCustomSettings {
+ data: string;
+}
+
+export interface VSTSWidgetSettings {
+ customSettings: VSTSCustomSettings;
+}
+
+export interface Settings {
+ project?: string;
+}
+
+export function parseWidgetSettings(widgetSettings: VSTSWidgetSettings): Settings {
+ try {
+ return JSON.parse(widgetSettings.customSettings.data) || {};
+ } catch (e) {
+ return {};
+ }
+}
+
+export function serializeWidgetSettings(parsedSettings: Settings): VSTSCustomSettings {
+ return { data: JSON.stringify(parsedSettings) };
+}
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
index 8c0b5a62242..634a9bd345a 100644
--- a/server/sonar-web/src/main/js/app/integration/vsts/vsts.css
+++ b/server/sonar-web/src/main/js/app/integration/vsts/vsts.css
@@ -61,6 +61,64 @@
color: white;
}
+.widget .footer {
+ display: flex;
+ align-items: center;
+}
+
+.widget .footer svg {
+ margin-right: 8px;
+}
+
+.vsts-widget-login {
+ text-align: center;
+ padding-top: 4px;
+}
+
+.vsts-widget-login .login-message-text {
+ color: #666;
+ margin: 0;
+}
+
+.vsts-widget-login .oauth-providers {
+ margin-top: 8px;
+ margin-bottom: 8px;
+}
+.vsts-widget-login .oauth-providers a {
+ display: inline-block;
+ line-height: 22px;
+ padding: 4px 6px;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 2px;
+ box-sizing: border-box;
+ background-color: var(--darkBlue);
+ color: #fff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.vsts-widget-login .oauth-providers a:hover,
+.vsts-widget-login .oauth-providers a:focus {
+ box-shadow: 0 0 16px rgba(0, 0, 0, 0.2);
+}
+
+.vsts-widget-login .oauth-providers span {
+ padding-left: 4px;
+}
+
+.vsts-widget-login .oauth-providers img {
+ vertical-align: top;
+}
+
+.vsts-configuration {
+ min-height: 540px;
+}
+
+.vsts-configuration .config-settings-field {
+ margin-bottom: 20px;
+}
+
.big-value {
font-size: 36px;
line-height: 68px;
@@ -87,3 +145,150 @@
.Select {
width: 100%;
}
+
+.project-picker {
+ position: relative;
+ width: 100%;
+ height: 32px;
+}
+
+.filtered-list-dropdown-menu {
+ white-space: nowrap;
+ position: relative;
+ cursor: pointer;
+ padding: 6px;
+ border: 1px solid #c8c8c8;
+}
+
+.filtered-list-dropdown-menu .drop-icon {
+ float: right;
+ position: relative;
+ overflow: hidden;
+ vertical-align: middle;
+}
+
+.filtered-list-dropdown-menu .selected-item-text {
+ width: 90%;
+ padding-left: 5px;
+ padding-right: 5px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ word-wrap: normal;
+ vertical-align: middle;
+ display: inline-block;
+}
+
+.filtered-list-popup {
+ position: absolute;
+ display: block;
+ width: 100%;
+ top: 30px;
+ left: 0;
+ z-index: 20000;
+ overflow-y: auto;
+ max-height: 400px;
+ font-size: 12px;
+ background-color: #fff;
+ border: 1px solid #c8c8c8;
+ box-shadow: 0 2.5px 5px rgba(0, 0, 0, 0.4);
+}
+
+.filtered-list-popup .filtered-list-control .pivot-view {
+ margin-left: 0;
+}
+
+.filtered-list-control.bowtie-filtered-list .filtered-list-tab > a {
+ outline: none;
+}
+
+.filtered-list-tab.selected::before {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 10px;
+ right: 10px;
+ height: 2px;
+ background-color: #0078d7;
+}
+
+.filtered-list-tab:first-child.selected::before {
+ left: 0;
+}
+
+.filtered-list-control .filter-container {
+ position: relative;
+}
+
+.filtered-list-search::placeholder {
+ font-size: 12px;
+}
+
+.filtered-list-search-container .spinner {
+ position: absolute;
+ right: 16px;
+ bottom: 18px;
+}
+
+.filtered-list-control.bowtie-filtered-list .filter-container {
+ padding: 10px;
+ width: 100%;
+}
+
+.filtered-list-control.bowtie-filtered-list .filtered-list {
+ padding: 0;
+ margin: 0 0 4px 0;
+ max-height: 300px;
+ overflow: auto;
+}
+
+.filtered-list-control .filtered-list > li {
+ list-style-type: none;
+ padding: 5px 10px;
+ border: none;
+ margin: 0;
+ height: 30px;
+ line-height: 20px;
+ cursor: pointer;
+ position: relative;
+ vertical-align: middle;
+ outline: none;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-wrap: normal;
+ white-space: pre;
+}
+
+.filtered-list-control .filtered-list > li.filtered-list-message {
+ white-space: normal;
+ color: #666;
+ cursor: default;
+ overflow: visible;
+}
+
+.filtered-list-control .filtered-list > li.filtered-list-item.current-item {
+ font-weight: 700;
+ color: #212121;
+ background-color: #f4f4f4;
+}
+
+.filtered-list-control .filtered-list > li.filtered-list-item.active-item {
+ font-weight: normal;
+ color: #212121;
+ background-color: #eff6fc;
+}
+
+.filtered-list-control .filtered-list > li.filtered-list-item.active-item::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ pointer-events: none;
+ border: 1px solid #a6a6a6;
+}
+
+.filtered-list-control .filtered-list > li.filtered-list-item.current-item.active-item {
+ font-weight: 700;
+}
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 5da7fdb1c12..5fd7540ac1d 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,8 +140,10 @@ 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", "*.css.map", "*.ico", "*.png", "*.gif", "*.svg", "*.js", "*.js.map", "*.eot", "*.ttf", "*.woff", "/static/*",
- "/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*"));
+ 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<>();
private final Set<String> exclusions = new LinkedHashSet<>();