Procházet zdrojové kódy

SONAR-8693 Do not allow to filter portfolios or applications by status on management page

tags/6.6-RC1
Stas Vilchik před 6 roky
rodič
revize
b734fdfd93

+ 25
- 19
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx Zobrazit soubor

@@ -25,7 +25,7 @@ import Search from './Search';
import Projects from './Projects';
import CreateProjectForm from './CreateProjectForm';
import ListFooter from '../../components/controls/ListFooter';
import { PAGE_SIZE, Type, Project } from './utils';
import { PAGE_SIZE, Project } from './utils';
import { getComponents, getProvisioned } from '../../api/components';
import { Organization } from '../../app/types';
import { translate } from '../../helpers/l10n';
@@ -41,12 +41,12 @@ interface State {
createProjectForm: boolean;
page: number;
projects: Project[];
provisioned: boolean;
qualifiers: string;
query: string;
ready: boolean;
selection: string[];
total: number;
type: Type;
}

export default class App extends React.PureComponent<Props, State> {
@@ -58,11 +58,11 @@ export default class App extends React.PureComponent<Props, State> {
createProjectForm: false,
ready: false,
projects: [],
provisioned: false,
total: 0,
page: 1,
query: '',
qualifiers: 'TRK',
type: Type.All,
selection: []
};
this.requestProjects = debounce(this.requestProjects, 250);
@@ -84,16 +84,8 @@ export default class App extends React.PureComponent<Props, State> {
q: this.state.query ? this.state.query : undefined
});

requestProjects = () => {
switch (this.state.type) {
case Type.All:
this.requestAllProjects();
break;
case Type.Provisioned:
this.requestProvisioned();
break;
}
};
requestProjects = () =>
this.state.provisioned ? this.requestProvisioned() : this.requestAllProjects();

requestProvisioned = () => {
const data = this.getFilters();
@@ -134,16 +126,23 @@ export default class App extends React.PureComponent<Props, State> {
this.setState({ ready: false, page: 1, query, selection: [] }, this.requestProjects);
};

onTypeChanged = (newType: Type) => {
onProvisionedChanged = (provisioned: boolean) => {
this.setState(
{ ready: false, page: 1, query: '', type: newType, qualifiers: 'TRK', selection: [] },
{ ready: false, page: 1, query: '', provisioned, qualifiers: 'TRK', selection: [] },
this.requestProjects
);
};

onQualifierChanged = (newQualifier: string) => {
this.setState(
{ ready: false, page: 1, query: '', type: Type.All, qualifiers: newQualifier, selection: [] },
{
ready: false,
page: 1,
provisioned: false,
query: '',
qualifiers: newQualifier,
selection: []
},
this.requestProjects
);
};
@@ -188,14 +187,21 @@ export default class App extends React.PureComponent<Props, State> {
/>

<Search
{...this.props}
{...this.state}
onAllSelected={this.onAllSelected}
onAllDeselected={this.onAllDeselected}
onDeleteProjects={this.requestProjects}
onProvisionedChanged={this.onProvisionedChanged}
onQualifierChanged={this.onQualifierChanged}
onSearch={this.onSearch}
onTypeChanged={this.onTypeChanged}
organization={this.props.organization}
projects={this.state.projects}
provisioned={this.state.provisioned}
qualifiers={this.state.qualifiers}
query={this.state.query}
ready={this.state.ready}
selection={this.state.selection}
topLevelQualifiers={this.props.topLevelQualifiers}
total={this.state.total}
/>

<Projects

+ 1
- 2
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx Zobrazit soubor

@@ -20,7 +20,6 @@
import * as React from 'react';
import Modal from 'react-modal';
import * as Select from 'react-select';
import { Type } from './utils';
import {
getPermissionTemplates,
PermissionTemplate,
@@ -32,11 +31,11 @@ import { translate, translateWithParameters } from '../../helpers/l10n';
export interface Props {
onClose: () => void;
organization: string;
provisioned: boolean;
qualifier: string;
query: string;
selection: string[];
total: number;
type: Type;
}

interface State {

+ 54
- 24
server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx Zobrazit soubor

@@ -18,32 +18,34 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import * as Select from 'react-select';
import { sortBy } from 'lodash';
import BulkApplyTemplateModal from './BulkApplyTemplateModal';
import DeleteModal from './DeleteModal';
import { Type, QUALIFIERS_ORDER } from './utils';
import { QUALIFIERS_ORDER } from './utils';
import { Project } from './utils';
import { Organization } from '../../app/types';
import RadioToggle from '../../components/controls/RadioToggle';
import Checkbox from '../../components/controls/Checkbox';
import { translate } from '../../helpers/l10n';
import QualifierIcon from '../../components/shared/QualifierIcon';
import Tooltip from '../../components/controls/Tooltip';

export interface Props {
onAllDeselected: () => void;
onAllSelected: () => void;
onDeleteProjects: () => void;
onProvisionedChanged: (provisioned: boolean) => void;
onQualifierChanged: (qualifier: string) => void;
onSearch: (query: string) => void;
onTypeChanged: (type: Type) => void;
organization: Organization;
projects: Project[];
provisioned: boolean;
qualifiers: string;
query: string;
ready: boolean;
selection: any[];
topLevelQualifiers: string[];
total: number;
type: Type;
}

interface State {
@@ -66,15 +68,11 @@ export default class Search extends React.PureComponent<Props, State> {
this.props.onSearch(q);
};

getTypeOptions = () => [
{ value: Type.All, label: 'All' },
{ value: Type.Provisioned, label: 'Provisioned' }
];

getQualifierOptions = () => {
const options = this.props.topLevelQualifiers.map(q => {
return { value: q, label: translate('qualifiers', q) };
});
const options = this.props.topLevelQualifiers.map(q => ({
label: translate('qualifiers', q),
value: q
}));
return sortBy(options, option => QUALIFIERS_ORDER.indexOf(option.value));
};

@@ -111,6 +109,8 @@ export default class Search extends React.PureComponent<Props, State> {
this.setState({ bulkApplyTemplateModal: false });
};

handleQualifierChange = ({ value }: { value: string }) => this.props.onQualifierChanged(value);

renderCheckbox = () => {
const isAllChecked =
this.props.projects.length > 0 && this.props.selection.length === this.props.projects.length;
@@ -119,9 +119,22 @@ export default class Search extends React.PureComponent<Props, State> {
this.props.selection.length > 0 &&
this.props.selection.length < this.props.projects.length;
const checked = isAllChecked || thirdState;
return <Checkbox checked={checked} thirdState={thirdState} onCheck={this.onCheck} />;
return (
<Checkbox
checked={checked}
id="projects-selection"
thirdState={thirdState}
onCheck={this.onCheck}
/>
);
};

renderQualifierOption = (option: { label: string; value: string }) =>
<span>
<QualifierIcon className="little-spacer-right" qualifier={option.value} />
{option.label}
</span>;

renderQualifierFilter = () => {
const options = this.getQualifierOptions();
if (options.length < 2) {
@@ -129,16 +142,40 @@ export default class Search extends React.PureComponent<Props, State> {
}
return (
<td className="thin nowrap text-middle">
<RadioToggle
<Select
className="input-medium"
clearable={false}
disabled={!this.props.ready}
optionRenderer={this.renderQualifierOption}
options={this.getQualifierOptions()}
value={this.props.qualifiers}
valueRenderer={this.renderQualifierOption}
name="projects-qualifier"
onCheck={this.props.onQualifierChanged}
onChange={this.handleQualifierChange}
searchable={false}
/>
</td>
);
};

renderTypeFilter = () =>
this.props.qualifiers === 'TRK'
? <td className="thin nowrap text-middle">
<Checkbox
className="link-checkbox-control"
checked={this.props.provisioned}
id="projects-provisioned"
onCheck={this.props.onProvisionedChanged}>
<span className="little-spacer-left">
{translate('provisioning.only_provisioned')}
<Tooltip overlay={translate('provisioning.only_provisioned.tooltip')}>
<i className="spacer-left icon-help" />
</Tooltip>
</span>
</Checkbox>
</td>
: null;

render() {
const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0;
return (
@@ -150,14 +187,7 @@ export default class Search extends React.PureComponent<Props, State> {
{this.props.ready ? this.renderCheckbox() : <i className="spinner" />}
</td>
{this.renderQualifierFilter()}
<td className="thin nowrap text-middle">
<RadioToggle
options={this.getTypeOptions()}
value={this.props.type}
name="projects-type"
onCheck={this.props.onTypeChanged}
/>
</td>
{this.renderTypeFilter()}
<td className="text-middle">
<form onSubmit={this.onSubmit} className="search-box">
<button className="search-box-submit button-clean">
@@ -194,11 +224,11 @@ export default class Search extends React.PureComponent<Props, State> {
<BulkApplyTemplateModal
onClose={this.closeBulkApplyTemplateModal}
organization={this.props.organization.key}
provisioned={this.props.provisioned}
qualifier={this.props.qualifiers}
query={this.props.query}
selection={this.props.selection}
total={this.props.total}
type={this.props.type}
/>}

{this.state.deleteModal &&

+ 11
- 5
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx Zobrazit soubor

@@ -23,6 +23,13 @@ jest.mock('lodash', () => {
return lodash;
});

// actual version breaks `mount`
jest.mock('rc-tooltip', () => ({
default: function Tooltip() {
return null;
}
}));

jest.mock('../../../api/components', () => ({
getComponents: jest.fn(),
getProvisioned: jest.fn(() => Promise.resolve({ paging: { total: 0 }, projects: [] }))
@@ -31,7 +38,6 @@ jest.mock('../../../api/components', () => ({
import * as React from 'react';
import { mount } from 'enzyme';
import App, { Props } from '../App';
import { Type } from '../utils';

const getComponents = require('../../../api/components').getComponents as jest.Mock<any>;
const getProvisioned = require('../../../api/components').getProvisioned as jest.Mock<any>;
@@ -57,15 +63,15 @@ it('fetches all projects on mount', () => {
expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'TRK' });
});

it('changes type', () => {
it('selects provisioned', () => {
const wrapper = mountRender();
wrapper.find('Search').prop<Function>('onTypeChanged')(Type.Provisioned);
wrapper.find('Search').prop<Function>('onProvisionedChanged')(true);
expect(getProvisioned).lastCalledWith(defaultSearchParameters);
});

it('changes qualifier and resets type', () => {
it('changes qualifier and resets provisioned', () => {
const wrapper = mountRender();
wrapper.setState({ type: Type.Provisioned });
wrapper.setState({ provisioned: true });
wrapper.find('Search').prop<Function>('onQualifierChanged')('VW');
expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'VW' });
});

+ 1
- 2
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx Zobrazit soubor

@@ -26,7 +26,6 @@ jest.mock('../../../api/permissions', () => ({
import * as React from 'react';
import { mount, shallow } from 'enzyme';
import BulkApplyTemplateModal, { Props } from '../BulkApplyTemplateModal';
import { Type } from '../utils';
import { click } from '../../../helpers/testUtils';

const applyTemplateToProject = require('../../../api/permissions')
@@ -117,11 +116,11 @@ function render(props?: { [P in keyof Props]?: Props[P] }) {
<BulkApplyTemplateModal
onClose={jest.fn()}
organization="org"
provisioned={true}
qualifier="TRK"
query="bla"
selection={[]}
total={17}
type={Type.All}
{...props}
/>
);

+ 17
- 11
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx Zobrazit soubor

@@ -20,7 +20,6 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import Search, { Props } from '../Search';
import { Type } from '../utils';
import { change, click } from '../../../helpers/testUtils';

const organization = { key: 'org', name: 'org', projectVisibility: 'public' };
@@ -36,15 +35,22 @@ it('render qualifiers filter', () => {
it('updates qualifier', () => {
const onQualifierChanged = jest.fn();
const wrapper = shallowRender({ onQualifierChanged, topLevelQualifiers: ['TRK', 'VW', 'APP'] });
wrapper.find('RadioToggle[name="projects-qualifier"]').prop<Function>('onCheck')('VW');
wrapper.find('Select[name="projects-qualifier"]').prop<Function>('onChange')({ value: 'VW' });
expect(onQualifierChanged).toBeCalledWith('VW');
});

it('updates type', () => {
const onTypeChanged = jest.fn();
const wrapper = shallowRender({ onTypeChanged });
wrapper.find('RadioToggle[name="projects-type"]').prop<Function>('onCheck')(Type.Provisioned);
expect(onTypeChanged).toBeCalledWith(Type.Provisioned);
it('selects provisioned', () => {
const onProvisionedChanged = jest.fn();
const wrapper = shallowRender({ onProvisionedChanged });
wrapper.find('Checkbox[id="projects-provisioned"]').prop<Function>('onCheck')(true);
expect(onProvisionedChanged).toBeCalledWith(true);
});

it('does not render provisioned filter for portfolios', () => {
const wrapper = shallowRender();
expect(wrapper.find('Checkbox[id="projects-provisioned"]').exists()).toBeTruthy();
wrapper.setProps({ qualifiers: 'VW' });
expect(wrapper.find('Checkbox[id="projects-provisioned"]').exists()).toBeFalsy();
});

it('searches', () => {
@@ -59,10 +65,10 @@ it('checks all or none projects', () => {
const onAllSelected = jest.fn();
const wrapper = shallowRender({ onAllDeselected, onAllSelected });

wrapper.find('Checkbox').prop<Function>('onCheck')(true);
wrapper.find('Checkbox[id="projects-selection"]').prop<Function>('onCheck')(true);
expect(onAllSelected).toBeCalled();

wrapper.find('Checkbox').prop<Function>('onCheck')(false);
wrapper.find('Checkbox[id="projects-selection"]').prop<Function>('onCheck')(false);
expect(onAllDeselected).toBeCalled();
});

@@ -89,18 +95,18 @@ function shallowRender(props?: { [P in keyof Props]?: Props[P] }) {
onAllDeselected={jest.fn()}
onAllSelected={jest.fn()}
onDeleteProjects={jest.fn()}
onProvisionedChanged={jest.fn()}
onQualifierChanged={jest.fn()}
onSearch={jest.fn()}
onTypeChanged={jest.fn()}
organization={organization}
projects={[]}
provisioned={false}
qualifiers="TRK"
query=""
ready={true}
selection={[]}
topLevelQualifiers={['TRK']}
total={0}
type={Type.All}
{...props}
/>
);

+ 85
- 37
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap Zobrazit soubor

@@ -4,11 +4,11 @@ exports[`bulk applies permission template 1`] = `
<BulkApplyTemplateModal
onClose={[Function]}
organization="org"
provisioned={false}
qualifier="TRK"
query=""
selection={Array []}
total={0}
type="ALL"
/>
`;

@@ -41,6 +41,7 @@ exports[`render qualifiers filter 1`] = `
>
<Checkbox
checked={false}
id="projects-selection"
onCheck={[Function]}
thirdState={false}
/>
@@ -48,10 +49,40 @@ exports[`render qualifiers filter 1`] = `
<td
className="thin nowrap text-middle"
>
<RadioToggle
<Select
addLabelText="Add \\"{label}\\"?"
arrowRenderer={[Function]}
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
className="input-medium"
clearAllText="Clear all"
clearRenderer={[Function]}
clearValueText="Clear value"
clearable={false}
deleteRemoves={true}
delimiter=","
disabled={false}
escapeClearsValue={true}
filterOptions={[Function]}
ignoreAccents={true}
ignoreCase={true}
inputProps={Object {}}
isLoading={false}
joinValues={false}
labelKey="label"
matchPos="any"
matchProp="any"
menuBuffer={0}
menuRenderer={[Function]}
multi={false}
name="projects-qualifier"
onCheck={[Function]}
noResultsText="No results found"
onBlurResetsInput={true}
onChange={[Function]}
onCloseResetsInput={true}
optionComponent={[Function]}
optionRenderer={[Function]}
options={
Array [
Object {
@@ -68,30 +99,43 @@ exports[`render qualifiers filter 1`] = `
},
]
}
pageSize={5}
placeholder="Select..."
required={false}
scrollMenuIntoView={true}
searchable={false}
simpleValue={false}
tabSelectsValue={true}
value="TRK"
valueComponent={[Function]}
valueKey="value"
valueRenderer={[Function]}
/>
</td>
<td
className="thin nowrap text-middle"
>
<RadioToggle
disabled={false}
name="projects-type"
<Checkbox
checked={false}
className="link-checkbox-control"
id="projects-provisioned"
onCheck={[Function]}
options={
Array [
Object {
"label": "All",
"value": "ALL",
},
Object {
"label": "Provisioned",
"value": "PROVISIONED",
},
]
}
value="ALL"
/>
thirdState={false}
>
<span
className="little-spacer-left"
>
provisioning.only_provisioned
<Tooltip
overlay="provisioning.only_provisioned.tooltip"
placement="bottom"
>
<i
className="spacer-left icon-help"
/>
</Tooltip>
</span>
</Checkbox>
</td>
<td
className="text-middle"
@@ -153,6 +197,7 @@ exports[`renders 1`] = `
>
<Checkbox
checked={false}
id="projects-selection"
onCheck={[Function]}
thirdState={false}
/>
@@ -160,24 +205,27 @@ exports[`renders 1`] = `
<td
className="thin nowrap text-middle"
>
<RadioToggle
disabled={false}
name="projects-type"
<Checkbox
checked={false}
className="link-checkbox-control"
id="projects-provisioned"
onCheck={[Function]}
options={
Array [
Object {
"label": "All",
"value": "ALL",
},
Object {
"label": "Provisioned",
"value": "PROVISIONED",
},
]
}
value="ALL"
/>
thirdState={false}
>
<span
className="little-spacer-left"
>
provisioning.only_provisioned
<Tooltip
overlay="provisioning.only_provisioned.tooltip"
placement="bottom"
>
<i
className="spacer-left icon-help"
/>
</Tooltip>
</span>
</Checkbox>
</td>
<td
className="text-middle"

+ 1
- 6
server/sonar-web/src/main/js/apps/projectsManagement/utils.ts Zobrazit soubor

@@ -19,12 +19,7 @@
*/
export const PAGE_SIZE = 50;

export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP', 'DEV'];

export enum Type {
All = 'ALL',
Provisioned = 'PROVISIONED'
}
export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP'];

export interface Project {
key: string;

+ 1
- 1
server/sonar-web/src/main/less/components/react-select.less Zobrazit soubor

@@ -103,7 +103,7 @@

.Select-value svg,
.Select-value [class^="icon-"] {
padding-top: 4px;
padding-top: 5px;
}

.Select-value img {

+ 6
- 0
server/sonar-web/src/main/less/init/links.less Zobrazit soubor

@@ -83,6 +83,12 @@ a {
}
}

.link-checkbox-control {
display: inline-block;
padding: 4px 0 5px;
line-height: 16px;
}

a.active-link,
.link-active {
.link-no-underline;

+ 2
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties Zobrazit soubor

@@ -1997,6 +1997,8 @@ provisioning.missing.name=Name is missing
provisioning.no_analysis=No analysis has been performed since creation. The only available section is the configuration.
provisioning.no_analysis.delete=Either you should retry to analyze the project, or simply {0}.
provisioning.no_analysis.delete_it=delete it
provisioning.only_provisioned=Only Provisioned
provisioning.only_provisioned.tooltip=Provisioned projects are projects that have been created, but have not been analyzed yet.


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

Načítá se…
Zrušit
Uložit