diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-03-14 12:47:17 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-14 12:47:17 +0100 |
commit | 6f189a7c95ee207e02c7c9321ed37d0c6ca4afe6 (patch) | |
tree | 494c72690c0ac9607716574a5bfa49f9c4ca315a /server | |
parent | 47d50b8a9c17d595a07b6a7e157849ccf1ffc302 (diff) | |
download | sonarqube-6f189a7c95ee207e02c7c9321ed37d0c6ca4afe6.tar.gz sonarqube-6f189a7c95ee207e02c7c9321ed37d0c6ca4afe6.zip |
rewrite workspace in react (#3140)
Diffstat (limited to 'server')
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}} {{/if}}{{#eq type 'rule'}}<i class="icon-workspace-doc"></i> {{/eq}}{{limitString name}} - -<button class="js-close button-clean" style="color: #fff;">×</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}} {{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}} {{/if}}{{#eq type 'rule'}}<i class="icon-workspace-doc"></i> {{/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 - ); - } -}); |