return getJSON('/api/qualityprofiles/changelog', data);
}
-export function compareProfiles(leftKey: string, rightKey: string): Promise<any> {
+export interface CompareResponse {
+ left: { name: string };
+ right: { name: string };
+ inLeft: Array<{ key: string; name: string; severity: string }>;
+ inRight: Array<{ key: string; name: string; severity: string }>;
+ modified: Array<{
+ key: string;
+ name: string;
+ left: { params: { [p: string]: string }; severity: string };
+ right: { params: { [p: string]: string }; severity: string };
+ }>;
+}
+
+export function compareProfiles(leftKey: string, rightKey: string): Promise<CompareResponse> {
return getJSON('/api/qualityprofiles/compare', { leftKey, rightKey });
}
organization: string | undefined;
profiles: BaseProfile[];
rule: T.Rule | T.RuleDetails;
- updateMode?: boolean;
}
interface State {
}
export default class ActivationButton extends React.PureComponent<Props, State> {
- mounted = false;
state: State = { modal: false };
- componentDidMount() {
- this.mounted = true;
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
handleButtonClick = () => {
this.setState({ modal: true });
};
organization={this.props.organization}
profiles={this.props.profiles}
rule={this.props.rule}
- updateMode={this.props.updateMode}
/>
)}
</>
organization: string | undefined;
profiles: BaseProfile[];
rule: T.Rule | T.RuleDetails;
- updateMode?: boolean;
}
interface State {
import { withRouter, WithRouterProps } from 'react-router';
import ComparisonForm from './ComparisonForm';
import ComparisonResults from './ComparisonResults';
-import { compareProfiles } from '../../../api/quality-profiles';
+import { compareProfiles, CompareResponse } from '../../../api/quality-profiles';
import { getProfileComparePath } from '../utils';
import { Profile } from '../types';
interface Props extends WithRouterProps {
- organization: string | null;
+ organization?: string;
profile: Profile;
profiles: Profile[];
}
-type Params = { [p: string]: string };
-
-interface State {
- loading: boolean;
- left?: { name: string };
- right?: { name: string };
- inLeft?: Array<{ key: string; name: string; severity: string }>;
- inRight?: Array<{ key: string; name: string; severity: string }>;
- modified?: Array<{
- key: string;
- name: string;
- left: { params: Params; severity: string };
- right: { params: Params; severity: string };
- }>;
-}
+type State = { loading: boolean } & Partial<CompareResponse>;
+type StateWithResults = { loading: boolean } & CompareResponse;
class ComparisonContainer extends React.PureComponent<Props, State> {
mounted = false;
this.mounted = false;
}
- loadResults() {
+ loadResults = () => {
const { withKey } = this.props.location.query;
if (!withKey) {
this.setState({ left: undefined, loading: false });
- return;
+ return Promise.resolve();
}
this.setState({ loading: true });
- compareProfiles(this.props.profile.key, withKey).then((r: any) => {
- if (this.mounted) {
- this.setState({
- left: r.left,
- right: r.right,
- inLeft: r.inLeft,
- inRight: r.inRight,
- modified: r.modified,
- loading: false
- });
+ return compareProfiles(this.props.profile.key, withKey).then(
+ ({ left, right, inLeft, inRight, modified }) => {
+ if (this.mounted) {
+ this.setState({ left, right, inLeft, inRight, modified, loading: false });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
}
- });
- }
+ );
+ };
handleCompare = (withKey: string) => {
const path = getProfileComparePath(
this.props.router.push(path);
};
+ hasResults(state: State): state is StateWithResults {
+ return state.left !== undefined;
+ }
+
render() {
const { profile, profiles, location } = this.props;
const { withKey } = location.query;
- const { left, right, inLeft, inRight, modified } = this.state;
return (
<div className="boxed-group boxed-group-inner js-profile-comparison">
{this.state.loading && <i className="spinner spacer-left" />}
</header>
- {left != null &&
- inLeft != null &&
- right != null &&
- inRight != null &&
- modified != null && (
- <div className="spacer-top">
- <ComparisonResults
- inLeft={inLeft}
- inRight={inRight}
- left={left}
- modified={modified}
- organization={this.props.organization}
- right={right}
- />
- </div>
- )}
+ {this.hasResults(this.state) && (
+ <div className="spacer-top">
+ <ComparisonResults
+ inLeft={this.state.inLeft}
+ inRight={this.state.inRight}
+ left={this.state.left}
+ leftProfile={profile}
+ modified={this.state.modified}
+ organization={this.props.organization}
+ refresh={this.loadResults}
+ right={this.state.right}
+ rightProfile={profiles.find(p => p.key === withKey)}
+ />
+ </div>
+ )}
</div>
);
}
}
export default class ComparisonForm extends React.PureComponent<Props> {
- handleChange(option: { value: string }) {
+ handleChange = (option: { value: string }) => {
this.props.onCompare(option.value);
- }
+ };
render() {
const { profile, profiles, withKey } = this.props;
<Select
className="input-large"
clearable={false}
- onChange={this.handleChange.bind(this)}
+ onChange={this.handleChange}
options={options}
placeholder={translate('select_verb')}
value={withKey}
--- /dev/null
+/*
+ * 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 { Profile } from '../../../api/quality-profiles';
+import { lazyLoad } from '../../../components/lazyLoad';
+import { Button } from '../../../components/ui/buttons';
+import { translate } from '../../../helpers/l10n';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { getRuleDetails } from '../../../api/rules';
+
+const ActivationFormModal = lazyLoad(
+ () => import('../../coding-rules/components/ActivationFormModal'),
+ 'ActivationFormModal'
+);
+
+interface Props {
+ onDone: () => Promise<void>;
+ organization?: string;
+ profile: Profile;
+ ruleKey: string;
+}
+
+interface State {
+ rule?: T.RuleDetails;
+ state: 'closed' | 'opening' | 'open';
+}
+
+export default class ComparisonResultActivation extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { state: 'closed' };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ handleButtonClick = () => {
+ this.setState({ state: 'opening' });
+ getRuleDetails({ key: this.props.ruleKey, organization: this.props.organization }).then(
+ ({ rule }) => {
+ if (this.mounted) {
+ this.setState({ rule, state: 'open' });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ state: 'closed' });
+ }
+ }
+ );
+ };
+
+ handleCloseModal = () => {
+ this.setState({ state: 'closed' });
+ };
+
+ isOpen(state: State): state is { state: 'open'; rule: T.RuleDetails } {
+ return state.state === 'open';
+ }
+
+ render() {
+ const { profile } = this.props;
+
+ const canActivate = !profile.isBuiltIn && profile.actions && profile.actions.edit;
+ if (!canActivate) {
+ return null;
+ }
+
+ return (
+ <DeferredSpinner loading={this.state.state === 'opening'}>
+ <Button disabled={this.state.state !== 'closed'} onClick={this.handleButtonClick}>
+ {this.props.children}
+ </Button>
+
+ {this.isOpen(this.state) && (
+ <ActivationFormModal
+ modalHeader={translate('coding_rules.activate_in_quality_profile')}
+ onClose={this.handleCloseModal}
+ onDone={this.props.onDone}
+ organization={this.props.organization}
+ profiles={[profile]}
+ rule={this.state.rule}
+ />
+ )}
+ </DeferredSpinner>
+ );
+ }
+}
import * as React from 'react';
import { Link } from 'react-router';
import ComparisonEmpty from './ComparisonEmpty';
+import ComparisonResultActivation from './ComparisonResultActivation';
import SeverityIcon from '../../../components/icons-components/SeverityIcon';
import { translateWithParameters } from '../../../helpers/l10n';
import { getRulesUrl } from '../../../helpers/urls';
+import { CompareResponse, Profile } from '../../../api/quality-profiles';
+import ChevronRightIcon from '../../../components/icons-components/ChevronRightcon';
+import ChevronLeftIcon from '../../../components/icons-components/ChevronLeftIcon';
type Params = { [p: string]: string };
-interface Props {
- left: { name: string };
- right: { name: string };
- inLeft: Array<{ key: string; name: string; severity: string }>;
- inRight: Array<{ key: string; name: string; severity: string }>;
- modified: Array<{
- key: string;
- name: string;
- left: { params: Params; severity: string };
- right: { params: Params; severity: string };
- }>;
- organization: string | null;
+interface Props extends CompareResponse {
+ organization?: string;
+ leftProfile: Profile;
+ refresh: () => Promise<void>;
+ rightProfile?: Profile;
}
export default class ComparisonResults extends React.PureComponent<Props> {
return (
<div>
<SeverityIcon severity={severity} />{' '}
- <Link to={getRulesUrl({ rule_key: rule.key }, this.props.organization)}>{rule.name}</Link>
+ <Link to={getRulesUrl({ rule_key: rule.key, open: rule.key }, this.props.organization)}>
+ {rule.name}
+ </Link>
</div>
);
}
{this.props.inLeft.map(rule => (
<tr className="js-comparison-in-left" key={`left-${rule.key}`}>
<td>{this.renderRule(rule, rule.severity)}</td>
- <td> </td>
+ <td>
+ {this.props.rightProfile && (
+ <ComparisonResultActivation
+ key={rule.key}
+ onDone={this.props.refresh}
+ organization={this.props.organization || undefined}
+ profile={this.props.rightProfile}
+ ruleKey={rule.key}>
+ <ChevronRightIcon />
+ </ComparisonResultActivation>
+ )}
+ </td>
</tr>
))}
</>
</tr>
{this.props.inRight.map(rule => (
<tr className="js-comparison-in-right" key={`right-${rule.key}`}>
- <td> </td>
+ <td className="text-right">
+ <ComparisonResultActivation
+ key={rule.key}
+ onDone={this.props.refresh}
+ organization={this.props.organization || undefined}
+ profile={this.props.leftProfile}
+ ruleKey={rule.key}>
+ <ChevronLeftIcon />
+ </ComparisonResultActivation>
+ </td>
<td>{this.renderRule(rule, rule.severity)}</td>
</tr>
))}
--- /dev/null
+/*
+ * 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 ComparisonResultActivation from '../ComparisonResultActivation';
+import { Profile } from '../../../../api/quality-profiles';
+import { click, waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/rules', () => ({
+ getRuleDetails: jest.fn().mockResolvedValue({ key: 'foo' })
+}));
+
+it('should activate', async () => {
+ const profile = { actions: { edit: true }, key: 'profile-key' } as Profile;
+ const wrapper = shallow(
+ <ComparisonResultActivation onDone={jest.fn()} profile={profile} ruleKey="foo" />
+ );
+ expect(wrapper).toMatchSnapshot();
+
+ click(wrapper.find('Button'));
+ expect(wrapper).toMatchSnapshot();
+
+ await waitAndUpdate(wrapper);
+ expect(wrapper).toMatchSnapshot();
+
+ wrapper.find('ActivationFormModal').prop<Function>('onClose')();
+ expect(wrapper).toMatchSnapshot();
+});
import { Link } from 'react-router';
import ComparisonResults from '../ComparisonResults';
import ComparisonEmpty from '../ComparisonEmpty';
+import { Profile } from '../../../../api/quality-profiles';
it('should render ComparisonEmpty', () => {
const output = shallow(
inLeft={[]}
inRight={[]}
left={{ name: 'left' }}
+ leftProfile={{} as Profile}
modified={[]}
- organization={null}
+ refresh={jest.fn()}
right={{ name: 'right' }}
/>
);
inLeft={inLeft}
inRight={inRight}
left={{ name: 'left' }}
+ leftProfile={{} as Profile}
modified={modified}
- organization={null}
+ refresh={jest.fn()}
right={{ name: 'right' }}
/>
);
const leftDiffs = output.find('.js-comparison-in-left');
expect(leftDiffs.length).toBe(1);
expect(leftDiffs.find(Link).length).toBe(1);
- expect(leftDiffs.find(Link).prop('to')).toHaveProperty('query', { rule_key: 'rule1' });
+ expect(leftDiffs.find(Link).prop('to')).toHaveProperty('query', {
+ rule_key: 'rule1',
+ open: 'rule1'
+ });
expect(leftDiffs.find(Link).prop('children')).toContain('rule1');
expect(leftDiffs.find('SeverityIcon').length).toBe(1);
expect(leftDiffs.find('SeverityIcon').prop('severity')).toBe('BLOCKER');
.at(0)
.find(Link)
.prop('to')
- ).toHaveProperty('query', { rule_key: 'rule2' });
+ ).toHaveProperty('query', { rule_key: 'rule2', open: 'rule2' });
expect(
rightDiffs
.at(0)
.find(Link)
.at(0)
.prop('to')
- ).toHaveProperty('query', { rule_key: 'rule4' });
+ ).toHaveProperty('query', { rule_key: 'rule4', open: 'rule4' });
expect(
modifiedDiffs
.find(Link)
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should activate 1`] = `
+<DeferredSpinner
+ loading={false}
+ timeout={100}
+>
+ <Button
+ disabled={false}
+ onClick={[Function]}
+ />
+</DeferredSpinner>
+`;
+
+exports[`should activate 2`] = `
+<DeferredSpinner
+ loading={true}
+ timeout={100}
+>
+ <Button
+ disabled={true}
+ onClick={[Function]}
+ />
+</DeferredSpinner>
+`;
+
+exports[`should activate 3`] = `
+<DeferredSpinner
+ loading={false}
+ timeout={100}
+>
+ <Button
+ disabled={true}
+ onClick={[Function]}
+ />
+ <ActivationFormModal
+ modalHeader="coding_rules.activate_in_quality_profile"
+ onClose={[Function]}
+ onDone={[MockFunction]}
+ profiles={
+ Array [
+ Object {
+ "actions": Object {
+ "edit": true,
+ },
+ "key": "profile-key",
+ },
+ ]
+ }
+ />
+</DeferredSpinner>
+`;
+
+exports[`should activate 4`] = `
+<DeferredSpinner
+ loading={false}
+ timeout={100}
+>
+ <Button
+ disabled={false}
+ onClick={[Function]}
+ />
+</DeferredSpinner>
+`;