aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-03-14 12:47:17 +0100
committerGitHub <noreply@github.com>2018-03-14 12:47:17 +0100
commit6f189a7c95ee207e02c7c9321ed37d0c6ca4afe6 (patch)
tree494c72690c0ac9607716574a5bfa49f9c4ca315a /server
parent47d50b8a9c17d595a07b6a7e157849ccf1ffc302 (diff)
downloadsonarqube-6f189a7c95ee207e02c7c9321ed37d0c6ca4afe6.tar.gz
sonarqube-6f189a7c95ee207e02c7c9321ed37d0c6ca4afe6.zip
rewrite workspace in react (#3140)
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalContainer.tsx19
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx42
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx5
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx12
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx14
-rw-r--r--server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx18
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/CollapseIcon.tsx39
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/ExpandIcon.tsx39
-rw-r--r--server/sonar-web/src/main/js/components/icons-components/MinimizeIcon.tsx39
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueMessage.js10
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js3
-rw-r--r--server/sonar-web/src/main/js/components/workspace/Workspace.tsx235
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceComponentTitle.tsx40
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx102
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceHeader.tsx102
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceNav.tsx63
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceNavComponent.tsx (renamed from server/sonar-web/src/main/js/components/workspace/views/base-viewer-view.js)53
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceNavItem.tsx52
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceNavRule.tsx47
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspacePortal.tsx (renamed from server/sonar-web/src/main/js/components/workspace/models/item.js)42
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx114
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceRuleTitle.tsx36
-rw-r--r--server/sonar-web/src/main/js/components/workspace/WorkspaceRuleViewer.tsx68
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentTitle-test.tsx32
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx61
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceHeader-test.tsx47
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNav-test.tsx51
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavComponent-test.tsx52
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavItem-test.tsx49
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavRule-test.tsx47
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspacePortal-test.tsx (renamed from server/sonar-web/src/main/js/components/workspace/views/items-view.js)20
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx49
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleTitle-test.tsx (renamed from server/sonar-web/src/main/js/components/workspace/views/item-view.js)45
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleViewer-test.tsx57
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentTitle-test.tsx.snap17
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap37
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceHeader-test.tsx.snap52
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNav-test.tsx.snap111
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavComponent-test.tsx.snap18
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavItem-test.tsx.snap26
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavRule-test.tsx.snap18
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap51
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleTitle-test.tsx.snap19
-rw-r--r--server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleViewer-test.tsx.snap38
-rw-r--r--server/sonar-web/src/main/js/components/workspace/context.ts39
-rw-r--r--server/sonar-web/src/main/js/components/workspace/main.js143
-rw-r--r--server/sonar-web/src/main/js/components/workspace/models/items.js59
-rw-r--r--server/sonar-web/src/main/js/components/workspace/styles.css42
-rw-r--r--server/sonar-web/src/main/js/components/workspace/templates/workspace-item.hbs3
-rw-r--r--server/sonar-web/src/main/js/components/workspace/templates/workspace-items.hbs1
-rw-r--r--server/sonar-web/src/main/js/components/workspace/templates/workspace-rule.hbs76
-rw-r--r--server/sonar-web/src/main/js/components/workspace/templates/workspace-viewer-header.hbs17
-rw-r--r--server/sonar-web/src/main/js/components/workspace/templates/workspace-viewer.hbs3
-rw-r--r--server/sonar-web/src/main/js/components/workspace/views/rule-view.js53
-rw-r--r--server/sonar-web/src/main/js/components/workspace/views/viewer-header-view.js110
-rw-r--r--server/sonar-web/src/main/js/components/workspace/views/viewer-view.js75
56 files changed, 2027 insertions, 685 deletions
diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
index d4c6fb3a426..a89319b690a 100644
--- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
+++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
@@ -22,6 +22,7 @@ import * as PropTypes from 'prop-types';
import GlobalNav from './nav/global/GlobalNav';
import GlobalFooterContainer from './GlobalFooterContainer';
import GlobalMessagesContainer from './GlobalMessagesContainer';
+import Workspace from '../../components/workspace/Workspace';
interface Props {
children: React.ReactNode;
@@ -61,14 +62,16 @@ export default class GlobalContainer extends React.PureComponent<Props, State> {
<div className="global-container">
<div className="page-wrapper" id="container">
<div className="page-container">
- <GlobalNav
- closeOnboardingTutorial={this.closeOnboardingTutorial}
- isOnboardingTutorialOpen={this.state.isOnboardingTutorialOpen}
- location={this.props.location}
- openOnboardingTutorial={this.openOnboardingTutorial}
- />
- <GlobalMessagesContainer />
- {this.props.children}
+ <Workspace>
+ <GlobalNav
+ closeOnboardingTutorial={this.closeOnboardingTutorial}
+ isOnboardingTutorialOpen={this.state.isOnboardingTutorialOpen}
+ location={this.props.location}
+ openOnboardingTutorial={this.openOnboardingTutorial}
+ />
+ <GlobalMessagesContainer />
+ {this.props.children}
+ </Workspace>
</div>
</div>
<GlobalFooterContainer />
diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx
index 867f4bd476f..c9b3fb86af6 100644
--- a/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx
+++ b/server/sonar-web/src/main/js/apps/code/components/ComponentPin.tsx
@@ -18,10 +18,11 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as PropTypes from 'prop-types';
import { Component } from '../types';
import { BranchLike } from '../../../app/types';
import PinIcon from '../../../components/shared/pin-icon';
-import Workspace from '../../../components/workspace/main';
+import { WorkspaceContext } from '../../../components/workspace/context';
import { translate } from '../../../helpers/l10n';
interface Props {
@@ -29,19 +30,34 @@ interface Props {
component: Component;
}
-export default function ComponentPin({ branchLike, component }: Props) {
- const handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+export default class ComponentPin extends React.PureComponent<Props> {
+ // prettier-ignore
+ context!: { workspace: WorkspaceContext };
+
+ static contextTypes = {
+ workspace: PropTypes.object.isRequired
+ };
+
+ handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
- Workspace.openComponent({ branchLike, key: component.key });
+ event.currentTarget.blur();
+ this.context.workspace.openComponent({
+ branchLike: this.props.branchLike,
+ key: this.props.component.key,
+ name: this.props.component.path,
+ qualifier: this.props.component.qualifier
+ });
};
- return (
- <a
- className="link-no-underline"
- onClick={handleClick}
- title={translate('component_viewer.open_in_workspace')}
- href="#">
- <PinIcon />
- </a>
- );
+ render() {
+ return (
+ <a
+ className="link-no-underline"
+ href="#"
+ onClick={this.handleClick}
+ title={translate('component_viewer.open_in_workspace')}>
+ <PinIcon />
+ </a>
+ );
+ }
}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
index 1a272610065..97b99759784 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
@@ -37,6 +37,7 @@ import { Button } from '../../../components/ui/buttons';
interface Props {
canWrite: boolean | undefined;
+ hideSimilarRulesFilter?: boolean;
onFilterChange: (changes: Partial<Query>) => void;
onTagsChange: (tags: string[]) => void;
organization: string | undefined;
@@ -228,7 +229,9 @@ export default class RuleDetailsMeta extends React.PureComponent<Props, State> {
to={getRuleUrl(ruleDetails.key, this.props.organization)}>
<LinkIcon />
</Link>
- <SimilarRulesFilter onFilterChange={this.props.onFilterChange} rule={ruleDetails} />
+ {!this.props.hideSimilarRulesFilter && (
+ <SimilarRulesFilter onFilterChange={this.props.onFilterChange} rule={ruleDetails} />
+ )}
</div>
<h3 className="page-title coding-rules-detail-header">
<big>{ruleDetails.name}</big>
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
index 0ba3cd6ec40..72ac6fa5bcd 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx
@@ -20,10 +20,12 @@
import { stringify } from 'querystring';
import * as React from 'react';
import { Link } from 'react-router';
+import * as PropTypes from 'prop-types';
import MeasuresOverlay from './components/MeasuresOverlay';
import { SourceViewerFile, BranchLike } from '../../app/types';
import QualifierIcon from '../shared/QualifierIcon';
import FavoriteContainer from '../controls/FavoriteContainer';
+import { WorkspaceContext } from '../workspace/context';
import {
getPathUrlAsString,
getBranchLikeUrl,
@@ -46,6 +48,13 @@ interface State {
}
export default class SourceViewerHeader extends React.PureComponent<Props, State> {
+ // prettier-ignore
+ context!: { workspace: WorkspaceContext };
+
+ static contextTypes = {
+ workspace: PropTypes.object.isRequired
+ };
+
state: State = { measuresOverlay: false };
handleShowMeasuresClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
@@ -60,8 +69,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State
openInWorkspace = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
const { key } = this.props.sourceViewerFile;
- const Workspace = require('../workspace/main').default;
- Workspace.openComponent({ key, branchLike: this.props.branchLike });
+ this.context.workspace.openComponent({ branchLike: this.props.branchLike, key });
};
render() {
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx
index ef06788347e..4d60126638e 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx
@@ -19,10 +19,12 @@
*/
import * as React from 'react';
import { groupBy } from 'lodash';
+import * as PropTypes from 'prop-types';
import { getTests } from '../../../api/components';
import { BranchLike, SourceLine, TestCase } from '../../../app/types';
import BubblePopup from '../../common/BubblePopup';
import TestStatusIcon from '../../shared/TestStatusIcon';
+import { WorkspaceContext } from '../../workspace/context';
import { isSameBranchLike, getBranchLikeQuery } from '../../../helpers/branches';
import { translate } from '../../../helpers/l10n';
import { collapsePath } from '../../../helpers/path';
@@ -41,7 +43,14 @@ interface State {
}
export default class CoveragePopup extends React.PureComponent<Props, State> {
+ // prettier-ignore
+ context!: { workspace: WorkspaceContext };
mounted = false;
+
+ static contextTypes = {
+ workspace: PropTypes.object.isRequired
+ };
+
state: State = { loading: true, testCases: [] };
componentDidMount() {
@@ -87,8 +96,9 @@ export default class CoveragePopup extends React.PureComponent<Props, State> {
event.preventDefault();
event.currentTarget.blur();
const { key } = event.currentTarget.dataset;
- const Workspace = require('../../workspace/main').default;
- Workspace.openComponent({ key, branchLike: this.props.branchLike });
+ if (key) {
+ this.context.workspace.openComponent({ branchLike: this.props.branchLike, key });
+ }
this.props.onClose();
};
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
index d4b75a93b28..445b750f807 100644
--- a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
+++ b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx
@@ -19,10 +19,12 @@
*/
import * as React from 'react';
import { Link } from 'react-router';
+import * as PropTypes from 'prop-types';
import { groupBy, sortBy } from 'lodash';
import { BranchLike, DuplicatedFile, DuplicationBlock, SourceViewerFile } from '../../../app/types';
import BubblePopup from '../../common/BubblePopup';
import QualifierIcon from '../../shared/QualifierIcon';
+import { WorkspaceContext } from '../../workspace/context';
import { translate } from '../../../helpers/l10n';
import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path';
import { getProjectUrl } from '../../../helpers/urls';
@@ -38,6 +40,13 @@ interface Props {
}
export default class DuplicationPopup extends React.PureComponent<Props> {
+ // prettier-ignore
+ context!: { workspace: WorkspaceContext };
+
+ static contextTypes = {
+ workspace: PropTypes.object.isRequired
+ };
+
isDifferentComponent = (
a: { project: string; subProject?: string },
b: { project: string; subProject?: string }
@@ -48,9 +57,14 @@ export default class DuplicationPopup extends React.PureComponent<Props> {
handleFileClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- const Workspace = require('../../workspace/main').default;
const { key, line } = event.currentTarget.dataset;
- Workspace.openComponent({ key, line, branchLike: this.props.branchLike });
+ if (key) {
+ this.context.workspace.openComponent({
+ branchLike: this.props.branchLike,
+ key,
+ line: line ? Number(line) : undefined
+ });
+ }
this.props.onClose();
};
diff --git a/server/sonar-web/src/main/js/components/icons-components/CollapseIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/CollapseIcon.tsx
new file mode 100644
index 00000000000..f1070ac740e
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/CollapseIcon.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 './types';
+
+export default function CollapseIcon({ className, fill = 'currentColor', size = 16 }: IconProps) {
+ return (
+ <svg
+ className={className}
+ height={size}
+ version="1.1"
+ viewBox="0 0 16 16"
+ width={size}
+ xmlSpace="preserve"
+ xmlnsXlink="http://www.w3.org/1999/xlink">
+ <path
+ d="M8 8.509v3.56c0 .138-.05.257-.151.357-.1.101-.22.151-.358.151a.489.489 0 0 1-.357-.15l-1.145-1.145-2.638 2.639a.251.251 0 0 1-.366 0l-.906-.906a.251.251 0 0 1 0-.366l2.639-2.638-1.144-1.145a.489.489 0 0 1-.151-.357c0-.138.05-.257.15-.358.101-.1.22-.151.358-.151h3.56c.138 0 .257.05.358.151.1.1.151.22.151.358zm6-5.34c0 .068-.026.129-.08.182l-2.638 2.638 1.144 1.145c.101.1.151.22.151.357 0 .138-.05.257-.15.358-.101.1-.22.151-.358.151h-3.56a.489.489 0 0 1-.358-.151A.489.489 0 0 1 8 7.491v-3.56c0-.138.05-.257.151-.357.1-.101.22-.151.358-.151.137 0 .257.05.357.15l1.145 1.145 2.638-2.639a.251.251 0 0 1 .366 0l.906.906c.053.053.079.114.079.183z"
+ style={{ fill }}
+ />
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/icons-components/ExpandIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/ExpandIcon.tsx
new file mode 100644
index 00000000000..1105b7833ed
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/ExpandIcon.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 './types';
+
+export default function ExpandIcon({ className, fill = 'currentColor', size = 16 }: IconProps) {
+ return (
+ <svg
+ className={className}
+ height={size}
+ version="1.1"
+ viewBox="0 0 16 16"
+ width={size}
+ xmlSpace="preserve"
+ xmlnsXlink="http://www.w3.org/1999/xlink">
+ <path
+ d="M7.898 9.25a.247.247 0 0 1-.078.18l-2.593 2.593 1.125 1.125a.48.48 0 0 1 .148.352.48.48 0 0 1-.148.352A.48.48 0 0 1 6 14H2.5a.48.48 0 0 1-.352-.148A.48.48 0 0 1 2 13.5V10a.48.48 0 0 1 .148-.352A.48.48 0 0 1 2.5 9.5a.48.48 0 0 1 .352.148l1.125 1.125L6.57 8.18a.247.247 0 0 1 .36 0l.89.89a.247.247 0 0 1 .078.18zM14 2.5V6a.48.48 0 0 1-.148.352.48.48 0 0 1-.352.148.48.48 0 0 1-.352-.148l-1.125-1.125L9.43 7.82a.247.247 0 0 1-.36 0l-.89-.89a.247.247 0 0 1 0-.36l2.593-2.593-1.125-1.125A.48.48 0 0 1 9.5 2.5a.48.48 0 0 1 .148-.352A.48.48 0 0 1 10 2h3.5a.48.48 0 0 1 .352.148A.48.48 0 0 1 14 2.5z"
+ style={{ fill }}
+ />
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/icons-components/MinimizeIcon.tsx b/server/sonar-web/src/main/js/components/icons-components/MinimizeIcon.tsx
new file mode 100644
index 00000000000..4478239ddf1
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/icons-components/MinimizeIcon.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 './types';
+
+export default function MinimizeIcon({ className, fill = 'currentColor', size = 16 }: IconProps) {
+ return (
+ <svg
+ className={className}
+ height={size}
+ version="1.1"
+ viewBox="0 0 16 16"
+ width={size}
+ xmlSpace="preserve"
+ xmlnsXlink="http://www.w3.org/1999/xlink">
+ <path
+ d="M14 12.1v1.267c0 .176-.08.325-.239.448a.918.918 0 0 1-.58.185H2.819a.918.918 0 0 1-.58-.185C2.08 13.692 2 13.543 2 13.367V12.1c0-.176.08-.326.239-.449a.918.918 0 0 1 .58-.185h10.363c.227 0 .42.062.58.185.158.123.238.273.238.449z"
+ style={{ fill }}
+ />
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
index df26092b983..515edd9b3ba 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.js
@@ -19,6 +19,7 @@
*/
// @flow
import React from 'react';
+import PropTypes from 'prop-types';
import { translate } from '../../../helpers/l10n';
export default class IssueMessage extends React.PureComponent {
@@ -27,13 +28,16 @@ export default class IssueMessage extends React.PureComponent {
rule: string,
organization: string
};
-*/
+ */
+
+ static contextTypes = {
+ workspace: PropTypes.object.isRequired
+ };
handleClick = (e /*: MouseEvent */) => {
e.preventDefault();
e.stopPropagation();
- const Workspace = require('../../workspace/main').default;
- Workspace.openRule({
+ this.context.workspace.openRule({
key: this.props.rule,
organization: this.props.organization
});
diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js
index eafe943c67a..785bc2a21f1 100644
--- a/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js
+++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/IssueMessage-test.js
@@ -27,7 +27,8 @@ it('should render with the message and a link to open the rule', () => {
rule="javascript:S1067"
message="Reduce the number of conditional operators (4) used in the expression"
organization="myorg"
- />
+ />,
+ { context: { workspace: {} } }
);
expect(element).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/components/workspace/Workspace.tsx b/server/sonar-web/src/main/js/components/workspace/Workspace.tsx
new file mode 100644
index 00000000000..a5dcce0d75b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/Workspace.tsx
@@ -0,0 +1,235 @@
+/*
+ * 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 PropTypes from 'prop-types';
+import { omit, uniqBy } from 'lodash';
+import { WorkspaceContext, ComponentDescriptor, RuleDescriptor } from './context';
+import WorkspaceNav from './WorkspaceNav';
+import WorkspacePortal from './WorkspacePortal';
+import { lazyLoad } from '../lazyLoad';
+import './styles.css';
+
+const WorkspaceRuleViewer = lazyLoad(() => import('./WorkspaceRuleViewer'));
+const WorkspaceComponentViewer = lazyLoad(() => import('./WorkspaceComponentViewer'));
+
+interface State {
+ components: ComponentDescriptor[];
+ height: number;
+ maximized?: boolean;
+ open: { component?: string; rule?: string };
+ rules: RuleDescriptor[];
+}
+
+const MIN_HEIGHT = 0.05;
+const MAX_HEIGHT = 0.85;
+const INITIAL_HEIGHT = 300;
+
+const STORAGE_KEY = 'sonarqube-workspace';
+const TYPE_KEY = '__type__';
+
+export default class Workspace extends React.PureComponent<{}, State> {
+ mounted = false;
+
+ static childContextTypes = {
+ workspace: PropTypes.object
+ };
+
+ constructor(props: {}) {
+ super(props);
+ this.state = { height: INITIAL_HEIGHT, open: {}, ...this.loadWorkspace() };
+ }
+
+ getChildContext = (): { workspace: WorkspaceContext } => {
+ return { workspace: { openComponent: this.openComponent, openRule: this.openRule } };
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentDidUpdate(_: {}, prevState: State) {
+ if (prevState.components !== this.state.components || prevState.rules !== this.state.rules) {
+ this.saveWorkspace();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ loadWorkspace = () => {
+ try {
+ const data: any[] = JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '');
+ const components: ComponentDescriptor[] = data.filter(x => x[TYPE_KEY] === 'component');
+ const rules: RuleDescriptor[] = data.filter(x => x[TYPE_KEY] === 'rule');
+ return { components, rules };
+ } catch {
+ // fail silently
+ return { components: [], rules: [] };
+ }
+ };
+
+ saveWorkspace = () => {
+ const data = [
+ // do not save line number, next time the file is open, it should be open on the first line
+ ...this.state.components.map(x => omit({ ...x, [TYPE_KEY]: 'component' }, 'line')),
+ ...this.state.rules.map(x => ({ ...x, [TYPE_KEY]: 'rule' }))
+ ];
+ try {
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
+ } catch {
+ // fail silently
+ }
+ };
+
+ openComponent = (component: ComponentDescriptor) => {
+ this.setState((state: State): Partial<State> => ({
+ components: uniqBy([...state.components, component], component => component.key),
+ open: { component: component.key }
+ }));
+ };
+
+ reopenComponent = (componentKey: string) => {
+ this.setState({ open: { component: componentKey } });
+ };
+
+ openRule = (rule: RuleDescriptor) => {
+ this.setState((state: State): Partial<State> => ({
+ open: { rule: rule.key },
+ rules: uniqBy([...state.rules, rule], rule => rule.key)
+ }));
+ };
+
+ reopenRule = (ruleKey: string) => {
+ this.setState({ open: { rule: ruleKey } });
+ };
+
+ closeComponent = (componentKey: string) => {
+ this.setState((state: State): Partial<State> => ({
+ components: state.components.filter(x => x.key !== componentKey),
+ open: {
+ ...state.open,
+ component: state.open.component === componentKey ? undefined : state.open.component
+ }
+ }));
+ };
+
+ closeRule = (ruleKey: string) => {
+ this.setState((state: State): Partial<State> => ({
+ rules: state.rules.filter(x => x.key !== ruleKey),
+ open: {
+ ...state.open,
+ rule: state.open.rule === ruleKey ? undefined : state.open.rule
+ }
+ }));
+ };
+
+ handleComponentLoad = (details: { key: string; name: string; qualifier: string }) => {
+ if (this.mounted) {
+ const { key, name, qualifier } = details;
+ this.setState((state: State): Partial<State> => ({
+ components: state.components.map(
+ component => (component.key === key ? { ...component, name, qualifier } : component)
+ )
+ }));
+ }
+ };
+
+ handleRuleLoad = (details: { key: string; name: string }) => {
+ if (this.mounted) {
+ const { key, name } = details;
+ this.setState((state: State): Partial<State> => ({
+ rules: state.rules.map(rule => (rule.key === key ? { ...rule, name } : rule))
+ }));
+ }
+ };
+
+ collapse = () => {
+ this.setState({ open: {} });
+ };
+
+ maximize = () => {
+ this.setState({ maximized: true });
+ };
+
+ minimize = () => {
+ this.setState({ maximized: false });
+ };
+
+ resize = (deltaY: number) => {
+ const minHeight = window.innerHeight * MIN_HEIGHT;
+ const maxHeight = window.innerHeight * MAX_HEIGHT;
+ this.setState((state: State): Partial<State> => ({
+ height: Math.min(maxHeight, Math.max(minHeight, state.height - deltaY))
+ }));
+ };
+
+ render() {
+ const { components, open, rules } = this.state;
+
+ const openComponent = open.component && components.find(x => x.key === open.component);
+ const openRule = open.rule && rules.find(x => x.key === open.rule);
+
+ const height = this.state.maximized ? window.innerHeight * MAX_HEIGHT : this.state.height;
+
+ return (
+ <>
+ {this.props.children}
+ <WorkspacePortal>
+ <WorkspaceNav
+ components={components}
+ onComponentClose={this.closeComponent}
+ onComponentOpen={this.reopenComponent}
+ onRuleClose={this.closeRule}
+ onRuleOpen={this.reopenRule}
+ open={this.state.open}
+ rules={this.state.rules}
+ />
+ {openComponent && (
+ <WorkspaceComponentViewer
+ component={openComponent}
+ height={height}
+ maximized={this.state.maximized}
+ onClose={this.closeComponent}
+ onCollapse={this.collapse}
+ onLoad={this.handleComponentLoad}
+ onMaximize={this.maximize}
+ onMinimize={this.minimize}
+ onResize={this.resize}
+ />
+ )}
+ {openRule && (
+ <WorkspaceRuleViewer
+ height={height}
+ maximized={this.state.maximized}
+ onClose={this.closeRule}
+ onCollapse={this.collapse}
+ onLoad={this.handleRuleLoad}
+ onMaximize={this.maximize}
+ onMinimize={this.minimize}
+ onResize={this.resize}
+ rule={openRule}
+ />
+ )}
+ </WorkspacePortal>
+ </>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentTitle.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentTitle.tsx
new file mode 100644
index 00000000000..8679e169731
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentTitle.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { ComponentDescriptor } from './context';
+import QualifierIcon from '../shared/QualifierIcon';
+import { collapsePath } from '../../helpers/path';
+
+interface Props {
+ component: ComponentDescriptor;
+ limited?: boolean;
+}
+
+export default function WorkspaceComponentTitle({ component, limited }: Props) {
+ const { name = '—' } = component;
+ return (
+ <>
+ {component.qualifier && (
+ <QualifierIcon className="little-spacer-right" qualifier={component.qualifier} />
+ )}
+ {limited ? collapsePath(name, 15) : name}
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx
new file mode 100644
index 00000000000..eb2140ed5ef
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceComponentViewer.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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 { ComponentDescriptor } from './context';
+import WorkspaceHeader, { Props as WorkspaceHeaderProps } from './WorkspaceHeader';
+import WorkspaceComponentTitle from './WorkspaceComponentTitle';
+import SourceViewer from '../SourceViewer/SourceViewer';
+import { SourceViewerFile, Omit } from '../../app/types';
+import { scrollToElement } from '../../helpers/scrolling';
+
+export interface Props extends Omit<WorkspaceHeaderProps, 'children' | 'onClose'> {
+ component: ComponentDescriptor;
+ height: number;
+ onClose: (componentKey: string) => void;
+ onLoad: (details: { key: string; name: string; qualifier: string }) => void;
+}
+
+export default class WorkspaceComponentViewer extends React.PureComponent<Props> {
+ container?: HTMLElement | null;
+
+ componentDidMount() {
+ document.documentElement.classList.add('with-workspace');
+ }
+
+ componentWillUnmount() {
+ document.documentElement.classList.remove('with-workspace');
+ }
+
+ handleClose = () => {
+ this.props.onClose(this.props.component.key);
+ };
+
+ handleLoaded = (component: SourceViewerFile) => {
+ this.props.onLoad({
+ key: this.props.component.key,
+ name: component.path,
+ qualifier: component.q
+ });
+
+ if (this.container && this.props.component.line) {
+ const row = this.container.querySelector(
+ `.source-line[data-line-number="${this.props.component.line}"]`
+ );
+ if (row) {
+ scrollToElement(row, {
+ smooth: false,
+ parent: this.container,
+ topOffset: 50,
+ bottomOffset: 50
+ });
+ }
+ }
+ };
+
+ render() {
+ const { component } = this.props;
+
+ return (
+ <div className="workspace-viewer">
+ <WorkspaceHeader
+ maximized={this.props.maximized}
+ onClose={this.handleClose}
+ onCollapse={this.props.onCollapse}
+ onMaximize={this.props.onMaximize}
+ onMinimize={this.props.onMinimize}
+ onResize={this.props.onResize}>
+ <WorkspaceComponentTitle component={component} />
+ </WorkspaceHeader>
+
+ <div
+ className="workspace-viewer-container"
+ ref={node => (this.container = node)}
+ style={{ height: this.props.height }}>
+ <SourceViewer
+ aroundLine={component.line}
+ branchLike={component.branchLike}
+ component={component.key}
+ highlightedLine={component.line}
+ onLoaded={this.handleLoaded}
+ />
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceHeader.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceHeader.tsx
new file mode 100644
index 00000000000..011991dd54d
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceHeader.tsx
@@ -0,0 +1,102 @@
+/*
+ * 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 { DraggableCore, DraggableData } from 'react-draggable';
+import { translate } from '../../helpers/l10n';
+import { ButtonIcon } from '../ui/buttons';
+import ClearIcon from '../icons-components/ClearIcon';
+import CollapseIcon from '../icons-components/CollapseIcon';
+import ExpandIcon from '../icons-components/ExpandIcon';
+import MinimizeIcon from '../icons-components/MinimizeIcon';
+import { IconProps } from '../icons-components/types';
+
+export interface Props {
+ children: React.ReactNode;
+ maximized?: boolean;
+ onClose: () => void;
+ onCollapse: () => void;
+ onMaximize: () => void;
+ onMinimize: () => void;
+ onResize: (deltaY: number) => void;
+}
+
+export default class WorkspaceHeader extends React.PureComponent<Props> {
+ handleDrag = (_event: MouseEvent, data: DraggableData) => {
+ this.props.onResize(data.deltaY);
+ };
+
+ render() {
+ return (
+ <header className="workspace-viewer-header">
+ <h6 className="workspace-viewer-name">{this.props.children}</h6>
+
+ <DraggableCore offsetParent={document.body} onDrag={this.handleDrag}>
+ <div className="workspace-viewer-resize js-resize" />
+ </DraggableCore>
+
+ <div className="workspace-viewer-actions">
+ <WorkspaceHeaderButton
+ icon={MinimizeIcon}
+ onClick={this.props.onCollapse}
+ tooltip="workspace.minimize"
+ />
+
+ {this.props.maximized ? (
+ <WorkspaceHeaderButton
+ icon={CollapseIcon}
+ onClick={this.props.onMinimize}
+ tooltip="workspace.normal_size"
+ />
+ ) : (
+ <WorkspaceHeaderButton
+ icon={ExpandIcon}
+ onClick={this.props.onMaximize}
+ tooltip="workspace.full_window"
+ />
+ )}
+
+ <WorkspaceHeaderButton
+ icon={ClearIcon}
+ onClick={this.props.onClose}
+ tooltip="workspace.close"
+ />
+ </div>
+ </header>
+ );
+ }
+}
+
+interface WorkspaceHeaderButtonProps {
+ icon: React.SFC<IconProps>;
+ onClick: () => void;
+ tooltip: string;
+}
+
+function WorkspaceHeaderButton({ icon: Icon, onClick, tooltip }: WorkspaceHeaderButtonProps) {
+ return (
+ <ButtonIcon
+ className="workspace-header-icon"
+ color="#fff"
+ onClick={onClick}
+ tooltip={translate(tooltip)}>
+ <Icon fill={undefined} />
+ </ButtonIcon>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceNav.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceNav.tsx
new file mode 100644
index 00000000000..52471f7fb63
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceNav.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 { ComponentDescriptor, RuleDescriptor } from './context';
+import WorkspaceNavComponent from './WorkspaceNavComponent';
+import WorkspaceNavRule from './WorkspaceNavRule';
+
+export interface Props {
+ components: ComponentDescriptor[];
+ rules: RuleDescriptor[];
+ onComponentClose: (componentKey: string) => void;
+ onComponentOpen: (componentKey: string) => void;
+ onRuleClose: (ruleKey: string) => void;
+ onRuleOpen: (ruleKey: string) => void;
+ open: { component?: string; rule?: string };
+}
+
+export default function WorkspaceNav(props: Props) {
+ // do not show a tab for the currently open component/rule
+ const components = props.components.filter(x => x.key !== props.open.component);
+ const rules = props.rules.filter(x => x.key !== props.open.rule);
+
+ return (
+ <nav className="workspace-nav">
+ <ul className="workspace-nav-list">
+ {components.map(component => (
+ <WorkspaceNavComponent
+ component={component}
+ key={`component-${component.key}`}
+ onClose={props.onComponentClose}
+ onOpen={props.onComponentOpen}
+ />
+ ))}
+
+ {rules.map(rule => (
+ <WorkspaceNavRule
+ key={`rule-${rule.key}`}
+ onClose={props.onRuleClose}
+ onOpen={props.onRuleOpen}
+ rule={rule}
+ />
+ ))}
+ </ul>
+ </nav>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/views/base-viewer-view.js b/server/sonar-web/src/main/js/components/workspace/WorkspaceNavComponent.tsx
index f5883d57378..15a5c114b35 100644
--- a/server/sonar-web/src/main/js/components/workspace/views/base-viewer-view.js
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceNavComponent.tsx
@@ -17,38 +17,31 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Marionette from 'backbone.marionette';
-import HeaderView from './viewer-header-view';
+import * as React from 'react';
+import { ComponentDescriptor } from './context';
+import WorkspaceComponentTitle from './WorkspaceComponentTitle';
+import WorkspaceNavItem from './WorkspaceNavItem';
-export default Marionette.LayoutView.extend({
- className: 'workspace-viewer',
+export interface Props {
+ component: ComponentDescriptor;
+ onClose: (componentKey: string) => void;
+ onOpen: (componentKey: string) => void;
+}
- modelEvents: {
- destroy: 'destroy'
- },
+export default class WorkspaceNavComponent extends React.PureComponent<Props> {
+ handleClose = () => {
+ this.props.onClose(this.props.component.key);
+ };
- regions: {
- headerRegion: '.workspace-viewer-header',
- viewerRegion: '.workspace-viewer-container'
- },
+ handleOpen = () => {
+ this.props.onOpen(this.props.component.key);
+ };
- onRender() {
- this.showHeader();
- this.$('.workspace-viewer-container').isolatedScroll();
- },
-
- onViewerMinimize() {
- this.trigger('viewerMinimize');
- },
-
- onViewerClose() {
- this.trigger('viewerClose', this.model);
- },
-
- showHeader() {
- const headerView = new HeaderView({ model: this.model });
- this.listenTo(headerView, 'viewerMinimize', this.onViewerMinimize);
- this.listenTo(headerView, 'viewerClose', this.onViewerClose);
- this.headerRegion.show(headerView);
+ render() {
+ return (
+ <WorkspaceNavItem onClose={this.handleClose} onOpen={this.handleOpen}>
+ <WorkspaceComponentTitle component={this.props.component} limited={true} />
+ </WorkspaceNavItem>
+ );
}
-});
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceNavItem.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceNavItem.tsx
new file mode 100644
index 00000000000..6f29f4caef4
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceNavItem.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 ClearIcon from '../icons-components/ClearIcon';
+import { ButtonIcon } from '../ui/buttons';
+
+export interface Props {
+ children: React.ReactNode;
+ onClose: () => void;
+ onOpen: () => void;
+}
+
+export default class WorkspaceNavItem extends React.PureComponent<Props> {
+ handleNameClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+ event.preventDefault();
+ event.currentTarget.blur();
+ this.props.onOpen();
+ };
+
+ render() {
+ return (
+ <li className="workspace-nav-item">
+ <a className="workspace-nav-item-link" href="#" onClick={this.handleNameClick}>
+ {this.props.children}
+ </a>
+ <ButtonIcon
+ className="js-close workspace-nav-item-close workspace-header-icon button-small little-spacer-left"
+ color="#fff"
+ onClick={this.props.onClose}>
+ <ClearIcon fill={undefined} size={12} />
+ </ButtonIcon>
+ </li>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceNavRule.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceNavRule.tsx
new file mode 100644
index 00000000000..1dea8715464
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceNavRule.tsx
@@ -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.
+ */
+import * as React from 'react';
+import { RuleDescriptor } from './context';
+import WorkspaceNavItem from './WorkspaceNavItem';
+import WorkspaceRuleTitle from './WorkspaceRuleTitle';
+
+export interface Props {
+ rule: RuleDescriptor;
+ onClose: (ruleKey: string) => void;
+ onOpen: (ruleKey: string) => void;
+}
+
+export default class WorkspaceNavRule extends React.PureComponent<Props> {
+ handleClose = () => {
+ this.props.onClose(this.props.rule.key);
+ };
+
+ handleOpen = () => {
+ this.props.onOpen(this.props.rule.key);
+ };
+
+ render() {
+ return (
+ <WorkspaceNavItem onClose={this.handleClose} onOpen={this.handleOpen}>
+ <WorkspaceRuleTitle limited={true} rule={this.props.rule} />
+ </WorkspaceNavItem>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/models/item.js b/server/sonar-web/src/main/js/components/workspace/WorkspacePortal.tsx
index 08ad8fe02a1..63af9fe6be6 100644
--- a/server/sonar-web/src/main/js/components/workspace/models/item.js
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspacePortal.tsx
@@ -17,31 +17,27 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Backbone from 'backbone';
+import * as React from 'react';
+import { createPortal } from 'react-dom';
-export default Backbone.Model.extend({
- validate() {
- if (!this.has('__type__')) {
- return 'type is missing';
- }
- if (this.get('__type__') === 'component' && !this.has('key')) {
- return 'key is missing';
- }
- if (this.get('__type__') === 'rule' && !this.has('key')) {
- return 'key is missing';
- }
- },
+export default class WorkspacePortal extends React.PureComponent {
+ el: HTMLElement;
- isComponent() {
- return this.get('__type__') === 'component';
- },
+ constructor(props: {}) {
+ super(props);
+ this.el = document.createElement('div');
+ this.el.classList.add('workspace');
+ }
+
+ componentDidMount() {
+ document.body.appendChild(this.el);
+ }
- isRule() {
- return this.get('__type__') === 'rule';
- },
+ componentWillUnmount() {
+ document.body.removeChild(this.el);
+ }
- destroy(options) {
- this.stopListening();
- this.trigger('destroy', this, this.collection, options);
+ render() {
+ return createPortal(this.props.children, this.el);
}
-});
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx
new file mode 100644
index 00000000000..e154a145773
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleDetails.tsx
@@ -0,0 +1,114 @@
+/*
+ * 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 { keyBy } from 'lodash';
+import { getRuleDetails, getRulesApp } from '../../api/rules';
+import { RuleDetails } from '../../app/types';
+import DeferredSpinner from '../common/DeferredSpinner';
+import RuleDetailsMeta from '../../apps/coding-rules/components/RuleDetailsMeta';
+import RuleDetailsDescription from '../../apps/coding-rules/components/RuleDetailsDescription';
+import '../../apps/coding-rules/styles.css';
+
+interface Props {
+ onLoad: (details: { name: string }) => void;
+ organization: string | undefined;
+ ruleKey: string;
+}
+
+interface State {
+ loading: boolean;
+ referencedRepositories: { [repository: string]: { key: string; language: string; name: string } };
+ ruleDetails?: RuleDetails;
+}
+
+export default class WorkspaceRuleDetails extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { loading: true, referencedRepositories: {} };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchRuleDetails();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (
+ prevProps.ruleKey !== this.props.ruleKey ||
+ prevProps.organization !== this.props.organization
+ ) {
+ this.fetchRuleDetails();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchRuleDetails = () => {
+ this.setState({ loading: true });
+ Promise.all([
+ getRulesApp({ organization: this.props.organization }),
+ getRuleDetails({ key: this.props.ruleKey, organization: this.props.organization })
+ ]).then(
+ ([{ repositories }, { rule }]) => {
+ if (this.mounted) {
+ this.setState({
+ loading: false,
+ referencedRepositories: keyBy(repositories, 'key'),
+ ruleDetails: rule
+ });
+ this.props.onLoad({ name: rule.name });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ noOp = () => {};
+
+ render() {
+ return (
+ <DeferredSpinner loading={this.state.loading}>
+ {this.state.ruleDetails && (
+ <>
+ <RuleDetailsMeta
+ canWrite={false}
+ hideSimilarRulesFilter={true}
+ onFilterChange={this.noOp}
+ onTagsChange={this.noOp}
+ organization={this.props.organization}
+ referencedRepositories={this.state.referencedRepositories}
+ ruleDetails={this.state.ruleDetails}
+ />
+ <RuleDetailsDescription
+ canWrite={false}
+ onChange={this.noOp}
+ organization={this.props.organization}
+ ruleDetails={this.state.ruleDetails}
+ />
+ </>
+ )}
+ </DeferredSpinner>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleTitle.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleTitle.tsx
new file mode 100644
index 00000000000..ac7387ce23b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleTitle.tsx
@@ -0,0 +1,36 @@
+/*
+ * 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 { RuleDescriptor } from './context';
+
+interface Props {
+ limited?: boolean;
+ rule: RuleDescriptor;
+}
+
+export default function WorkspaceRuleTitle({ limited, rule }: Props) {
+ const { name = '—' } = rule;
+ return (
+ <>
+ <i className="icon-workspace-doc little-spacer-right" />
+ {limited ? <span className="text-limited">{name}</span> : name}
+ </>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleViewer.tsx b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleViewer.tsx
new file mode 100644
index 00000000000..513f2ca8311
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/WorkspaceRuleViewer.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { RuleDescriptor } from './context';
+import WorkspaceHeader, { Props as WorkspaceHeaderProps } from './WorkspaceHeader';
+import WorkspaceRuleDetails from './WorkspaceRuleDetails';
+import WorkspaceRuleTitle from './WorkspaceRuleTitle';
+import { Omit } from '../../app/types';
+
+export interface Props extends Omit<WorkspaceHeaderProps, 'children' | 'onClose'> {
+ rule: RuleDescriptor;
+ height: number;
+ onClose: (componentKey: string) => void;
+ onLoad: (details: { key: string; name: string }) => void;
+}
+
+export default class WorkspaceRuleViewer extends React.PureComponent<Props> {
+ handleClose = () => {
+ this.props.onClose(this.props.rule.key);
+ };
+
+ handleLoaded = (rule: { name: string }) => {
+ this.props.onLoad({ key: this.props.rule.key, name: rule.name });
+ };
+
+ render() {
+ const { rule } = this.props;
+
+ return (
+ <div className="workspace-viewer">
+ <WorkspaceHeader
+ maximized={this.props.maximized}
+ onClose={this.handleClose}
+ onCollapse={this.props.onCollapse}
+ onMaximize={this.props.onMaximize}
+ onMinimize={this.props.onMinimize}
+ onResize={this.props.onResize}>
+ <WorkspaceRuleTitle rule={rule} />
+ </WorkspaceHeader>
+
+ <div className="workspace-viewer-container" style={{ height: this.props.height }}>
+ <WorkspaceRuleDetails
+ onLoad={this.handleLoaded}
+ organization={rule.organization}
+ ruleKey={rule.key}
+ />
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentTitle-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentTitle-test.tsx
new file mode 100644
index 00000000000..2db40894949
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentTitle-test.tsx
@@ -0,0 +1,32 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import WorkspaceComponentTitle from '../WorkspaceComponentTitle';
+
+it('should render component', () => {
+ const component = { branchLike: undefined, key: 'foo' };
+ expect(shallow(<WorkspaceComponentTitle component={component} />)).toMatchSnapshot();
+});
+
+it('should render loaded component', () => {
+ const component = { branchLike: undefined, key: 'foo', name: 'src/foo.js', qualifier: 'FIL' };
+ expect(shallow(<WorkspaceComponentTitle component={component} />)).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx
new file mode 100644
index 00000000000..b3cab1c4b41
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceComponentViewer-test.tsx
@@ -0,0 +1,61 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import WorkspaceComponentViewer, { Props } from '../WorkspaceComponentViewer';
+
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should close', () => {
+ const onClose = jest.fn();
+ const wrapper = shallowRender({ onClose });
+ wrapper.find('WorkspaceHeader').prop<Function>('onClose')();
+ expect(onClose).toBeCalledWith('foo');
+});
+
+it('should call back after load', () => {
+ const onLoad = jest.fn();
+ const wrapper = shallowRender({ onLoad });
+ wrapper.find('[onLoaded]').prop<Function>('onLoaded')({
+ key: 'foo',
+ path: 'src/foo.js',
+ q: 'FIL'
+ });
+ expect(onLoad).toBeCalledWith({ key: 'foo', name: 'src/foo.js', qualifier: 'FIL' });
+});
+
+function shallowRender(props?: Partial<Props>) {
+ const component = { branchLike: undefined, key: 'foo' };
+ return shallow(
+ <WorkspaceComponentViewer
+ component={component}
+ height={300}
+ onClose={jest.fn()}
+ onCollapse={jest.fn()}
+ onLoad={jest.fn()}
+ onMaximize={jest.fn()}
+ onMinimize={jest.fn()}
+ onResize={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceHeader-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceHeader-test.tsx
new file mode 100644
index 00000000000..a8be1a80c29
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceHeader-test.tsx
@@ -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.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import WorkspaceHeader, { Props } from '../WorkspaceHeader';
+
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should resize', () => {
+ const onResize = jest.fn();
+ const wrapper = shallowRender({ onResize });
+ wrapper.find('DraggableCore').prop<Function>('onDrag')({}, { deltaY: 15 });
+ expect(onResize).toBeCalledWith(15);
+});
+
+function shallowRender(props?: Partial<Props>) {
+ return shallow(
+ <WorkspaceHeader
+ onClose={jest.fn()}
+ onCollapse={jest.fn()}
+ onMaximize={jest.fn()}
+ onMinimize={jest.fn()}
+ onResize={jest.fn()}
+ {...props}>
+ <div id="workspace-header-children" />
+ </WorkspaceHeader>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNav-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNav-test.tsx
new file mode 100644
index 00000000000..487e94a9b8c
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNav-test.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import WorkspaceNav, { Props } from '../WorkspaceNav';
+
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should not render open component', () => {
+ expect(shallowRender({ open: { component: 'bar' } })).toMatchSnapshot();
+});
+
+it('should not render open rule', () => {
+ expect(shallowRender({ open: { rule: 'qux' } })).toMatchSnapshot();
+});
+
+function shallowRender(props?: Partial<Props>) {
+ const components = [{ branchLike: undefined, key: 'foo' }, { branchLike: undefined, key: 'bar' }];
+ const rules = [{ key: 'qux', organization: 'org' }];
+ return shallow(
+ <WorkspaceNav
+ components={components}
+ onComponentClose={jest.fn()}
+ onComponentOpen={jest.fn()}
+ onRuleClose={jest.fn()}
+ onRuleOpen={jest.fn()}
+ open={{}}
+ rules={rules}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavComponent-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavComponent-test.tsx
new file mode 100644
index 00000000000..e14ddd018c1
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavComponent-test.tsx
@@ -0,0 +1,52 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import WorkspaceNavComponent, { Props } from '../WorkspaceNavComponent';
+
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should close', () => {
+ const onClose = jest.fn();
+ const wrapper = shallowRender({ onClose });
+ wrapper.find('WorkspaceNavItem').prop<Function>('onClose')();
+ expect(onClose).toBeCalledWith('foo');
+});
+
+it('should open', () => {
+ const onOpen = jest.fn();
+ const wrapper = shallowRender({ onOpen });
+ wrapper.find('WorkspaceNavItem').prop<Function>('onOpen')();
+ expect(onOpen).toBeCalledWith('foo');
+});
+
+function shallowRender(props?: Partial<Props>) {
+ const component = { branchLike: undefined, key: 'foo' };
+ return shallow(
+ <WorkspaceNavComponent
+ component={component}
+ onClose={jest.fn()}
+ onOpen={jest.fn()}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavItem-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavItem-test.tsx
new file mode 100644
index 00000000000..3f81b901171
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavItem-test.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import WorkspaceNavItem, { Props } from '../WorkspaceNavItem';
+import { click } from '../../../helpers/testUtils';
+
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should close', () => {
+ const onClose = jest.fn();
+ const wrapper = shallowRender({ onClose });
+ click(wrapper.find('ButtonIcon'));
+ expect(onClose).toBeCalled();
+});
+
+it('should open', () => {
+ const onOpen = jest.fn();
+ const wrapper = shallowRender({ onOpen });
+ click(wrapper.find('.workspace-nav-item-link'));
+ expect(onOpen).toBeCalled();
+});
+
+function shallowRender(props?: Partial<Props>) {
+ return shallow(
+ <WorkspaceNavItem onClose={jest.fn()} onOpen={jest.fn()} {...props}>
+ <div id="workspace-nav-item" />
+ </WorkspaceNavItem>
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavRule-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavRule-test.tsx
new file mode 100644
index 00000000000..15be6798e8b
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceNavRule-test.tsx
@@ -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.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import WorkspaceNavRule, { Props } from '../WorkspaceNavRule';
+
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should close', () => {
+ const onClose = jest.fn();
+ const wrapper = shallowRender({ onClose });
+ wrapper.find('WorkspaceNavItem').prop<Function>('onClose')();
+ expect(onClose).toBeCalledWith('foo');
+});
+
+it('should open', () => {
+ const onOpen = jest.fn();
+ const wrapper = shallowRender({ onOpen });
+ wrapper.find('WorkspaceNavItem').prop<Function>('onOpen')();
+ expect(onOpen).toBeCalledWith('foo');
+});
+
+function shallowRender(props?: Partial<Props>) {
+ const rule = { key: 'foo', organization: 'org' };
+ return shallow(
+ <WorkspaceNavRule onClose={jest.fn()} onOpen={jest.fn()} rule={rule} {...props} />
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/views/items-view.js b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspacePortal-test.tsx
index 06f3f9353bb..35b0909a04b 100644
--- a/server/sonar-web/src/main/js/components/workspace/views/items-view.js
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspacePortal-test.tsx
@@ -17,17 +17,13 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Marionette from 'backbone.marionette';
-import ItemView from './item-view';
-import Template from '../templates/workspace-items.hbs';
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import WorkspacePortal from '../WorkspacePortal';
-export default Marionette.CompositeView.extend({
- className: 'workspace-nav',
- template: Template,
- childViewContainer: '.workspace-nav-list',
- childView: ItemView,
-
- childViewOptions() {
- return { collectionView: this };
- }
+it('should create portal element', () => {
+ const wrapper = shallow(<WorkspacePortal />);
+ const { el } = wrapper.instance() as WorkspacePortal;
+ expect(el.tagName.toLowerCase()).toBe('div');
+ expect(el.className).toBe('workspace');
});
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx
new file mode 100644
index 00000000000..e1b5f68840f
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleDetails-test.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 { shallow } from 'enzyme';
+import WorkspaceRuleDetails from '../WorkspaceRuleDetails';
+import { waitAndUpdate } from '../../../helpers/testUtils';
+
+jest.mock('../../../api/rules', () => ({
+ getRulesApp: jest.fn(() =>
+ Promise.resolve({ repositories: [{ key: 'repo', language: 'xoo', name: 'Xoo Repository' }] })
+ ),
+ getRuleDetails: jest.fn(() => Promise.resolve({ rule: { key: 'foo', name: 'Foo' } }))
+}));
+
+it('should render', async () => {
+ const wrapper = shallow(
+ <WorkspaceRuleDetails onLoad={jest.fn()} organization="org" ruleKey="foo" />
+ );
+ expect(wrapper).toMatchSnapshot();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should call back on load', async () => {
+ const onLoad = jest.fn();
+ const wrapper = shallow(
+ <WorkspaceRuleDetails onLoad={onLoad} organization="org" ruleKey="foo" />
+ );
+ await waitAndUpdate(wrapper);
+ expect(onLoad).toBeCalledWith({ name: 'Foo' });
+});
diff --git a/server/sonar-web/src/main/js/components/workspace/views/item-view.js b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleTitle-test.tsx
index b007f55b7b9..b12796f1129 100644
--- a/server/sonar-web/src/main/js/components/workspace/views/item-view.js
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleTitle-test.tsx
@@ -17,41 +17,16 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import Marionette from 'backbone.marionette';
-import Template from '../templates/workspace-item.hbs';
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import WorkspaceRuleTitle from '../WorkspaceRuleTitle';
-export default Marionette.ItemView.extend({
- tagName: 'li',
- className: 'workspace-nav-item',
- template: Template,
-
- modelEvents: {
- change: 'render',
- showViewer: 'onViewerShow',
- hideViewer: 'onViewerHide'
- },
-
- events: {
- click: 'onClick',
- 'click .js-close': 'onCloseClick'
- },
-
- onClick(e) {
- e.preventDefault();
- this.options.collectionView.trigger('click', this.model);
- },
-
- onCloseClick(e) {
- e.preventDefault();
- e.stopPropagation();
- this.model.destroy();
- },
-
- onViewerShow() {
- this.$el.addClass('hidden');
- },
+it('should render rule', () => {
+ const rule = { key: 'foo', organization: 'org' };
+ expect(shallow(<WorkspaceRuleTitle rule={rule} />)).toMatchSnapshot();
+});
- onViewerHide() {
- this.$el.removeClass('hidden');
- }
+it('should render loaded rule', () => {
+ const rule = { key: 'foo', name: 'Foo', organization: 'org' };
+ expect(shallow(<WorkspaceRuleTitle rule={rule} />)).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleViewer-test.tsx b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleViewer-test.tsx
new file mode 100644
index 00000000000..978b87768e4
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/WorkspaceRuleViewer-test.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 { shallow } from 'enzyme';
+import WorkspaceRuleViewer, { Props } from '../WorkspaceRuleViewer';
+
+it('should render', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should close', () => {
+ const onClose = jest.fn();
+ const wrapper = shallowRender({ onClose });
+ wrapper.find('WorkspaceHeader').prop<Function>('onClose')();
+ expect(onClose).toBeCalledWith('foo');
+});
+
+it('should call back after load', () => {
+ const onLoad = jest.fn();
+ const wrapper = shallowRender({ onLoad });
+ wrapper.find('WorkspaceRuleDetails').prop<Function>('onLoad')({ name: 'Foo' });
+ expect(onLoad).toBeCalledWith({ key: 'foo', name: 'Foo' });
+});
+
+function shallowRender(props?: Partial<Props>) {
+ const rule = { key: 'foo', organization: 'org' };
+ return shallow(
+ <WorkspaceRuleViewer
+ height={300}
+ onClose={jest.fn()}
+ onCollapse={jest.fn()}
+ onLoad={jest.fn()}
+ onMaximize={jest.fn()}
+ onMinimize={jest.fn()}
+ onResize={jest.fn()}
+ rule={rule}
+ {...props}
+ />
+ );
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentTitle-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentTitle-test.tsx.snap
new file mode 100644
index 00000000000..ed1cae463d9
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentTitle-test.tsx.snap
@@ -0,0 +1,17 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render component 1`] = `
+<React.Fragment>
+ —
+</React.Fragment>
+`;
+
+exports[`should render loaded component 1`] = `
+<React.Fragment>
+ <QualifierIcon
+ className="little-spacer-right"
+ qualifier="FIL"
+ />
+ src/foo.js
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap
new file mode 100644
index 00000000000..221000a98f5
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceComponentViewer-test.tsx.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+ className="workspace-viewer"
+>
+ <WorkspaceHeader
+ onClose={[Function]}
+ onCollapse={[MockFunction]}
+ onMaximize={[MockFunction]}
+ onMinimize={[MockFunction]}
+ onResize={[MockFunction]}
+ >
+ <WorkspaceComponentTitle
+ component={
+ Object {
+ "branchLike": undefined,
+ "key": "foo",
+ }
+ }
+ />
+ </WorkspaceHeader>
+ <div
+ className="workspace-viewer-container"
+ style={
+ Object {
+ "height": 300,
+ }
+ }
+ >
+ <Connect(SourceViewerBase)
+ component="foo"
+ onLoaded={[Function]}
+ />
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceHeader-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceHeader-test.tsx.snap
new file mode 100644
index 00000000000..11212c4c2e5
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceHeader-test.tsx.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<header
+ className="workspace-viewer-header"
+>
+ <h6
+ className="workspace-viewer-name"
+ >
+ <div
+ id="workspace-header-children"
+ />
+ </h6>
+ <DraggableCore
+ allowAnyClick={false}
+ cancel={null}
+ disabled={false}
+ enableUserSelectHack={true}
+ grid={null}
+ handle={null}
+ offsetParent={<body />}
+ onDrag={[Function]}
+ onMouseDown={[Function]}
+ onStart={[Function]}
+ onStop={[Function]}
+ transform={null}
+ >
+ <div
+ className="workspace-viewer-resize js-resize"
+ />
+ </DraggableCore>
+ <div
+ className="workspace-viewer-actions"
+ >
+ <WorkspaceHeaderButton
+ icon={[Function]}
+ onClick={[MockFunction]}
+ tooltip="workspace.minimize"
+ />
+ <WorkspaceHeaderButton
+ icon={[Function]}
+ onClick={[MockFunction]}
+ tooltip="workspace.full_window"
+ />
+ <WorkspaceHeaderButton
+ icon={[Function]}
+ onClick={[MockFunction]}
+ tooltip="workspace.close"
+ />
+ </div>
+</header>
+`;
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNav-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNav-test.tsx.snap
new file mode 100644
index 00000000000..28dfed90df5
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNav-test.tsx.snap
@@ -0,0 +1,111 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render open component 1`] = `
+<nav
+ className="workspace-nav"
+>
+ <ul
+ className="workspace-nav-list"
+ >
+ <WorkspaceNavComponent
+ component={
+ Object {
+ "branchLike": undefined,
+ "key": "foo",
+ }
+ }
+ key="component-foo"
+ onClose={[MockFunction]}
+ onOpen={[MockFunction]}
+ />
+ <WorkspaceNavRule
+ key="rule-qux"
+ onClose={[MockFunction]}
+ onOpen={[MockFunction]}
+ rule={
+ Object {
+ "key": "qux",
+ "organization": "org",
+ }
+ }
+ />
+ </ul>
+</nav>
+`;
+
+exports[`should not render open rule 1`] = `
+<nav
+ className="workspace-nav"
+>
+ <ul
+ className="workspace-nav-list"
+ >
+ <WorkspaceNavComponent
+ component={
+ Object {
+ "branchLike": undefined,
+ "key": "foo",
+ }
+ }
+ key="component-foo"
+ onClose={[MockFunction]}
+ onOpen={[MockFunction]}
+ />
+ <WorkspaceNavComponent
+ component={
+ Object {
+ "branchLike": undefined,
+ "key": "bar",
+ }
+ }
+ key="component-bar"
+ onClose={[MockFunction]}
+ onOpen={[MockFunction]}
+ />
+ </ul>
+</nav>
+`;
+
+exports[`should render 1`] = `
+<nav
+ className="workspace-nav"
+>
+ <ul
+ className="workspace-nav-list"
+ >
+ <WorkspaceNavComponent
+ component={
+ Object {
+ "branchLike": undefined,
+ "key": "foo",
+ }
+ }
+ key="component-foo"
+ onClose={[MockFunction]}
+ onOpen={[MockFunction]}
+ />
+ <WorkspaceNavComponent
+ component={
+ Object {
+ "branchLike": undefined,
+ "key": "bar",
+ }
+ }
+ key="component-bar"
+ onClose={[MockFunction]}
+ onOpen={[MockFunction]}
+ />
+ <WorkspaceNavRule
+ key="rule-qux"
+ onClose={[MockFunction]}
+ onOpen={[MockFunction]}
+ rule={
+ Object {
+ "key": "qux",
+ "organization": "org",
+ }
+ }
+ />
+ </ul>
+</nav>
+`;
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavComponent-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavComponent-test.tsx.snap
new file mode 100644
index 00000000000..0f6abcbcdb4
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavComponent-test.tsx.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<WorkspaceNavItem
+ onClose={[Function]}
+ onOpen={[Function]}
+>
+ <WorkspaceComponentTitle
+ component={
+ Object {
+ "branchLike": undefined,
+ "key": "foo",
+ }
+ }
+ limited={true}
+ />
+</WorkspaceNavItem>
+`;
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavItem-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavItem-test.tsx.snap
new file mode 100644
index 00000000000..3834b942dba
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavItem-test.tsx.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<li
+ className="workspace-nav-item"
+>
+ <a
+ className="workspace-nav-item-link"
+ href="#"
+ onClick={[Function]}
+ >
+ <div
+ id="workspace-nav-item"
+ />
+ </a>
+ <ButtonIcon
+ className="js-close workspace-nav-item-close workspace-header-icon button-small little-spacer-left"
+ color="#fff"
+ onClick={[MockFunction]}
+ >
+ <ClearIcon
+ size={12}
+ />
+ </ButtonIcon>
+</li>
+`;
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavRule-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavRule-test.tsx.snap
new file mode 100644
index 00000000000..2855436401d
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceNavRule-test.tsx.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<WorkspaceNavItem
+ onClose={[Function]}
+ onOpen={[Function]}
+>
+ <WorkspaceRuleTitle
+ limited={true}
+ rule={
+ Object {
+ "key": "foo",
+ "organization": "org",
+ }
+ }
+ />
+</WorkspaceNavItem>
+`;
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap
new file mode 100644
index 00000000000..682c1d5ac77
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleDetails-test.tsx.snap
@@ -0,0 +1,51 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<DeferredSpinner
+ loading={true}
+ timeout={100}
+/>
+`;
+
+exports[`should render 2`] = `
+<DeferredSpinner
+ loading={false}
+ timeout={100}
+>
+ <React.Fragment>
+ <RuleDetailsMeta
+ canWrite={false}
+ hideSimilarRulesFilter={true}
+ onFilterChange={[Function]}
+ onTagsChange={[Function]}
+ organization="org"
+ referencedRepositories={
+ Object {
+ "repo": Object {
+ "key": "repo",
+ "language": "xoo",
+ "name": "Xoo Repository",
+ },
+ }
+ }
+ ruleDetails={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ <RuleDetailsDescription
+ canWrite={false}
+ onChange={[Function]}
+ organization="org"
+ ruleDetails={
+ Object {
+ "key": "foo",
+ "name": "Foo",
+ }
+ }
+ />
+ </React.Fragment>
+</DeferredSpinner>
+`;
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleTitle-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleTitle-test.tsx.snap
new file mode 100644
index 00000000000..5ba5fc924a9
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleTitle-test.tsx.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render loaded rule 1`] = `
+<React.Fragment>
+ <i
+ className="icon-workspace-doc little-spacer-right"
+ />
+ Foo
+</React.Fragment>
+`;
+
+exports[`should render rule 1`] = `
+<React.Fragment>
+ <i
+ className="icon-workspace-doc little-spacer-right"
+ />
+ —
+</React.Fragment>
+`;
diff --git a/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleViewer-test.tsx.snap b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleViewer-test.tsx.snap
new file mode 100644
index 00000000000..f8bc63a9a99
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/__tests__/__snapshots__/WorkspaceRuleViewer-test.tsx.snap
@@ -0,0 +1,38 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<div
+ className="workspace-viewer"
+>
+ <WorkspaceHeader
+ onClose={[Function]}
+ onCollapse={[MockFunction]}
+ onMaximize={[MockFunction]}
+ onMinimize={[MockFunction]}
+ onResize={[MockFunction]}
+ >
+ <WorkspaceRuleTitle
+ rule={
+ Object {
+ "key": "foo",
+ "organization": "org",
+ }
+ }
+ />
+ </WorkspaceHeader>
+ <div
+ className="workspace-viewer-container"
+ style={
+ Object {
+ "height": 300,
+ }
+ }
+ >
+ <WorkspaceRuleDetails
+ onLoad={[Function]}
+ organization="org"
+ ruleKey="foo"
+ />
+ </div>
+</div>
+`;
diff --git a/server/sonar-web/src/main/js/components/workspace/context.ts b/server/sonar-web/src/main/js/components/workspace/context.ts
new file mode 100644
index 00000000000..07dde023200
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/workspace/context.ts
@@ -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 { BranchLike } from '../../app/types';
+
+export interface ComponentDescriptor {
+ branchLike: BranchLike | undefined;
+ key: string;
+ line?: number;
+ name?: string;
+ qualifier?: string;
+}
+
+export interface RuleDescriptor {
+ key: string;
+ name?: string;
+ organization: string;
+}
+
+export interface WorkspaceContext {
+ openComponent: (component: ComponentDescriptor) => void;
+ openRule: (rule: RuleDescriptor) => void;
+}
diff --git a/server/sonar-web/src/main/js/components/workspace/main.js b/server/sonar-web/src/main/js/components/workspace/main.js
deleted file mode 100644
index b2fd7941ecf..00000000000
--- a/server/sonar-web/src/main/js/components/workspace/main.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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.
- */
-// @flow
-import $ from 'jquery';
-import Item from './models/item';
-import Items from './models/items';
-import ItemsView from './views/items-view';
-import ViewerView from './views/viewer-view';
-import RuleView from './views/rule-view';
-import { getRuleDetails } from '../../api/rules';
-import './styles.css';
-import '../../apps/coding-rules/styles.css';
-
-let instance = null;
-const Workspace = function() {
- if (instance != null) {
- throw new Error('Cannot instantiate more than one Workspace, use Workspace.getInstance()');
- }
- this.initialize();
-};
-
-Workspace.prototype = {
- initialize() {
- const that = this;
-
- this.items = new Items();
- this.items.load();
- this.items.on('change', () => {
- that.save();
- });
-
- this.itemsView = new ItemsView({ collection: this.items });
- this.itemsView.render().$el.appendTo(document.body);
- this.itemsView.on('click', model => {
- that.open(model);
- });
- },
-
- save() {
- this.items.save();
- },
-
- addComponent(model) {
- const m = this.items.add2(model);
- this.save();
- return m;
- },
-
- open(options) {
- const model = typeof options.toJSON === 'function' ? options : new Item(options);
- if (!model.isValid()) {
- throw new Error(model.validationError);
- }
- const m = this.addComponent(model);
- if (m.isComponent()) {
- this.showComponentViewer(m);
- }
- if (m.isRule()) {
- this.showRule(m);
- }
- },
-
- openComponent(options) {
- return this.open({ ...options, __type__: 'component' });
- },
-
- openRule(options /*: { key: string, organization: string } */) {
- return this.open({ ...options, __type__: 'rule' });
- },
-
- showViewer(Viewer, model) {
- const that = this;
- if (this.viewerView != null) {
- this.viewerView.model.trigger('hideViewer');
- this.viewerView.destroy();
- }
- $('html').addClass('with-workspace');
- model.trigger('showViewer');
- this.viewerView = new Viewer({ model });
- this.viewerView
- .on('viewerMinimize', () => {
- model.trigger('hideViewer');
- that.closeComponentViewer();
- })
- .on('viewerClose', m => {
- that.closeComponentViewer();
- m.destroy();
- });
- this.viewerView.$el.appendTo(document.body);
- this.viewerView.render();
- },
-
- showComponentViewer(model) {
- this.showViewer(ViewerView, model);
- },
-
- closeComponentViewer() {
- if (this.viewerView != null) {
- this.viewerView.destroy();
- $('.with-workspace').removeClass('with-workspace');
- }
- },
-
- showRule(model) {
- const that = this;
- getRuleDetails({ key: model.get('key') }).then(
- r => {
- model.set({ ...r.rule, exist: true });
- that.showViewer(RuleView, model);
- },
- () => {
- model.set({ exist: false });
- that.showViewer(RuleView, model);
- }
- );
- }
-};
-
-Workspace.getInstance = function() {
- if (instance == null) {
- instance = new Workspace();
- }
- return instance;
-};
-
-export default Workspace.getInstance();
diff --git a/server/sonar-web/src/main/js/components/workspace/models/items.js b/server/sonar-web/src/main/js/components/workspace/models/items.js
deleted file mode 100644
index c191ca98a90..00000000000
--- a/server/sonar-web/src/main/js/components/workspace/models/items.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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 Backbone from 'backbone';
-import Item from './item';
-
-const STORAGE_KEY = 'sonarqube-workspace';
-
-export default Backbone.Collection.extend({
- model: Item,
-
- initialize() {
- this.on('remove', this.save);
- },
-
- save() {
- const dump = JSON.stringify(this.toJSON());
- window.localStorage.setItem(STORAGE_KEY, dump);
- },
-
- load() {
- const dump = window.localStorage.getItem(STORAGE_KEY);
- if (dump != null) {
- try {
- const parsed = JSON.parse(dump);
- this.reset(parsed);
- } catch (err) {
- // fail silently
- }
- }
- },
-
- has(model) {
- const forComponent = model.isComponent() && this.findWhere({ key: model.get('key') }) != null;
- const forRule = model.isRule() && this.findWhere({ key: model.get('key') }) != null;
- return forComponent || forRule;
- },
-
- add2(model) {
- const tryModel = this.findWhere({ key: model.get('key') });
- return tryModel != null ? tryModel : this.add(model);
- }
-});
diff --git a/server/sonar-web/src/main/js/components/workspace/styles.css b/server/sonar-web/src/main/js/components/workspace/styles.css
index 170a4e2c986..229b996cdea 100644
--- a/server/sonar-web/src/main/js/components/workspace/styles.css
+++ b/server/sonar-web/src/main/js/components/workspace/styles.css
@@ -22,7 +22,7 @@
z-index: 451;
bottom: 0;
right: 0;
- height: 30px;
+ height: 28px;
}
.workspace-nav-list {
@@ -30,21 +30,33 @@
}
.workspace-nav-item {
- float: left;
- vertical-align: middle;
- margin-right: 10px;
- padding: 3px 10px;
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ margin-right: var(--gridSize);
+}
+
+.workspace-nav-item-link {
+ display: inline-flex;
+ align-items: center;
+ height: 28px;
+ padding: 0 calc(3.5 * var(--gridSize)) 0 var(--gridSize);
+ border: none;
background-color: var(--gray40);
color: #fff;
font-size: var(--smallFontSize);
- font-weight: 300;
- cursor: pointer;
}
-.workspace-nav-item > i {
- position: relative;
- top: -2px;
- vertical-align: middle;
+.workspace-nav-item-link:hover,
+.workspace-nav-item-link:focus {
+ color: #fff;
+ opacity: 0.9;
+}
+
+.workspace-nav-item-close {
+ position: absolute;
+ right: 4px;
+ top: 4px;
}
.workspace-viewer {
@@ -72,7 +84,7 @@
float: left;
line-height: var(--controlHeight);
color: #fff;
- font-weight: 300;
+ font-weight: 400;
}
.workspace-viewer-name i {
@@ -92,13 +104,17 @@
.workspace-viewer-actions {
float: right;
- line-height: var(--controlHeight);
}
.workspace-viewer-actions a {
color: inherit;
}
+.workspace-header-icon:hover path,
+.workspace-header-icon:focus path {
+ color: var(--gray40);
+}
+
.workspace-viewer-container {
height: calc(40vh - 30px);
min-height: 100px;
diff --git a/server/sonar-web/src/main/js/components/workspace/templates/workspace-item.hbs b/server/sonar-web/src/main/js/components/workspace/templates/workspace-item.hbs
deleted file mode 100644
index a98380cb682..00000000000
--- a/server/sonar-web/src/main/js/components/workspace/templates/workspace-item.hbs
+++ /dev/null
@@ -1,3 +0,0 @@
-{{#if q}}{{qualifierIcon q}}&nbsp;{{/if}}{{#eq type 'rule'}}<i class="icon-workspace-doc"></i>&nbsp;{{/eq}}{{limitString name}}
-
-<button class="js-close button-clean" style="color: #fff;">&times;</button>
diff --git a/server/sonar-web/src/main/js/components/workspace/templates/workspace-items.hbs b/server/sonar-web/src/main/js/components/workspace/templates/workspace-items.hbs
deleted file mode 100644
index 6fe99c04b19..00000000000
--- a/server/sonar-web/src/main/js/components/workspace/templates/workspace-items.hbs
+++ /dev/null
@@ -1 +0,0 @@
-<ul class="workspace-nav-list"></ul>
diff --git a/server/sonar-web/src/main/js/components/workspace/templates/workspace-rule.hbs b/server/sonar-web/src/main/js/components/workspace/templates/workspace-rule.hbs
deleted file mode 100644
index 304daeaca48..00000000000
--- a/server/sonar-web/src/main/js/components/workspace/templates/workspace-rule.hbs
+++ /dev/null
@@ -1,76 +0,0 @@
-<div class="workspace-viewer-header"></div>
-
-<div class="workspace-viewer-container">
-
- {{#if exist}}
-
- <ul class="coding-rules-detail-properties">
- {{#if severity}}
- <li class="coding-rules-detail-property"
- data-toggle="tooltip" data-placement="bottom" title="{{t 'coding_rules.type.tooltip' this.type}}">
- <span class="little-spacer-right">{{issueTypeIcon this.type}}</span>{{issueType this.type}}
- </li>
-
- <li class="coding-rules-detail-property"
- data-toggle="tooltip" data-placement="bottom" title="Default rule severity">
- {{severityIcon severity}}&nbsp;{{t "severity" severity}}
- </li>
- {{/if}}
-
- {{#notEq status 'READY'}}
- <li class="coding-rules-detail-property"
- data-toggle="tooltip" data-placement="bottom" title="Rule status">{{status}}</li>
- {{/notEq}}
-
- <li class="coding-rules-detail-property coding-rules-detail-tag-list {{#if canWrite}}coding-rules-detail-tags-change{{/if}}"
- data-toggle="tooltip" data-placement="bottom" title="Rule tags">
- <i class="icon-tags little-spacer-right"></i>
- <span>{{#if allTags}}{{join allTags ', '}}{{else}}{{t 'coding_rules.no_tags'}}{{/if}}</span>
- </li>
-
- <li class="coding-rules-detail-property">{{t 'coding_rules.available_since'}} {{d createdAt}}</li>
-
- {{#if debtRemFnType}}
- <li class="coding-rules-detail-property"
- data-toggle="tooltip" data-placement="bottom" title="{{t 'coding_rules.remediation_function'}}">
- {{t 'coding_rules.remediation_function' debtRemFnType}}:
-
- {{#if debtRemFnOffset}}{{debtRemFnOffset}}{{/if}}
- {{#if debtRemFnCoeff}}{{#if debtRemFnOffset}}+{{/if}}{{debtRemFnCoeff}}{{/if}}
- {{#if effortToFixDescription}}{{effortToFixDescription}}{{/if}}
- </li>
- {{/if}}
-
- <li class="coding-rules-detail-property spacer-left">
- <a class="link-no-underline" target="_blank" href="{{permalink}}">
- <svg
- xmlns="http://www.w3.org/2000/svg"
- height=14
- width=14
- viewBox="0 0 16 16">
- <path
- fill="currentColor"
- d="M13.501 11.429q0-0.357-0.25-0.607l-1.857-1.857q-0.25-0.25-0.607-0.25-0.375 0-0.643 0.286 0.027 0.027 0.17 0.165t0.192 0.192 0.134 0.17 0.116 0.228 0.031 0.246q0 0.357-0.25 0.607t-0.607 0.25q-0.134 0-0.246-0.031t-0.228-0.116-0.17-0.134-0.192-0.192-0.165-0.17q-0.295 0.277-0.295 0.652 0 0.357 0.25 0.607l1.839 1.848q0.241 0.241 0.607 0.241 0.357 0 0.607-0.232l1.313-1.304q0.25-0.25 0.25-0.598zM7.224 5.134q0-0.357-0.25-0.607l-1.839-1.848q-0.25-0.25-0.607-0.25-0.348 0-0.607 0.241l-1.313 1.304q-0.25 0.25-0.25 0.598 0 0.357 0.25 0.607l1.857 1.857q0.241 0.241 0.607 0.241 0.375 0 0.643-0.277-0.027-0.027-0.17-0.165t-0.192-0.192-0.134-0.17-0.116-0.228-0.031-0.246q0-0.357 0.25-0.607t0.607-0.25q0.134 0 0.246 0.031t0.228 0.116 0.17 0.134 0.192 0.192 0.165 0.17q0.295-0.277 0.295-0.652zM15.215 11.429q0 1.071-0.759 1.813l-1.313 1.304q-0.741 0.741-1.813 0.741-1.080 0-1.821-0.759l-1.839-1.848q-0.741-0.741-0.741-1.813 0-1.098 0.786-1.866l-0.786-0.786q-0.768 0.786-1.857 0.786-1.071 0-1.821-0.75l-1.857-1.857q-0.75-0.75-0.75-1.821t0.759-1.813l1.313-1.304q0.741-0.741 1.813-0.741 1.080 0 1.821 0.759l1.839 1.848q0.741 0.741 0.741 1.813 0 1.098-0.786 1.866l0.786 0.786q0.768-0.786 1.857-0.786 1.071 0 1.821 0.75l1.857 1.857q0.75 0.75 0.75 1.821z"
- />
- </svg>
- </a>
- <span class="note little-spacer-left">{{key}}</span>
- </li>
- </ul>
-
- <div class="coding-rules-detail-description rule-desc markdown">{{{htmlDesc}}}</div>
-
- {{#if htmlNote}}
- <div id="coding-rules-detail-description-extra">
- <div class="rule-desc markdown">{{{htmlNote}}}</div>
- </div>
- {{/if}}
-
- {{else}}
-
- {{! does not exist}}
- <div class="alert alert-warning">{{t 'workspace.no_rule'}}</div>
-
- {{/if}}
-
-</div>
diff --git a/server/sonar-web/src/main/js/components/workspace/templates/workspace-viewer-header.hbs b/server/sonar-web/src/main/js/components/workspace/templates/workspace-viewer-header.hbs
deleted file mode 100644
index 409dcd3a6cf..00000000000
--- a/server/sonar-web/src/main/js/components/workspace/templates/workspace-viewer-header.hbs
+++ /dev/null
@@ -1,17 +0,0 @@
-<h6 class="workspace-viewer-name">{{#if q}}{{qualifierIcon q}}&nbsp;{{/if}}{{#eq type 'rule'}}<i class="icon-workspace-doc"></i>&nbsp;{{/eq}}{{name}}</h6>
-
-<div class="workspace-viewer-resize js-resize"></div>
-
-<div class="workspace-viewer-actions">
- <a href="#" class="js-minimize icon-minimize spacer-right"
- title="{{t 'workspace.minimize'}}" data-placement="bottom" data-toggle="tooltip"></a>
-
- <a href="#" class="js-full-screen icon-bigger-size spacer-right"
- title="{{t 'workspace.full_window'}}" data-placement="bottom" data-toggle="tooltip"></a>
-
- <a href="#" class="js-normal-size icon-smaller-size spacer-right"
- title="{{t 'workspace.normal_size'}}" data-placement="bottom" data-toggle="tooltip"></a>
-
- <a href="#" class="js-close icon-close"
- title="{{t 'workspace.close'}}" data-placement="bottom" data-toggle="tooltip"></a>
-</div>
diff --git a/server/sonar-web/src/main/js/components/workspace/templates/workspace-viewer.hbs b/server/sonar-web/src/main/js/components/workspace/templates/workspace-viewer.hbs
deleted file mode 100644
index 45515fbecb0..00000000000
--- a/server/sonar-web/src/main/js/components/workspace/templates/workspace-viewer.hbs
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="workspace-viewer-header"></div>
-
-<div class="workspace-viewer-container"></div>
diff --git a/server/sonar-web/src/main/js/components/workspace/views/rule-view.js b/server/sonar-web/src/main/js/components/workspace/views/rule-view.js
deleted file mode 100644
index 88c2dbb5b8f..00000000000
--- a/server/sonar-web/src/main/js/components/workspace/views/rule-view.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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 { union } from 'lodash';
-import Marionette from 'backbone.marionette';
-import BaseView from './base-viewer-view';
-import Template from '../templates/workspace-rule.hbs';
-import { getPathUrlAsString, getRulesUrl } from '../../../helpers/urls';
-import { areThereCustomOrganizations } from '../../../store/organizations/utils';
-
-export default BaseView.extend({
- template: Template,
-
- onRender() {
- BaseView.prototype.onRender.apply(this, arguments);
- this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
- },
-
- onDestroy() {
- this.$('[data-toggle="tooltip"]').tooltip('destroy');
- },
-
- serializeData() {
- const query = { rule_key: this.model.get('key') };
- const permalink = getPathUrlAsString(
- areThereCustomOrganizations()
- ? getRulesUrl(query, this.model.get('organization'))
- : getRulesUrl(query, undefined)
- );
-
- return {
- ...Marionette.LayoutView.prototype.serializeData.apply(this, arguments),
- allTags: union(this.model.get('sysTags'), this.model.get('tags')),
- permalink
- };
- }
-});
diff --git a/server/sonar-web/src/main/js/components/workspace/views/viewer-header-view.js b/server/sonar-web/src/main/js/components/workspace/views/viewer-header-view.js
deleted file mode 100644
index 756ede70c71..00000000000
--- a/server/sonar-web/src/main/js/components/workspace/views/viewer-header-view.js
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import Marionette from 'backbone.marionette';
-import Template from '../templates/workspace-viewer-header.hbs';
-
-export default Marionette.ItemView.extend({
- template: Template,
-
- modelEvents: {
- change: 'render'
- },
-
- events: {
- 'mousedown .js-resize': 'onResizeClick',
-
- 'click .js-minimize': 'onMinimizeClick',
- 'click .js-full-screen': 'onFullScreenClick',
- 'click .js-normal-size': 'onNormalSizeClick',
- 'click .js-close': 'onCloseClick'
- },
-
- onRender() {
- this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
- this.$('.js-normal-size').addClass('hidden');
- },
-
- onDestroy() {
- this.$('[data-toggle="tooltip"]').tooltip('destroy');
- $('.tooltip').remove();
- },
-
- onResizeClick(e) {
- e.preventDefault();
- this.startResizing(e);
- },
-
- onMinimizeClick(e) {
- e.preventDefault();
- this.trigger('viewerMinimize');
- },
-
- onFullScreenClick(e) {
- e.preventDefault();
- this.toFullScreen();
- },
-
- onNormalSizeClick(e) {
- e.preventDefault();
- this.toNormalSize();
- },
-
- onCloseClick(e) {
- e.preventDefault();
- this.trigger('viewerClose');
- },
-
- startResizing(e) {
- this.initialResizePosition = e.clientY;
- this.initialResizeHeight = $('.workspace-viewer-container').height();
- const processResizing = this.processResizing.bind(this);
- const stopResizing = this.stopResizing.bind(this);
- $('body')
- .on('mousemove.workspace', processResizing)
- .on('mouseup.workspace', stopResizing);
- },
-
- processResizing(e) {
- const currentResizePosition = e.clientY;
- const resizeDelta = this.initialResizePosition - currentResizePosition;
- const height = this.initialResizeHeight + resizeDelta;
- $('.workspace-viewer-container').height(height);
- },
-
- stopResizing() {
- $('body')
- .off('mousemove.workspace')
- .off('mouseup.workspace');
- },
-
- toFullScreen() {
- this.$('.js-normal-size').removeClass('hidden');
- this.$('.js-full-screen').addClass('hidden');
- this.initialResizeHeight = $('.workspace-viewer-container').height();
- $('.workspace-viewer-container').height('9999px');
- },
-
- toNormalSize() {
- this.$('.js-normal-size').addClass('hidden');
- this.$('.js-full-screen').removeClass('hidden');
- $('.workspace-viewer-container').height(this.initialResizeHeight);
- }
-});
diff --git a/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js b/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
deleted file mode 100644
index eafcac4460e..00000000000
--- a/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * 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 $ from 'jquery';
-import React from 'react';
-import { render } from 'react-dom';
-import BaseView from './base-viewer-view';
-import SourceViewer from '../../SourceViewer/SourceViewer';
-import Template from '../templates/workspace-viewer.hbs';
-import WithStore from '../../shared/WithStore';
-
-export default BaseView.extend({
- template: Template,
-
- onRender() {
- BaseView.prototype.onRender.apply(this, arguments);
- this.showViewer();
- },
-
- scrollToLine(line) {
- const row = this.$el.find(`.source-line[data-line-number="${line}"]`);
- if (row.length > 0) {
- const sourceViewer = this.$el.find('.source-viewer');
- let p = sourceViewer.scrollParent();
- if (p.is(document) || p.is('body')) {
- p = $(window);
- }
- const pTopOffset = p.offset() != null ? p.offset().top : 0;
- const pHeight = p.height();
- const goal = row.offset().top - pHeight / 3 - pTopOffset;
- p.scrollTop(goal);
- }
- },
-
- showViewer() {
- const { branchLike, key, line } = this.model.toJSON();
-
- const el = document.querySelector(this.viewerRegion.el);
-
- render(
- <WithStore>
- <SourceViewer
- aroundLine={line}
- branchLike={branchLike}
- component={key}
- fromWorkspace={true}
- highlightedLine={line}
- onLoaded={component => {
- this.model.set({ name: component.name, q: component.q });
- if (line) {
- this.scrollToLine(line);
- }
- }}
- />
- </WithStore>,
- el
- );
- }
-});