瀏覽代碼

SONAR-11477 Drop UI for updating module keys (#958)

tags/7.6
Stas Vilchik 5 年之前
父節點
當前提交
a0acec09a7
共有 24 個文件被更改,包括 299 次插入927 次删除
  1. 1
    0
      server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkUpdateKeyAction.java
  2. 2
    21
      server/sonar-web/src/main/js/api/components.ts
  3. 1
    1
      server/sonar-web/src/main/js/app/utils/startReactApp.tsx
  4. 0
    148
      server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js
  5. 0
    71
      server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js
  6. 0
    94
      server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js
  7. 0
    46
      server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js
  8. 0
    156
      server/sonar-web/src/main/js/apps/project-admin/key/Key.js
  9. 0
    65
      server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyConfirm.tsx
  10. 0
    75
      server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.tsx
  11. 0
    24
      server/sonar-web/src/main/js/apps/project-admin/key/utils.js
  12. 0
    51
      server/sonar-web/src/main/js/apps/project-admin/store/actions.js
  13. 0
    45
      server/sonar-web/src/main/js/apps/project-admin/store/components.js
  14. 0
    49
      server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js
  15. 0
    40
      server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js
  16. 55
    0
      server/sonar-web/src/main/js/apps/projectKey/Key.tsx
  17. 26
    7
      server/sonar-web/src/main/js/apps/projectKey/UpdateForm.tsx
  18. 42
    0
      server/sonar-web/src/main/js/apps/projectKey/__tests__/Key-test.tsx
  19. 19
    8
      server/sonar-web/src/main/js/apps/projectKey/__tests__/UpdateForm-test.tsx
  20. 37
    0
      server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/Key-test.tsx.snap
  21. 112
    0
      server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/UpdateForm-test.tsx.snap
  22. 1
    1
      server/sonar-web/src/main/js/components/SelectList/SelectListListContainer.tsx
  23. 0
    7
      server/sonar-web/src/main/js/store/rootReducer.ts
  24. 3
    18
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 1
- 0
server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkUpdateKeyAction.java 查看文件

@@ -92,6 +92,7 @@ public class BulkUpdateKeyAction implements ProjectsWsAction {
PARAM_DRY_RUN,
PARAM_PROJECT, PARAM_FROM, PARAM_TO,
PARAM_PROJECT_ID, PARAM_PROJECT)
.setDeprecatedSince("7.6")
.setSince("6.1")
.setPost(true)
.setResponseExample(getClass().getResource("bulk_update_key-example.json"))

+ 2
- 21
server/sonar-web/src/main/js/api/components.ts 查看文件

@@ -222,27 +222,8 @@ export function searchComponents(data?: {
return getJSON('/api/components/search', data);
}

/**
* Change component's key
*/
export function changeKey(from: string, to: string): Promise<void> {
const url = '/api/projects/update_key';
const data = { from, to };
return post(url, data);
}

/**
* Bulk change component's key
*/
export function bulkChangeKey(
project: string,
from: string,
to: string,
dryRun: boolean = false
): Promise<any> {
const url = '/api/projects/bulk_update_key';
const data = { project, from, to, dryRun };
return postJSON(url, data);
export function changeKey(data: { from: string; to: string }) {
return post('/api/projects/update_key', data).catch(throwGlobalError);
}

export interface SuggestionsResponse {

+ 1
- 1
server/sonar-web/src/main/js/app/utils/startReactApp.tsx 查看文件

@@ -268,7 +268,7 @@ export default function startReactApp(
/>
<Route
path="project/key"
component={lazyLoad(() => import('../../apps/project-admin/key/Key'))}
component={lazyLoad(() => import('../../apps/projectKey/Key'))}
/>
</Route>
</Route>

+ 0
- 148
server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdate.js 查看文件

@@ -1,148 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import BulkUpdateForm from './BulkUpdateForm';
import BulkUpdateResults from './BulkUpdateResults';
import { reloadUpdateKeyPage } from './utils';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { bulkChangeKey } from '../../../api/components';
import { parseError } from '../../../helpers/request';
import {
addGlobalErrorMessage,
addGlobalSuccessMessage,
closeAllGlobalMessages
} from '../../../store/globalMessages';
import RecentHistory from '../../../app/components/RecentHistory';

class BulkUpdate extends React.PureComponent {
static propTypes = {
component: PropTypes.object.isRequired,
addGlobalErrorMessage: PropTypes.func.isRequired,
addGlobalSuccessMessage: PropTypes.func.isRequired,
closeAllGlobalMessages: PropTypes.func.isRequired
};

state = {
updating: false,
updated: false,
newComponentKey: null
};

handleSubmit(replace, by) {
this.loadResults(replace, by);
}

handleConfirm() {
this.setState({ updating: true });

const { component } = this.props;
const { replace, by } = this.state;

bulkChangeKey(component.key, replace, by)
.then(r => {
const result = r.keys.find(result => result.key === component.key);
const newComponentKey = result != null ? result.newKey : component.key;

if (newComponentKey !== component.key) {
RecentHistory.remove(component.key);
}

this.props.addGlobalSuccessMessage(translate('update_key.key_updated.reload'));
this.setState({ updating: false });
reloadUpdateKeyPage(newComponentKey);
})
.catch(e => {
this.setState({ updating: false });
parseError(e).then(message => this.props.addGlobalErrorMessage(message));
});
}

loadResults(replace, by) {
const { component } = this.props;
bulkChangeKey(component.key, replace, by, true)
.then(r => {
this.setState({ results: r.keys, replace, by });
this.props.closeAllGlobalMessages();
})
.catch(e => {
this.setState({ results: null });
parseError(e).then(message => this.props.addGlobalErrorMessage(message));
});
}

renderUpdating() {
return (
<div id="project-key-bulk-update">
<i className="spinner" />
</div>
);
}

render() {
const { component } = this.props;
const { updating, updated } = this.state;
const { results, replace, by } = this.state;

if (updating) {
return this.renderUpdating();
}

if (updated) {
return this.renderUpdated();
}

return (
<div id="project-key-bulk-update">
<header className="big-spacer-bottom">
<div className="spacer-bottom">{translate('update_key.bulk_change_description')}</div>
<div>
{translateWithParameters(
'update_key.current_key_for_project_x_is_x',
component.name,
component.key
)}
</div>
</header>

<BulkUpdateForm onSubmit={this.handleSubmit.bind(this)} />

{results != null && (
<BulkUpdateResults
by={by}
onConfirm={this.handleConfirm.bind(this)}
replace={replace}
results={results}
/>
)}
</div>
);
}
}

export default connect(
null,
{
addGlobalErrorMessage,
addGlobalSuccessMessage,
closeAllGlobalMessages
}
)(BulkUpdate);

+ 0
- 71
server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateForm.js 查看文件

@@ -1,71 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 React from 'react';
import PropTypes from 'prop-types';
import { translate } from '../../../helpers/l10n';
import { SubmitButton } from '../../../components/ui/buttons';

export default class BulkUpdateForm extends React.PureComponent {
static propTypes = {
onSubmit: PropTypes.func.isRequired
};

handleSubmit = e => {
e.preventDefault();

const replace = this.refs.replace.value;
const by = this.refs.by.value;

this.props.onSubmit(replace, by);
};

render() {
return (
<form onSubmit={this.handleSubmit}>
<div className="modal-field">
<label htmlFor="bulk-update-replace">{translate('update_key.replace')}</label>
<input
id="bulk-update-replace"
name="replace"
placeholder={translate('update_key.replace_example')}
ref="replace"
required={true}
type="text"
/>
</div>

<div className="modal-field">
<label htmlFor="bulk-update-by">{translate('update_key.by')}</label>
<input
id="bulk-update-by"
name="by"
placeholder={translate('update_key.by_example')}
ref="by"
required={true}
type="text"
/>
<SubmitButton className="big-spacer-left" id="bulk-update-see-results">
{translate('update_key.see_results')}
</SubmitButton>
</div>
</form>
);
}
}

+ 0
- 94
server/sonar-web/src/main/js/apps/project-admin/key/BulkUpdateResults.js 查看文件

@@ -1,94 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 React from 'react';
import PropTypes from 'prop-types';
import { some } from 'lodash';
import { translateWithParameters, translate } from '../../../helpers/l10n';
import { Button } from '../../../components/ui/buttons';

export default class BulkUpdateResults extends React.PureComponent {
static propTypes = {
results: PropTypes.array.isRequired,
onConfirm: PropTypes.func.isRequired
};

render() {
const { results, replace, by } = this.props;
const isEmpty = results.length === 0;
const hasDuplications = some(results, r => r.duplicate);
const canUpdate = !isEmpty && !hasDuplications;

return (
<div className="big-spacer-top" id="bulk-update-simulation">
{isEmpty && (
<div className="spacer-bottom" id="bulk-update-nothing">
{translateWithParameters('update_key.no_key_to_update', replace)}
</div>
)}

{hasDuplications && (
<div className="spacer-bottom" id="bulk-update-duplicate">
{translateWithParameters('update_key.cant_update_because_duplicate_keys', replace, by)}
</div>
)}

{canUpdate && (
<div className="spacer-bottom">
{translate('update_key.keys_will_be_updated_as_follows')}
</div>
)}

{!isEmpty && (
<table className="data zebra zebra-hover" id="bulk-update-results">
<thead>
<tr>
<th>{translate('update_key.old_key')}</th>
<th>{translate('update_key.new_key')}</th>
</tr>
</thead>
<tbody>
{results.map(result => (
<tr data-key={result.key} key={result.key}>
<td className="js-old-key">{result.key}</td>
<td className="js-new-key">
{result.duplicate && (
<span className="spacer-right badge badge-danger">
{translate('update_key.duplicate_key')}
</span>
)}
{result.newKey}
</td>
</tr>
))}
</tbody>
</table>
)}

<div className="big-spacer-top">
{canUpdate && (
<Button id="bulk-update-confirm" onClick={this.props.onConfirm}>
{translate('update_verb')}
</Button>
)}
</div>
</div>
);
}
}

+ 0
- 46
server/sonar-web/src/main/js/apps/project-admin/key/FineGrainedUpdate.js 查看文件

@@ -1,46 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 React from 'react';
import UpdateKeyForm from './UpdateKeyForm';
import QualifierIcon from '../../../components/icons-components/QualifierIcon';

export default function FineGrainedUpdate(props) {
const { component, modules } = props;
const components = [component, ...modules];

return (
<div id="project-key-fine-grained-update">
<table className="data zebra">
<tbody>
{components.map(component => (
<tr key={component.key}>
<td className="width-40">
<QualifierIcon qualifier={component.qualifier} /> {component.name}
</td>
<td>
<UpdateKeyForm component={component} onKeyChange={props.onKeyChange} />
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

+ 0
- 156
server/sonar-web/src/main/js/apps/project-admin/key/Key.js 查看文件

@@ -1,156 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 React from 'react';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';
import Header from './Header';
import UpdateForm from './UpdateForm';
import BulkUpdate from './BulkUpdate';
import FineGrainedUpdate from './FineGrainedUpdate';
import { reloadUpdateKeyPage } from './utils';
import { changeKey, fetchProjectModules } from '../store/actions';
import { translate } from '../../../helpers/l10n';
import {
addGlobalErrorMessage,
addGlobalSuccessMessage,
closeAllGlobalMessages
} from '../../../store/globalMessages';
import RecentHistory from '../../../app/components/RecentHistory';
import { getProjectAdminProjectModules } from '../../../store/rootReducer';

class Key extends React.PureComponent {
static propTypes = {
component: PropTypes.object,
fetchProjectModules: PropTypes.func.isRequired,
changeKey: PropTypes.func.isRequired,
addGlobalErrorMessage: PropTypes.func.isRequired,
addGlobalSuccessMessage: PropTypes.func.isRequired,
closeAllGlobalMessages: PropTypes.func.isRequired
};

state = {
tab: 'bulk'
};

componentDidMount() {
this.props.fetchProjectModules(this.props.component.key);
}

handleChangeKey = (key, newKey) => {
return this.props.changeKey(key, newKey).then(() => {
if (key === this.props.component.key) {
this.props.addGlobalSuccessMessage(translate('update_key.key_updated.reload'));
RecentHistory.remove(key);
reloadUpdateKeyPage(newKey);
} else {
this.props.addGlobalSuccessMessage(translate('update_key.key_updated'));
}
});
};

handleChangeTab = event => {
event.preventDefault();
event.currentTarget.blur();
const { tab } = event.currentTarget.dataset;
this.setState({ tab });
this.props.closeAllGlobalMessages();
};

render() {
const { component, modules } = this.props;

const noModules = modules != null && modules.length === 0;
const hasModules = modules != null && modules.length > 0;

const { tab } = this.state;

return (
<div className="page page-limited" id="project-key">
<Helmet title={translate('update_key.page')} />
<Header />

{modules == null && <i className="spinner" />}

{noModules && (
<div>
<UpdateForm component={component} onKeyChange={this.handleChangeKey} />
</div>
)}

{hasModules && (
<div className="boxed-group boxed-group-inner">
<div className="big-spacer-bottom">
<ul className="tabs">
<li>
<a
className={tab === 'bulk' ? 'selected' : ''}
data-tab="bulk"
href="#"
id="update-key-tab-bulk"
onClick={this.handleChangeTab}>
{translate('update_key.bulk_update')}
</a>
</li>
<li>
<a
className={tab === 'fine' ? 'selected' : ''}
data-tab="fine"
href="#"
id="update-key-tab-fine"
onClick={this.handleChangeTab}>
{translate('update_key.fine_grained_key_update')}
</a>
</li>
</ul>
</div>

{tab === 'bulk' && <BulkUpdate component={component} />}

{tab === 'fine' && (
<FineGrainedUpdate
component={component}
modules={modules}
onError={this.props.addGlobalErrorMessage}
onKeyChange={this.handleChangeKey}
onSuccess={this.props.closeAllGlobalMessages}
/>
)}
</div>
)}
</div>
);
}
}

const mapStateToProps = (state, ownProps) => ({
modules: getProjectAdminProjectModules(state, ownProps.location.query.id)
});

export default connect(
mapStateToProps,
{
fetchProjectModules,
changeKey,
addGlobalErrorMessage,
addGlobalSuccessMessage,
closeAllGlobalMessages
}
)(Key);

+ 0
- 65
server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyConfirm.tsx 查看文件

@@ -1,65 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 ConfirmButton, { ChildrenProps } from '../../../components/controls/ConfirmButton';
import { translate, translateWithParameters } from '../../../helpers/l10n';

interface Props {
children: (props: ChildrenProps) => React.ReactNode;
component: { key: string; name: string };
newKey: string | undefined;
onConfirm: (oldKey: string, newKey: string) => Promise<void>;
}

export default class UpdateKeyConfirm extends React.PureComponent<Props> {
handleConfirm = () => {
return this.props.newKey
? this.props.onConfirm(this.props.component.key, this.props.newKey)
: Promise.reject(undefined);
};

render() {
const { children, component, newKey } = this.props;

return (
<ConfirmButton
confirmButtonText={translate('update_verb')}
modalBody={
<>
{translateWithParameters('update_key.are_you_sure_to_change_key', component.name)}
<div className="spacer-top">
{translate('update_key.old_key')}
{': '}
<strong>{component.key}</strong>
</div>
<div className="spacer-top">
{translate('update_key.new_key')}
{': '}
<strong>{newKey}</strong>
</div>
</>
}
modalHeader={translate('update_key.page')}
onConfirm={this.handleConfirm}>
{children}
</ConfirmButton>
);
}
}

+ 0
- 75
server/sonar-web/src/main/js/apps/project-admin/key/UpdateKeyForm.tsx 查看文件

@@ -1,75 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 UpdateKeyConfirm from './UpdateKeyConfirm';
import { Button } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';

interface Props {
component: { key: string; name: string };
onKeyChange: (oldKey: string, newKey: string) => Promise<void>;
}

interface State {
newKey?: string;
}

export default class UpdateKeyForm extends React.PureComponent<Props, State> {
state: State = {};

handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newKey = event.currentTarget.value;
this.setState({ newKey });
};

handleResetClick = () => {
this.setState({ newKey: undefined });
};

render() {
const { component } = this.props;
const { newKey } = this.state;
const value = newKey !== undefined ? newKey : component.key;
const hasChanged = newKey !== undefined && newKey !== component.key;

return (
<div className="js-fine-grained-update" data-key={component.key}>
<input
className="input-super-large big-spacer-right"
onChange={this.handleInputChange}
type="text"
value={value}
/>

<UpdateKeyConfirm component={component} newKey={newKey} onConfirm={this.props.onKeyChange}>
{({ onClick }) => (
<Button disabled={!hasChanged} onClick={onClick}>
{translate('update_verb')}
</Button>
)}
</UpdateKeyConfirm>

<Button className="spacer-left" disabled={!hasChanged} onClick={this.handleResetClick}>
{translate('reset_verb')}
</Button>
</div>
);
}
}

+ 0
- 24
server/sonar-web/src/main/js/apps/project-admin/key/utils.js 查看文件

@@ -1,24 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
export const reloadUpdateKeyPage = componentKey => {
setTimeout(() => {
window.location = window.baseUrl + '/project/key?id=' + encodeURIComponent(componentKey);
}, 3000);
};

+ 0
- 51
server/sonar-web/src/main/js/apps/project-admin/store/actions.js 查看文件

@@ -1,51 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 { getTree, changeKey as changeKeyApi } from '../../../api/components';
import throwGlobalError from '../../../app/utils/throwGlobalError';

export const RECEIVE_PROJECT_MODULES = 'projectAdmin/RECEIVE_PROJECT_MODULES';
const receiveProjectModules = (projectKey, modules) => ({
type: RECEIVE_PROJECT_MODULES,
projectKey,
modules
});

export const fetchProjectModules = projectKey => dispatch => {
getTree({ component: projectKey, qualifiers: 'BRC', s: 'name', ps: 500 }).then(
r => {
dispatch(receiveProjectModules(projectKey, r.components));
},
() => {}
);
};

export const CHANGE_KEY = 'projectAdmin/CHANGE_KEY';
const changeKeyAction = (key, newKey) => ({
type: CHANGE_KEY,
key,
newKey
});

export const changeKey = (key, newKey) => dispatch => {
return changeKeyApi(key, newKey).then(
() => dispatch(changeKeyAction(key, newKey)),
throwGlobalError
);
};

+ 0
- 45
server/sonar-web/src/main/js/apps/project-admin/store/components.js 查看文件

@@ -1,45 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 { keyBy, omit } from 'lodash';
import { RECEIVE_PROJECT_MODULES, CHANGE_KEY } from './actions';

const components = (state = {}, action = {}) => {
if (action.type === RECEIVE_PROJECT_MODULES) {
const newComponentsByKey = keyBy(action.modules, 'key');
return { ...state, ...newComponentsByKey };
}

if (action.type === CHANGE_KEY) {
const oldComponent = state[action.key];
if (oldComponent != null) {
const newComponent = { ...oldComponent, key: action.newKey };
return {
...omit(state, action.key),
[action.newKey]: newComponent
};
}
}

return state;
};

export default components;

export const getComponentByKey = (state, key) => state[key];

+ 0
- 49
server/sonar-web/src/main/js/apps/project-admin/store/modulesByProject.js 查看文件

@@ -1,49 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 { RECEIVE_PROJECT_MODULES, CHANGE_KEY } from './actions';

const modulesByProject = (state = {}, action = {}) => {
if (action.type === RECEIVE_PROJECT_MODULES) {
const moduleKeys = action.modules.map(module => module.key);
return { ...state, [action.projectKey]: moduleKeys };
}

if (action.type === CHANGE_KEY) {
const newState = {};
Object.keys(state).forEach(projectKey => {
const moduleKeys = state[projectKey];
const changedKeyIndex = moduleKeys.indexOf(action.key);
if (changedKeyIndex !== -1) {
const newModuleKeys = [...moduleKeys];
newModuleKeys.splice(changedKeyIndex, 1, action.newKey);
newState[projectKey] = newModuleKeys;
} else {
newState[projectKey] = moduleKeys;
}
});
return newState;
}

return state;
};

export default modulesByProject;

export const getProjectModules = (state, projectKey) => state[projectKey];

+ 0
- 40
server/sonar-web/src/main/js/apps/project-admin/store/rootReducer.js 查看文件

@@ -1,40 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 { combineReducers } from 'redux';
import components, { getComponentByKey as nextGetComponentByKey } from './components';
import modulesByProject, { getProjectModules as nextGetProjectModules } from './modulesByProject';

const rootReducer = combineReducers({
components,
modulesByProject
});

export default rootReducer;

export const getComponentByKey = (state, componentKey) =>
nextGetComponentByKey(state.components, componentKey);

export const getProjectModules = (state, projectKey) => {
const moduleKeys = nextGetProjectModules(state.modulesByProject, projectKey);
if (moduleKeys == null) {
return null;
}
return moduleKeys.map(moduleKey => getComponentByKey(state, moduleKey));
};

+ 55
- 0
server/sonar-web/src/main/js/apps/projectKey/Key.tsx 查看文件

@@ -0,0 +1,55 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import Helmet from 'react-helmet';
import { withRouter, WithRouterProps } from 'react-router';
import UpdateForm from './UpdateForm';
import { changeKey } from '../../api/components';
import RecentHistory from '../../app/components/RecentHistory';
import { translate } from '../../helpers/l10n';

interface Props {
component: Pick<T.Component, 'key' | 'name'>;
}

export class Key extends React.PureComponent<Props & WithRouterProps> {
handleChangeKey = (newKey: string) => {
return changeKey({ from: this.props.component.key, to: newKey }).then(() => {
RecentHistory.remove(this.props.component.key);
this.props.router.replace({ pathname: '/project/key', query: { id: newKey } });
});
};

render() {
const { component } = this.props;
return (
<div className="page page-limited" id="project-key">
<Helmet title={translate('update_key.page')} />
<header className="page-header">
<h1 className="page-title">{translate('update_key.page')}</h1>
<div className="page-description">{translate('update_key.page.description')}</div>
</header>
<UpdateForm component={component} onKeyChange={this.handleChangeKey} />
</div>
);
}
}

export default withRouter(Key);

server/sonar-web/src/main/js/apps/project-admin/key/UpdateForm.tsx → server/sonar-web/src/main/js/apps/projectKey/UpdateForm.tsx 查看文件

@@ -18,13 +18,13 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import UpdateKeyConfirm from './UpdateKeyConfirm';
import { Button, SubmitButton } from '../../../components/ui/buttons';
import { translate } from '../../../helpers/l10n';
import { Button, SubmitButton } from '../../components/ui/buttons';
import { translate, translateWithParameters } from '../../helpers/l10n';
import ConfirmButton from '../../components/controls/ConfirmButton';

interface Props {
component: { key: string; name: string };
onKeyChange: (oldKey: string, newKey: string) => Promise<void>;
component: Pick<T.Component, 'key' | 'name'>;
onKeyChange: (newKey: string) => Promise<void>;
}

interface State {
@@ -50,7 +50,26 @@ export default class UpdateForm extends React.PureComponent<Props, State> {
const hasChanged = value !== component.key;

return (
<UpdateKeyConfirm component={component} newKey={newKey} onConfirm={this.props.onKeyChange}>
<ConfirmButton
confirmButtonText={translate('update_verb')}
confirmData={newKey}
modalBody={
<>
{translateWithParameters('update_key.are_you_sure_to_change_key', component.name)}
<div className="spacer-top">
{translate('update_key.old_key')}
{': '}
<strong>{component.key}</strong>
</div>
<div className="spacer-top">
{translate('update_key.new_key')}
{': '}
<strong>{newKey}</strong>
</div>
</>
}
modalHeader={translate('update_key.page')}
onConfirm={this.props.onKeyChange}>
{({ onFormSubmit }) => (
<form onSubmit={onFormSubmit}>
<input
@@ -79,7 +98,7 @@ export default class UpdateForm extends React.PureComponent<Props, State> {
</div>
</form>
)}
</UpdateKeyConfirm>
</ConfirmButton>
);
}
}

+ 42
- 0
server/sonar-web/src/main/js/apps/projectKey/__tests__/Key-test.tsx 查看文件

@@ -0,0 +1,42 @@
/*
* SonarQube
* Copyright (C) 2009-2018 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import { WithRouterProps } from 'react-router';
import { Key } from '../Key';
import { changeKey } from '../../../api/components';

jest.mock('../../../api/components', () => ({
changeKey: jest.fn().mockResolvedValue(undefined)
}));

it('should render and change key', async () => {
const withRouterProps = { router: { replace: jest.fn() } as any } as WithRouterProps;
const wrapper = shallow(<Key component={{ key: 'foo', name: 'Foo' }} {...withRouterProps} />);
expect(wrapper).toMatchSnapshot();

wrapper.find('UpdateForm').prop<Function>('onKeyChange')('bar');
await new Promise(setImmediate);
expect(changeKey).toBeCalledWith({ from: 'foo', to: 'bar' });
expect(withRouterProps.router.replace).toBeCalledWith({
pathname: '/project/key',
query: { id: 'bar' }
});
});

server/sonar-web/src/main/js/apps/project-admin/key/Header.js → server/sonar-web/src/main/js/apps/projectKey/__tests__/UpdateForm-test.tsx 查看文件

@@ -17,14 +17,25 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import React from 'react';
import { translate } from '../../../helpers/l10n';
import * as React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import UpdateForm from '../UpdateForm';
import { change, click } from '../../../helpers/testUtils';

export default function Header() {
return (
<header className="page-header">
<h1 className="page-title">{translate('update_key.page')}</h1>
<div className="page-description">{translate('update_key.page.description')}</div>
</header>
it('should render', () => {
const wrapper = shallow(
<UpdateForm component={{ key: 'foo', name: 'Foo' }} onKeyChange={jest.fn()} />
);
expect(getInner(wrapper)).toMatchSnapshot();

change(getInner(wrapper).find('input'), 'bar');
expect(getInner(wrapper)).toMatchSnapshot();

click(getInner(wrapper).find('Button'));
expect(getInner(wrapper)).toMatchSnapshot();
});

function getInner(wrapper: ShallowWrapper) {
// TODO find a better way to do this
return wrapper.dive().dive();
}

+ 37
- 0
server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/Key-test.tsx.snap 查看文件

@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render and change key 1`] = `
<div
className="page page-limited"
id="project-key"
>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="update_key.page"
/>
<header
className="page-header"
>
<h1
className="page-title"
>
update_key.page
</h1>
<div
className="page-description"
>
update_key.page.description
</div>
</header>
<UpdateForm
component={
Object {
"key": "foo",
"name": "Foo",
}
}
onKeyChange={[Function]}
/>
</div>
`;

+ 112
- 0
server/sonar-web/src/main/js/apps/projectKey/__tests__/__snapshots__/UpdateForm-test.tsx.snap 查看文件

@@ -0,0 +1,112 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render 1`] = `
<Fragment>
<form
onSubmit={[Function]}
>
<input
className="input-super-large"
id="update-key-new-key"
onChange={[Function]}
placeholder="update_key.new_key"
required={true}
type="text"
value="foo"
/>
<div
className="spacer-top"
>
<SubmitButton
disabled={true}
id="update-key-submit"
>
update_verb
</SubmitButton>
<Button
className="spacer-left"
disabled={true}
id="update-key-reset"
onClick={[Function]}
type="reset"
>
reset_verb
</Button>
</div>
</form>
</Fragment>
`;

exports[`should render 2`] = `
<Fragment>
<form
onSubmit={[Function]}
>
<input
className="input-super-large"
id="update-key-new-key"
onChange={[Function]}
placeholder="update_key.new_key"
required={true}
type="text"
value="bar"
/>
<div
className="spacer-top"
>
<SubmitButton
disabled={false}
id="update-key-submit"
>
update_verb
</SubmitButton>
<Button
className="spacer-left"
disabled={false}
id="update-key-reset"
onClick={[Function]}
type="reset"
>
reset_verb
</Button>
</div>
</form>
</Fragment>
`;

exports[`should render 3`] = `
<Fragment>
<form
onSubmit={[Function]}
>
<input
className="input-super-large"
id="update-key-new-key"
onChange={[Function]}
placeholder="update_key.new_key"
required={true}
type="text"
value="foo"
/>
<div
className="spacer-top"
>
<SubmitButton
disabled={true}
id="update-key-submit"
>
update_verb
</SubmitButton>
<Button
className="spacer-left"
disabled={true}
id="update-key-reset"
onClick={[Function]}
type="reset"
>
reset_verb
</Button>
</div>
</form>
</Fragment>
`;

+ 1
- 1
server/sonar-web/src/main/js/components/SelectList/SelectListListContainer.tsx 查看文件

@@ -91,7 +91,7 @@ export default class SelectListListContainer extends React.PureComponent<Props,
onCheck={this.handleBulkChange}
thirdState={selectedElements.length > 0 && elements.length !== selectedElements.length}>
<span className="big-spacer-left">
{translate('update_key.bulk_update')}
{translate('bulk_change')}
<DeferredSpinner className="spacer-left" loading={this.state.loading} timeout={10} />
</span>
</Checkbox>

+ 0
- 7
server/sonar-web/src/main/js/store/rootReducer.ts 查看文件

@@ -24,7 +24,6 @@ import languages, * as fromLanguages from './languages';
import metrics, * as fromMetrics from './metrics';
import organizations, * as fromOrganizations from './organizations';
import users, * as fromUsers from './users';
import projectAdminApp, * as fromProjectAdminApp from '../apps/project-admin/store/rootReducer';
import settingsApp, * as fromSettingsApp from '../apps/settings/store/rootReducer';

export type Store = {
@@ -36,7 +35,6 @@ export type Store = {
users: fromUsers.State;

// apps
projectAdminApp: any;
settingsApp: any;
};

@@ -49,7 +47,6 @@ export default combineReducers<Store>({
users,

// apps
projectAdminApp,
settingsApp
});

@@ -128,7 +125,3 @@ export function isSettingsAppLoading(state: Store, key: string) {
export function getSettingsAppValidationMessage(state: Store, key: string) {
return fromSettingsApp.getValidationMessage(state.settingsApp, key);
}

export function getProjectAdminProjectModules(state: Store, projectKey: string) {
return fromProjectAdminApp.getProjectModules(state.projectAdminApp, projectKey);
}

+ 3
- 18
sonar-core/src/main/resources/org/sonar/l10n/core.properties 查看文件

@@ -514,7 +514,7 @@ project_quality_profiles.page.description=Choose which profile is associated wit
project_quality_gate.page=Quality Gate
project_quality_gate.page.description=Choose which quality gate is associated with this project.
update_key.page=Update Key
update_key.page.description=Edit the keys of a project and/or its modules. Key changes must be made here BEFORE analyzing the project with the new keys, otherwise the analysis will simply create another project with the new key, rather than updating the existing project.
update_key.page.description=Edit the key of a project. Key changes must be made here BEFORE analyzing the project with the new keys, otherwise the analysis will simply create another project with the new key, rather than updating the existing project.
deletion.page=Deletion
project_deletion.page.description=Delete this project. The operation cannot be undone.
portfolio_deletion.page.description=This portfolio and its sub-portfolios will be deleted. If this portfolio is referenced by other entities, it will be removed from them. Independent entities referenced by this portfolio, such as projects and other top-level portfolios will not be deleted. This operation cannot be undone.
@@ -1075,27 +1075,12 @@ project_activity.custom_metric.covered_lines=Covered Lines

#------------------------------------------------------------------------------
#
# PROJECT / MODULE "UPDATE KEY" PAGE
# PROJECT "UPDATE KEY" PAGE
#
#------------------------------------------------------------------------------
update_key.bulk_update=Bulk Update
update_key.fine_grained_key_update=Fine-grained Update
update_key.old_key=Old Key
update_key.new_key=New Key
update_key.key_updated=The key has successfully been updated for all required resources.
update_key.key_updated.reload=The key has successfully been updated for all required resources. This page will be reloaded shortly.
update_key.bulk_change_description=The bulk update allows to replace a part of the current key(s) by another string on the current project and all its submodules - if applicable.
update_key.current_key_for_project_x_is_x=The key of the "{0}" project is currently "{1}".
update_key.replace=Replace
update_key.by=By
update_key.replace_example=org.myCompany
update_key.by_example=com.myNewCompany
update_key.cant_update_because_duplicate_keys=The replacement of "{0}" by "{1}" is impossible as it would result in duplicate keys (in red below):
update_key.keys_will_be_updated_as_follows=The resources will be updated as follows:
update_key.duplicate_key=Duplicate Key
update_key.no_key_to_update=No key contains the string to replace ("{0}").
update_key.are_you_sure_to_change_key=Are you sure you want to change key of "{0}", as well as all its modules and resources?
update_key.see_results=See Results
update_key.are_you_sure_to_change_key=Are you sure you want to change key of "{0}"?


#------------------------------------------------------------------------------

Loading…
取消
儲存