@@ -32,8 +32,8 @@ export function getGhosts(data: RequestData): Promise<any> { | |||
return getJSON('/api/projects/ghosts', data); | |||
} | |||
export function deleteComponents(data: { projects: string; organization?: string }): Promise<void> { | |||
return post('/api/projects/bulk_delete', data); | |||
export function deleteComponents(projects: string[], organization: string): Promise<void> { | |||
return post('/api/projects/bulk_delete', { projects: projects.join(), organization }); | |||
} | |||
export function deleteProject(project: string): Promise<void> { |
@@ -85,10 +85,30 @@ export function revokePermissionFromGroup( | |||
return post('/api/permissions/remove_group', data); | |||
} | |||
/** | |||
* Get list of permission templates | |||
*/ | |||
export function getPermissionTemplates(organization?: string) { | |||
export interface PermissionTemplate { | |||
id: string; | |||
name: string; | |||
description?: string; | |||
projectKeyPattern?: string; | |||
createdAt: string; | |||
updatedAt?: string; | |||
permissions: Array<{ | |||
key: string; | |||
usersCount: number; | |||
groupsCount: number; | |||
withProjectCreator?: boolean; | |||
}>; | |||
} | |||
interface GetPermissionTemplatesResponse { | |||
permissionTemplates: PermissionTemplate[]; | |||
defaultTemplates: Array<{ templateId: string; qualifier: string }>; | |||
permissions: Array<{ key: string; name: string; description: string }>; | |||
} | |||
export function getPermissionTemplates( | |||
organization?: string | |||
): Promise<GetPermissionTemplatesResponse> { | |||
const url = '/api/permissions/search_templates'; | |||
return organization ? getJSON(url, { organization }) : getJSON(url); | |||
} |
@@ -42,7 +42,7 @@ class SettingsNav extends React.PureComponent { | |||
} | |||
isProjectsActive() { | |||
const urls = ['/projects_admin', '/background_tasks']; | |||
const urls = ['/admin/projects_management', '/background_tasks']; | |||
return this.isSomethingActive(urls); | |||
} | |||
@@ -158,7 +158,7 @@ class SettingsNav extends React.PureComponent { | |||
<ul className="dropdown-menu"> | |||
{!this.props.customOrganizations && | |||
<li> | |||
<IndexLink to="/projects_admin" activeClassName="active"> | |||
<IndexLink to="/admin/projects_management" activeClassName="active"> | |||
Management | |||
</IndexLink> | |||
</li>} |
@@ -154,7 +154,7 @@ exports[`should work with extensions 1`] = ` | |||
<li> | |||
<IndexLink | |||
activeClassName="active" | |||
to="/projects_admin" | |||
to="/admin/projects_management" | |||
> | |||
Management | |||
</IndexLink> |
@@ -0,0 +1,120 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 enum BranchType { | |||
LONG = 'LONG', | |||
SHORT = 'SHORT' | |||
} | |||
export interface MainBranch { | |||
isMain: true; | |||
name: string; | |||
status?: { | |||
qualityGateStatus: string; | |||
}; | |||
} | |||
export interface LongLivingBranch { | |||
isMain: false; | |||
name: string; | |||
status?: { | |||
qualityGateStatus: string; | |||
}; | |||
type: BranchType.LONG; | |||
} | |||
export interface ShortLivingBranch { | |||
isMain: false; | |||
isOrphan?: true; | |||
mergeBranch: string; | |||
name: string; | |||
status?: { | |||
bugs: number; | |||
codeSmells: number; | |||
vulnerabilities: number; | |||
}; | |||
type: BranchType.SHORT; | |||
} | |||
export type Branch = MainBranch | LongLivingBranch | ShortLivingBranch; | |||
export interface ComponentExtension { | |||
key: string; | |||
name: string; | |||
} | |||
export interface Component { | |||
analysisDate?: string; | |||
breadcrumbs: Array<{ | |||
key: string; | |||
name: string; | |||
qualifier: string; | |||
}>; | |||
configuration?: ComponentConfiguration; | |||
extensions?: ComponentExtension[]; | |||
isFavorite?: boolean; | |||
key: string; | |||
name: string; | |||
organization: string; | |||
path?: string; | |||
qualifier: string; | |||
refKey?: string; | |||
version?: string; | |||
} | |||
export interface ComponentConfiguration { | |||
extensions?: ComponentExtension[]; | |||
showBackgroundTasks?: boolean; | |||
showLinks?: boolean; | |||
showManualMeasures?: boolean; | |||
showQualityGates?: boolean; | |||
showQualityProfiles?: boolean; | |||
showPermissions?: boolean; | |||
showSettings?: boolean; | |||
showUpdateKey?: boolean; | |||
} | |||
export interface Metric { | |||
custom?: boolean; | |||
decimalScale?: number; | |||
description?: string; | |||
direction?: number; | |||
domain?: string; | |||
hidden?: boolean; | |||
key: string; | |||
name: string; | |||
qualitative?: boolean; | |||
type: string; | |||
} | |||
export interface Organization { | |||
adminPages?: Array<{ key: string; name: string }>; | |||
avatar?: string; | |||
canAdmin?: boolean; | |||
canDelete?: boolean; | |||
canProvisionProjects?: boolean; | |||
canUpdateProjectsVisibilityToPrivate?: boolean; | |||
description?: string; | |||
isDefault?: boolean; | |||
key: string; | |||
name: string; | |||
pages?: Array<{ key: string; name: string }>; | |||
projectVisibility: string; | |||
url?: string; | |||
} |
@@ -56,7 +56,7 @@ import permissionTemplatesRoutes from '../../apps/permission-templates/routes'; | |||
import projectActivityRoutes from '../../apps/projectActivity/routes'; | |||
import projectAdminRoutes from '../../apps/project-admin/routes'; | |||
import projectsRoutes from '../../apps/projects/routes'; | |||
import projectsAdminRoutes from '../../apps/projects-admin/routes'; | |||
import projectsManagementRoutes from '../../apps/projectsManagement/routes'; | |||
import qualityGatesRoutes from '../../apps/quality-gates/routes'; | |||
import qualityProfilesRoutes from '../../apps/quality-profiles/routes'; | |||
import sessionsRoutes from '../../apps/sessions/routes'; | |||
@@ -115,6 +115,7 @@ const startReactApp = () => { | |||
}} | |||
/> | |||
<Redirect from="/projects_admin" to="/admin/projects_management" /> | |||
<Redirect from="/component/index" to="/component" /> | |||
<Redirect from="/component_issues" to="/project/issues" /> | |||
<Redirect from="/dashboard/index" to="/dashboard" /> | |||
@@ -203,7 +204,10 @@ const startReactApp = () => { | |||
<Route path="groups" childRoutes={groupsRoutes} /> | |||
<Route path="metrics" childRoutes={metricsRoutes} /> | |||
<Route path="permission_templates" childRoutes={permissionTemplatesRoutes} /> | |||
<Route path="projects_admin" childRoutes={projectsAdminRoutes} /> | |||
<Route | |||
path="admin/projects_management" | |||
childRoutes={projectsManagementRoutes} | |||
/> | |||
<Route path="roles/global" childRoutes={globalPermissionsRoutes} /> | |||
<Route path="settings" childRoutes={settingsRoutes} /> | |||
<Route path="system" childRoutes={systemRoutes} /> |
@@ -20,7 +20,7 @@ | |||
// @flow | |||
import React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import AppContainer from '../../projects-admin/AppContainer'; | |||
import AppContainer from '../../projectsManagement/AppContainer'; | |||
import { getOrganizationByKey } from '../../../store/rootReducer'; | |||
/*:: import type { Organization } from '../../../store/organizations/duck'; */ | |||
@@ -87,7 +87,7 @@ export default class Form extends React.PureComponent { | |||
<form onSubmit={this.handleSubmit}> | |||
<div className="modal-head"> | |||
<h2> | |||
{translate('qualifiers.delete.TRK')} | |||
{translate('qualifier.delete.TRK')} | |||
</h2> | |||
</div> | |||
<div className="modal-body"> |
@@ -1,60 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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 { shallow } from 'enzyme'; | |||
import Projects from '../projects'; | |||
import Checkbox from '../../../components/controls/Checkbox'; | |||
it('should render list of projects with no selection', () => { | |||
const projects = [ | |||
{ id: '1', key: 'a', name: 'A', qualifier: 'TRK' }, | |||
{ id: '2', key: 'b', name: 'B', qualifier: 'TRK' } | |||
]; | |||
const result = shallow( | |||
<Projects | |||
organization={{ key: 'foo' }} | |||
projects={projects} | |||
selection={[]} | |||
refresh={jest.fn()} | |||
/> | |||
); | |||
expect(result.find('tr').length).toBe(2); | |||
expect(result.find(Checkbox).filterWhere(n => n.prop('checked')).length).toBe(0); | |||
}); | |||
it('should render list of projects with one selected', () => { | |||
const projects = [ | |||
{ id: '1', key: 'a', name: 'A', qualifier: 'TRK' }, | |||
{ id: '2', key: 'b', name: 'B', qualifier: 'TRK' } | |||
]; | |||
const selection = ['a']; | |||
const result = shallow( | |||
<Projects | |||
organization={{ key: 'foo' }} | |||
projects={projects} | |||
selection={selection} | |||
refresh={jest.fn()} | |||
/> | |||
); | |||
expect(result.find('tr').length).toBe(2); | |||
expect(result.find(Checkbox).filterWhere(n => n.prop('checked')).length).toBe(1); | |||
}); |
@@ -1,37 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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 ModalForm from '../../components/common/modal-form'; | |||
export default ModalForm.extend({ | |||
onRender() { | |||
ModalForm.prototype.onRender.apply(this, arguments); | |||
this.$('[data-toggle="tooltip"]').tooltip({ container: 'body', placement: 'bottom' }); | |||
}, | |||
onDestroy() { | |||
ModalForm.prototype.onDestroy.apply(this, arguments); | |||
this.$('[data-toggle="tooltip"]').tooltip('destroy'); | |||
}, | |||
onFormSubmit() { | |||
ModalForm.prototype.onFormSubmit.apply(this, arguments); | |||
this.sendRequest(); | |||
} | |||
}); |
@@ -1,68 +0,0 @@ | |||
<form class="js-form" autocomplete="off"> | |||
<div class="modal-head"> | |||
<h2>Bulk Apply Permission Template</h2> | |||
</div> | |||
<div class="modal-body"> | |||
<div class="js-modal-messages"></div> | |||
{{#if done}} | |||
<div class="alert alert-success"> | |||
{{t 'projects_role.apply_template.success'}} | |||
</div> | |||
{{/if}} | |||
{{#unless done}} | |||
{{#notNull permissionTemplates}} | |||
<div class="modal-field"> | |||
<label for="project-permissions-template"> | |||
Template<em class="mandatory">*</em> | |||
</label> | |||
<select id="project-permissions-template"> | |||
{{#each permissionTemplates}} | |||
<option value="{{id}}">{{name}}</option> | |||
{{/each}} | |||
</select> | |||
</div> | |||
{{else}} | |||
<i class="spinner"></i> | |||
{{/notNull}} | |||
<div class="modal-field"> | |||
<label>Apply To</label> | |||
<ul style="padding-top: 4px;"> | |||
{{#if selectionTotal}} | |||
<li> | |||
<input value="selected" id="apply-to-selected" name="apply-to" | |||
type="radio" checked> | |||
<label | |||
for="apply-to-selected" | |||
style="float: none; left: 0; display: inline; padding: 0;"> | |||
Only Selected ({{selectionTotal}}) | |||
</label> | |||
</li> | |||
{{/if}} | |||
<li> | |||
<input value="all" id="apply-to-all" name="apply-to" type="radio" | |||
{{#unless selectionTotal}}checked{{/unless}}> | |||
<label | |||
for="apply-to-all" | |||
style="float: none; left: 0; display: inline; padding: 0;"> | |||
All ({{total}}) | |||
</label> | |||
</li> | |||
</ul> | |||
</div> | |||
{{/unless}} | |||
</div> | |||
<div class="modal-foot"> | |||
{{#unless done}} | |||
{{#notNull permissionTemplates}} | |||
<button class="js-apply">Apply</button> | |||
{{/notNull}} | |||
{{/unless}} | |||
<a href="#" class="js-modal-close">Close</a> | |||
</div> | |||
</form> |
@@ -1,13 +0,0 @@ | |||
<form id="delete-project-form"> | |||
<div class="modal-head"> | |||
<h2>Delete Projects</h2> | |||
</div> | |||
<div class="modal-body"> | |||
<div class="js-modal-messages"></div> | |||
Are you sure you want to delete selected projects? | |||
</div> | |||
<div class="modal-foot"> | |||
<button id="delete-project-submit" class="button-red">Delete</button> | |||
<a href="#" class="js-modal-close" id="delete-project-cancel">Cancel</a> | |||
</div> | |||
</form> |
@@ -1,121 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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 ModalForm from '../../../components/common/modal-form'; | |||
import { | |||
applyTemplateToProject, | |||
bulkApplyTemplate, | |||
getPermissionTemplates | |||
} from '../../../api/permissions'; | |||
import Template from '../templates/BulkApplyTemplateTemplate.hbs'; | |||
export default ModalForm.extend({ | |||
template: Template, | |||
initialize() { | |||
this.loadPermissionTemplates(); | |||
this.done = false; | |||
}, | |||
loadPermissionTemplates() { | |||
const request = this.options.organization | |||
? getPermissionTemplates(this.options.organization.key) | |||
: getPermissionTemplates(); | |||
return request.then(r => { | |||
this.permissionTemplates = r.permissionTemplates; | |||
this.render(); | |||
}); | |||
}, | |||
onRender() { | |||
ModalForm.prototype.onRender.apply(this, arguments); | |||
this.$('#project-permissions-template').select2({ | |||
width: '250px', | |||
minimumResultsForSearch: 20 | |||
}); | |||
}, | |||
bulkApplyToAll(permissionTemplate) { | |||
const data = { templateId: permissionTemplate }; | |||
if (this.options.query) { | |||
data.q = this.options.query; | |||
} | |||
if (this.options.qualifier) { | |||
data.qualifier = this.options.qualifier; | |||
} | |||
if (this.options.organization) { | |||
data.organization = this.options.organization.key; | |||
} | |||
return bulkApplyTemplate(data); | |||
}, | |||
bulkApplyToSelected(permissionTemplate) { | |||
const { selection } = this.options; | |||
let lastRequest = Promise.resolve(); | |||
selection.forEach(projectKey => { | |||
const data = { templateId: permissionTemplate, projectKey }; | |||
if (this.options.organization) { | |||
data.organization = this.options.organization.key; | |||
} | |||
lastRequest = lastRequest.then(() => applyTemplateToProject(data)); | |||
}); | |||
return lastRequest; | |||
}, | |||
onFormSubmit() { | |||
ModalForm.prototype.onFormSubmit.apply(this, arguments); | |||
const permissionTemplate = this.$('#project-permissions-template').val(); | |||
const applyTo = this.$('[name="apply-to"]:checked').val(); | |||
this.disableForm(); | |||
const request = | |||
applyTo === 'all' | |||
? this.bulkApplyToAll(permissionTemplate) | |||
: this.bulkApplyToSelected(permissionTemplate); | |||
request | |||
.then(() => { | |||
this.trigger('done'); | |||
this.done = true; | |||
this.render(); | |||
}) | |||
.catch(e => { | |||
e.response.json().then(r => { | |||
this.showErrors(r.errors, r.warnings); | |||
this.enableForm(); | |||
}); | |||
}); | |||
}, | |||
serializeData() { | |||
return { | |||
permissionTemplates: this.permissionTemplates, | |||
selection: this.options.selection, | |||
selectionTotal: this.options.selection.length, | |||
total: this.options.total, | |||
done: this.done | |||
}; | |||
} | |||
}); |
@@ -17,48 +17,42 @@ | |||
* 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 React from 'react'; | |||
import * as React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import { debounce, uniq, without } from 'lodash'; | |||
import Header from './header'; | |||
import Search from './search'; | |||
import Projects from './projects'; | |||
import Header from './Header'; | |||
import Search from './Search'; | |||
import Projects from './Projects'; | |||
import CreateProjectForm from './CreateProjectForm'; | |||
import ListFooter from '../../components/controls/ListFooter'; | |||
import { PAGE_SIZE, TYPE } from './constants'; | |||
import { getComponents, getProvisioned, getGhosts, deleteComponents } from '../../api/components'; | |||
import { PAGE_SIZE, Type, Project } from './utils'; | |||
import { getComponents, getProvisioned, getGhosts } from '../../api/components'; | |||
import { Organization } from '../../app/types'; | |||
import { translate } from '../../helpers/l10n'; | |||
/*:: import type { Organization } from '../../store/organizations/duck'; */ | |||
/*:: | |||
type Props = {| | |||
hasProvisionPermission: boolean, | |||
onVisibilityChange: string => void, | |||
onRequestFail: Object => void, | |||
organization: Organization | |||
|}; | |||
*/ | |||
export interface Props { | |||
hasProvisionPermission?: boolean; | |||
onVisibilityChange: (visibility: string) => void; | |||
organization: Organization; | |||
topLevelQualifiers: string[]; | |||
} | |||
/*:: | |||
type State = { | |||
createProjectForm: boolean, | |||
ready: boolean, | |||
projects: Array<{ key: string }>, | |||
total: number, | |||
page: number, | |||
query: string, | |||
qualifiers: string, | |||
type: string, | |||
selection: Array<string> | |||
}; | |||
*/ | |||
interface State { | |||
createProjectForm: boolean; | |||
page: number; | |||
projects: Project[]; | |||
qualifiers: string; | |||
query: string; | |||
ready: boolean; | |||
selection: string[]; | |||
total: number; | |||
type: Type; | |||
} | |||
export default class Main extends React.PureComponent { | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
export default class App extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
constructor(props /*: Props */) { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
createProjectForm: false, | |||
@@ -68,86 +62,87 @@ export default class Main extends React.PureComponent { | |||
page: 1, | |||
query: '', | |||
qualifiers: 'TRK', | |||
type: TYPE.ALL, | |||
type: Type.All, | |||
selection: [] | |||
}; | |||
this.requestProjects = debounce(this.requestProjects, 250); | |||
} | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.requestProjects(); | |||
} | |||
getFilters = () => { | |||
const filters /*: { [string]: string | number } */ = { | |||
organization: this.props.organization.key, | |||
ps: PAGE_SIZE | |||
}; | |||
if (this.state.page !== 1) { | |||
filters.p = this.state.page; | |||
} | |||
if (this.state.query) { | |||
filters.q = this.state.query; | |||
} | |||
return filters; | |||
}; | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
getFilters = () => ({ | |||
organization: this.props.organization.key, | |||
p: this.state.page !== 1 ? this.state.page : undefined, | |||
ps: PAGE_SIZE, | |||
q: this.state.query ? this.state.query : undefined | |||
}); | |||
requestProjects = () => { | |||
switch (this.state.type) { | |||
case TYPE.ALL: | |||
case Type.All: | |||
this.requestAllProjects(); | |||
break; | |||
case TYPE.PROVISIONED: | |||
case Type.Provisioned: | |||
this.requestProvisioned(); | |||
break; | |||
case TYPE.GHOSTS: | |||
case Type.Ghosts: | |||
this.requestGhosts(); | |||
break; | |||
default: | |||
// should never happen | |||
} | |||
}; | |||
requestGhosts = () => { | |||
const data = this.getFilters(); | |||
getGhosts(data).then(r => { | |||
let projects = r.projects.map(project => ({ | |||
...project, | |||
id: project.uuid, | |||
qualifier: 'TRK' | |||
})); | |||
if (this.state.page > 1) { | |||
projects = [].concat(this.state.projects, projects); | |||
if (this.mounted) { | |||
let projects: Project[] = r.projects.map((project: any) => ({ | |||
...project, | |||
id: project.uuid, | |||
qualifier: 'TRK' | |||
})); | |||
if (this.state.page > 1) { | |||
projects = [...this.state.projects, ...projects]; | |||
} | |||
this.setState({ ready: true, projects, selection: [], total: r.total }); | |||
} | |||
this.setState({ ready: true, projects, total: r.total }); | |||
}); | |||
}; | |||
requestProvisioned = () => { | |||
const data = this.getFilters(); | |||
getProvisioned(data).then(r => { | |||
let projects = r.projects.map(project => ({ | |||
...project, | |||
id: project.uuid, | |||
qualifier: 'TRK' | |||
})); | |||
if (this.state.page > 1) { | |||
projects = [].concat(this.state.projects, projects); | |||
if (this.mounted) { | |||
let projects: Project[] = r.projects.map((project: any) => ({ | |||
...project, | |||
id: project.uuid, | |||
qualifier: 'TRK' | |||
})); | |||
if (this.state.page > 1) { | |||
projects = [...this.state.projects, ...projects]; | |||
} | |||
this.setState({ ready: true, projects, selection: [], total: r.paging.total }); | |||
} | |||
this.setState({ ready: true, projects, total: r.paging.total }); | |||
}); | |||
}; | |||
requestAllProjects = () => { | |||
const data = this.getFilters(); | |||
data.qualifiers = this.state.qualifiers; | |||
Object.assign(data, { qualifiers: this.state.qualifiers }); | |||
getComponents(data).then(r => { | |||
let projects = r.components; | |||
if (this.state.page > 1) { | |||
projects = [].concat(this.state.projects, projects); | |||
if (this.mounted) { | |||
let projects: Project[] = r.components; | |||
if (this.state.page > 1) { | |||
projects = [...this.state.projects, ...projects]; | |||
} | |||
this.setState({ ready: true, projects, selection: [], total: r.paging.total }); | |||
} | |||
this.setState({ ready: true, projects, total: r.paging.total }); | |||
}); | |||
}; | |||
@@ -155,53 +150,31 @@ export default class Main extends React.PureComponent { | |||
this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects); | |||
}; | |||
onSearch = (query /*: string */) => { | |||
this.setState( | |||
{ | |||
ready: false, | |||
page: 1, | |||
query, | |||
selection: [] | |||
}, | |||
this.requestProjects | |||
); | |||
onSearch = (query: string) => { | |||
this.setState({ ready: false, page: 1, query, selection: [] }, this.requestProjects); | |||
}; | |||
onTypeChanged = (newType /*: string */) => { | |||
onTypeChanged = (newType: Type) => { | |||
this.setState( | |||
{ | |||
ready: false, | |||
page: 1, | |||
query: '', | |||
type: newType, | |||
qualifiers: 'TRK', | |||
selection: [] | |||
}, | |||
{ ready: false, page: 1, query: '', type: newType, qualifiers: 'TRK', selection: [] }, | |||
this.requestProjects | |||
); | |||
}; | |||
onQualifierChanged = (newQualifier /*: string */) => { | |||
onQualifierChanged = (newQualifier: string) => { | |||
this.setState( | |||
{ | |||
ready: false, | |||
page: 1, | |||
query: '', | |||
type: TYPE.ALL, | |||
qualifiers: newQualifier, | |||
selection: [] | |||
}, | |||
{ ready: false, page: 1, query: '', type: Type.All, qualifiers: newQualifier, selection: [] }, | |||
this.requestProjects | |||
); | |||
}; | |||
onProjectSelected = (project /*: { key: string } */) => { | |||
const newSelection = uniq([].concat(this.state.selection, project.key)); | |||
onProjectSelected = (project: string) => { | |||
const newSelection = uniq([...this.state.selection, project]); | |||
this.setState({ selection: newSelection }); | |||
}; | |||
onProjectDeselected = (project /*: { key: string } */) => { | |||
const newSelection = without(this.state.selection, project.key); | |||
onProjectDeselected = (project: string) => { | |||
const newSelection = without(this.state.selection, project); | |||
this.setState({ selection: newSelection }); | |||
}; | |||
@@ -214,18 +187,6 @@ export default class Main extends React.PureComponent { | |||
this.setState({ selection: [] }); | |||
}; | |||
deleteProjects = () => { | |||
this.setState({ ready: false }); | |||
const projects = this.state.selection.join(','); | |||
const data = { | |||
organization: this.props.organization.key, | |||
projects | |||
}; | |||
deleteComponents(data).then(() => { | |||
this.setState({ page: 1, selection: [] }, this.requestProjects); | |||
}); | |||
}; | |||
openCreateProjectForm = () => { | |||
this.setState({ createProjectForm: true }); | |||
}; | |||
@@ -249,18 +210,17 @@ export default class Main extends React.PureComponent { | |||
<Search | |||
{...this.props} | |||
{...this.state} | |||
onSearch={this.onSearch} | |||
onTypeChanged={this.onTypeChanged} | |||
onQualifierChanged={this.onQualifierChanged} | |||
onAllSelected={this.onAllSelected} | |||
onAllDeselected={this.onAllDeselected} | |||
deleteProjects={this.deleteProjects} | |||
onDeleteProjects={this.requestProjects} | |||
onQualifierChanged={this.onQualifierChanged} | |||
onSearch={this.onSearch} | |||
onTypeChanged={this.onTypeChanged} | |||
/> | |||
<Projects | |||
ready={this.state.ready} | |||
projects={this.state.projects} | |||
refresh={this.requestProjects} | |||
selection={this.state.selection} | |||
onProjectSelected={this.onProjectSelected} | |||
onProjectDeselected={this.onProjectDeselected} | |||
@@ -278,7 +238,6 @@ export default class Main extends React.PureComponent { | |||
<CreateProjectForm | |||
onClose={this.closeCreateProjectForm} | |||
onProjectCreated={this.requestProjects} | |||
onRequestFail={this.props.onRequestFail} | |||
organization={this.props.organization} | |||
/>} | |||
</div> |
@@ -17,16 +17,28 @@ | |||
* 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 * as React from 'react'; | |||
import { connect } from 'react-redux'; | |||
import Main from './main'; | |||
import App from './App'; | |||
import { Organization } from '../../app/types'; | |||
import { onFail } from '../../store/rootActions'; | |||
import { getAppState, getOrganizationByKey } from '../../store/rootReducer'; | |||
import { receiveOrganizations } from '../../store/organizations/duck'; | |||
import { changeProjectVisibility } from '../../api/organizations'; | |||
import { fetchOrganization } from '../../apps/organizations/actions'; | |||
class AppContainer extends React.PureComponent { | |||
interface Props { | |||
appState: { | |||
defaultOrganization: string; | |||
qualifiers: string[]; | |||
}; | |||
fetchOrganization: (organization: string) => void; | |||
onVisibilityChange: (organization: Organization, visibility: string) => void; | |||
onRequestFail: (error: any) => void; | |||
organization?: Organization; | |||
} | |||
class AppContainer extends React.PureComponent<Props> { | |||
componentDidMount() { | |||
// if there is no organization, that means we are in the global scope | |||
// let's fetch defails for the default organization in this case | |||
@@ -35,12 +47,10 @@ class AppContainer extends React.PureComponent { | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleVisibilityChange = visibility => { | |||
this.props.onVisibilityChange(this.props.organization, visibility); | |||
handleVisibilityChange = (visibility: string) => { | |||
if (this.props.organization) { | |||
this.props.onVisibilityChange(this.props.organization, visibility); | |||
} | |||
}; | |||
render() { | |||
@@ -53,24 +63,25 @@ class AppContainer extends React.PureComponent { | |||
const topLevelQualifiers = organization.isDefault ? this.props.appState.qualifiers : ['TRK']; | |||
return ( | |||
<Main | |||
<App | |||
hasProvisionPermission={organization.canProvisionProjects} | |||
topLevelQualifiers={topLevelQualifiers} | |||
onVisibilityChange={this.handleVisibilityChange} | |||
onRequestFail={this.props.onRequestFail} | |||
organization={organization} | |||
topLevelQualifiers={topLevelQualifiers} | |||
/> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state, ownProps) => ({ | |||
const mapStateToProps = (state: any, ownProps: Props) => ({ | |||
appState: getAppState(state), | |||
organization: | |||
ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization) | |||
}); | |||
const onVisibilityChange = (organization, visibility) => dispatch => { | |||
const onVisibilityChange = (organization: Organization, visibility: string) => ( | |||
dispatch: Function | |||
) => { | |||
const currentVisibility = organization.projectVisibility; | |||
dispatch(receiveOrganizations([{ ...organization, projectVisibility: visibility }])); | |||
changeProjectVisibility(organization.key, visibility).catch(error => { | |||
@@ -79,11 +90,10 @@ const onVisibilityChange = (organization, visibility) => dispatch => { | |||
}); | |||
}; | |||
const mapDispatchToProps = dispatch => ({ | |||
fetchOrganization: key => dispatch(fetchOrganization(key)), | |||
onVisibilityChange: (organization, visibility) => | |||
dispatch(onVisibilityChange(organization, visibility)), | |||
onRequestFail: error => onFail(dispatch)(error) | |||
const mapDispatchToProps = (dispatch: Function) => ({ | |||
fetchOrganization: (key: string) => dispatch(fetchOrganization(key)), | |||
onVisibilityChange: (organization: Organization, visibility: string) => | |||
dispatch(onVisibilityChange(organization, visibility)) | |||
}); | |||
export default connect(mapStateToProps, mapDispatchToProps)(AppContainer); | |||
export default connect<any, any, any>(mapStateToProps, mapDispatchToProps)(AppContainer); |
@@ -0,0 +1,217 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 Modal from 'react-modal'; | |||
import * as Select from 'react-select'; | |||
import { Type } from './utils'; | |||
import { | |||
getPermissionTemplates, | |||
PermissionTemplate, | |||
bulkApplyTemplate, | |||
applyTemplateToProject | |||
} from '../../api/permissions'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
export interface Props { | |||
onClose: () => void; | |||
organization: string; | |||
qualifier: string; | |||
query: string; | |||
selection: string[]; | |||
total: number; | |||
type: Type; | |||
} | |||
interface State { | |||
done: boolean; | |||
loading: boolean; | |||
permissionTemplate?: string; | |||
permissionTemplates?: PermissionTemplate[]; | |||
submitting: boolean; | |||
} | |||
export default class BulkApplyTemplateModal extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { done: false, loading: true, submitting: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.loadPermissionTemplates(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
loadPermissionTemplates() { | |||
this.setState({ loading: true }); | |||
getPermissionTemplates(this.props.organization).then( | |||
({ permissionTemplates }) => { | |||
if (this.mounted) { | |||
this.setState({ | |||
loading: false, | |||
permissionTemplate: | |||
permissionTemplates.length > 0 ? permissionTemplates[0].id : undefined, | |||
permissionTemplates: permissionTemplates | |||
}); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
} | |||
bulkApplyToAll = (permissionTemplate: string) => { | |||
const data = { | |||
organization: this.props.organization, | |||
q: this.props.query ? this.props.query : undefined, | |||
qualifier: this.props.qualifier, | |||
templateId: permissionTemplate | |||
}; | |||
return bulkApplyTemplate(data); | |||
}; | |||
bulkApplyToSelected = (permissionTemplate: string) => { | |||
const { selection } = this.props; | |||
let lastRequest = Promise.resolve(); | |||
selection.forEach(projectKey => { | |||
const data = { | |||
organization: this.props.organization, | |||
projectKey, | |||
templateId: permissionTemplate | |||
}; | |||
lastRequest = lastRequest.then(() => applyTemplateToProject(data)); | |||
}); | |||
return lastRequest; | |||
}; | |||
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.props.onClose(); | |||
}; | |||
handleConfirmClick = () => { | |||
const { permissionTemplate } = this.state; | |||
if (permissionTemplate) { | |||
this.setState({ submitting: true }); | |||
const request = this.props.selection.length | |||
? this.bulkApplyToSelected(permissionTemplate) | |||
: this.bulkApplyToAll(permissionTemplate); | |||
request.then( | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ done: true, submitting: false }); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ submitting: false }); | |||
} | |||
} | |||
); | |||
} | |||
}; | |||
handlePermissionTemplateChange = ({ value }: { value: string }) => { | |||
this.setState({ permissionTemplate: value }); | |||
}; | |||
renderWarning = () => { | |||
return this.props.selection.length | |||
? <div className="alert alert-info"> | |||
{translateWithParameters( | |||
'permission_templates.bulk_apply_permission_template.apply_to_selected', | |||
this.props.selection.length | |||
)} | |||
</div> | |||
: <div className="alert alert-warning"> | |||
{translateWithParameters( | |||
'permission_templates.bulk_apply_permission_template.apply_to_all', | |||
this.props.total | |||
)} | |||
</div>; | |||
}; | |||
renderSelect = () => | |||
<div className="modal-field"> | |||
<label> | |||
{translate('template')} | |||
<em className="mandatory">*</em> | |||
</label> | |||
<Select | |||
clearable={false} | |||
disabled={this.state.submitting} | |||
onChange={this.handlePermissionTemplateChange} | |||
options={this.state.permissionTemplates!.map(t => ({ label: t.name, value: t.id }))} | |||
searchable={false} | |||
value={this.state.permissionTemplate} | |||
/> | |||
</div>; | |||
render() { | |||
const { done, loading, permissionTemplates, submitting } = this.state; | |||
const header = translate('permission_templates.bulk_apply_permission_template'); | |||
return ( | |||
<Modal | |||
isOpen={true} | |||
contentLabel={header} | |||
className="modal" | |||
overlayClassName="modal-overlay" | |||
onRequestClose={this.props.onClose}> | |||
<header className="modal-head"> | |||
<h2> | |||
{header} | |||
</h2> | |||
</header> | |||
<div className="modal-body"> | |||
{done && | |||
<div className="alert alert-success"> | |||
{translate('projects_role.apply_template.success')} | |||
</div>} | |||
{loading && <i className="spinner" />} | |||
{!loading && !done && permissionTemplates && this.renderWarning()} | |||
{!loading && !done && permissionTemplates && this.renderSelect()} | |||
</div> | |||
<footer className="modal-foot"> | |||
{submitting && <i className="spinner spacer-right" />} | |||
{!loading && | |||
!done && | |||
permissionTemplates && | |||
<button disabled={submitting} onClick={this.handleConfirmClick}> | |||
{translate('apply')} | |||
</button>} | |||
<a className="js-modal-close" href="#" onClick={this.handleCancelClick}> | |||
{done ? translate('close') : translate('cancel')} | |||
</a> | |||
</footer> | |||
</Modal> | |||
); | |||
} | |||
} |
@@ -17,53 +17,45 @@ | |||
* 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 React from 'react'; | |||
import * as React from 'react'; | |||
import Modal from 'react-modal'; | |||
import classNames from 'classnames'; | |||
import * as classNames from 'classnames'; | |||
import { Organization } from '../../app/types'; | |||
import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox'; | |||
import { translate } from '../../helpers/l10n'; | |||
/*:: import type { Organization } from '../../store/organizations/duck'; */ | |||
import { Visibility } from './utils'; | |||
/*:: | |||
type Props = { | |||
onClose: () => void, | |||
onConfirm: string => void, | |||
organization: Organization | |||
}; | |||
*/ | |||
/*:: | |||
type State = { | |||
visibility: string | |||
}; | |||
*/ | |||
export interface Props { | |||
onClose: () => void; | |||
onConfirm: (visiblity: Visibility) => void; | |||
organization: Organization; | |||
} | |||
export default class ChangeVisibilityForm extends React.PureComponent { | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
interface State { | |||
visibility: Visibility; | |||
} | |||
constructor(props /*: Props */) { | |||
export default class ChangeVisibilityForm extends React.PureComponent<Props, State> { | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { visibility: props.organization.projectVisibility }; | |||
this.state = { visibility: props.organization.projectVisibility as Visibility }; | |||
} | |||
handleCancelClick = (event /*: Event */) => { | |||
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.props.onClose(); | |||
}; | |||
handleConfirmClick = (event /*: Event */) => { | |||
handleConfirmClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { | |||
event.preventDefault(); | |||
this.props.onConfirm(this.state.visibility); | |||
this.props.onClose(); | |||
}; | |||
handleVisibilityClick = (visibility /*: string */) => ( | |||
event /*: Event & { currentTarget: HTMLElement } */ | |||
) => { | |||
handleVisibilityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
const visibility = event.currentTarget.dataset.visibility as Visibility; | |||
this.setState({ visibility }); | |||
}; | |||
@@ -84,10 +76,10 @@ export default class ChangeVisibilityForm extends React.PureComponent { | |||
</header> | |||
<div className="modal-body"> | |||
{['public', 'private'].map(visibility => | |||
{[Visibility.Public, Visibility.Private].map(visibility => | |||
<div className="big-spacer-bottom" key={visibility}> | |||
<p> | |||
{visibility === 'private' && !canUpdateProjectsVisibilityToPrivate | |||
{visibility === Visibility.Private && !canUpdateProjectsVisibilityToPrivate | |||
? <span className="text-muted cursor-not-allowed"> | |||
<i | |||
className={classNames('icon-radio', 'spacer-right', { | |||
@@ -98,8 +90,9 @@ export default class ChangeVisibilityForm extends React.PureComponent { | |||
</span> | |||
: <a | |||
className="link-base-color link-no-underline" | |||
data-visibility={visibility} | |||
href="#" | |||
onClick={this.handleVisibilityClick(visibility)}> | |||
onClick={this.handleVisibilityClick}> | |||
<i | |||
className={classNames('icon-radio', 'spacer-right', { | |||
'is-checked': this.state.visibility === visibility | |||
@@ -122,10 +115,10 @@ export default class ChangeVisibilityForm extends React.PureComponent { | |||
</div> | |||
<footer className="modal-foot"> | |||
<button onClick={this.handleConfirmClick}> | |||
<button className="js-confirm" onClick={this.handleConfirmClick}> | |||
{translate('organization.change_visibility_form.submit')} | |||
</button> | |||
<a href="#" onClick={this.handleCancelClick}> | |||
<a className="js-modal-close" href="#" onClick={this.handleCancelClick}> | |||
{translate('cancel')} | |||
</a> | |||
</footer> |
@@ -17,50 +17,44 @@ | |||
* 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 React from 'react'; | |||
import * as React from 'react'; | |||
import Modal from 'react-modal'; | |||
import { Link } from 'react-router'; | |||
import { Organization } from '../../app/types'; | |||
import UpgradeOrganizationBox from '../../components/common/UpgradeOrganizationBox'; | |||
import VisibilitySelector from '../../components/common/VisibilitySelector'; | |||
import { createProject } from '../../api/components'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { getProjectUrl } from '../../helpers/urls'; | |||
/*:: import type { Organization } from '../../store/organizations/duck'; */ | |||
/*:: | |||
type Props = {| | |||
onClose: () => void, | |||
onProjectCreated: () => void, | |||
onRequestFail: Object => void, | |||
organization?: Organization | |||
|}; | |||
*/ | |||
/*:: | |||
type State = { | |||
branch: string, | |||
createdProject?: Object, | |||
key: string, | |||
loading: boolean, | |||
name: string, | |||
visibility: string | |||
}; | |||
*/ | |||
export default class CreateProjectForm extends React.PureComponent { | |||
/*:: mounted: boolean; */ | |||
/*:: props: Props; */ | |||
/*:: state: State; */ | |||
constructor(props /*: Props */) { | |||
interface Props { | |||
onClose: () => void; | |||
onProjectCreated: () => void; | |||
organization: Organization; | |||
} | |||
interface State { | |||
branch: string; | |||
createdProject?: { key: string; name: string }; | |||
key: string; | |||
loading: boolean; | |||
name: string; | |||
visibility: string; | |||
// add index declaration to be able to do `this.setState({ [name]: value });` | |||
[x: string]: any; | |||
} | |||
export default class CreateProjectForm extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
constructor(props: Props) { | |||
super(props); | |||
this.state = { | |||
branch: '', | |||
key: '', | |||
loading: false, | |||
name: '', | |||
visibility: props.organization ? props.organization.projectVisibility : 'public' | |||
visibility: props.organization.projectVisibility | |||
}; | |||
} | |||
@@ -72,32 +66,30 @@ export default class CreateProjectForm extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
handleCancelClick = (event /*: Event */) => { | |||
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.props.onClose(); | |||
}; | |||
handleInputChange = (event /*: { currentTarget: HTMLInputElement } */) => { | |||
handleInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => { | |||
const { name, value } = event.currentTarget; | |||
this.setState({ [name]: value }); | |||
}; | |||
handleVisibilityChange = (visibility /*: string */) => { | |||
handleVisibilityChange = (visibility: string) => { | |||
this.setState({ visibility }); | |||
}; | |||
handleFormSubmit = (event /*: Event */) => { | |||
handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
const data /*: { [string]: string } */ = { | |||
const data = { | |||
name: this.state.name, | |||
branch: this.state.branch, | |||
organization: this.props.organization && this.props.organization.key, | |||
project: this.state.key, | |||
visibility: this.state.visibility | |||
}; | |||
if (this.props.organization) { | |||
data.organization = this.props.organization.key; | |||
} | |||
this.setState({ loading: true }); | |||
createProject(data).then( | |||
@@ -165,7 +157,7 @@ export default class CreateProjectForm extends React.PureComponent { | |||
<input | |||
autoFocus={true} | |||
id="create-project-name" | |||
maxLength="2000" | |||
maxLength={2000} | |||
name="name" | |||
onChange={this.handleInputChange} | |||
required={true} | |||
@@ -179,7 +171,7 @@ export default class CreateProjectForm extends React.PureComponent { | |||
</label> | |||
<input | |||
id="create-project-branch" | |||
maxLength="200" | |||
maxLength={200} | |||
name="branch" | |||
onChange={this.handleInputChange} | |||
type="text" | |||
@@ -193,7 +185,7 @@ export default class CreateProjectForm extends React.PureComponent { | |||
</label> | |||
<input | |||
id="create-project-key" | |||
maxLength="400" | |||
maxLength={400} | |||
name="key" | |||
onChange={this.handleInputChange} | |||
required={true} | |||
@@ -203,18 +195,15 @@ export default class CreateProjectForm extends React.PureComponent { | |||
</div> | |||
<div className="modal-field"> | |||
<label> | |||
{' '}{translate('visibility')}{' '} | |||
{translate('visibility')} | |||
</label> | |||
<VisibilitySelector | |||
canTurnToPrivate={ | |||
organization == null || organization.canUpdateProjectsVisibilityToPrivate | |||
} | |||
canTurnToPrivate={organization.canUpdateProjectsVisibilityToPrivate} | |||
className="little-spacer-top" | |||
onChange={this.handleVisibilityChange} | |||
visibility={this.state.visibility} | |||
/> | |||
{organization != null && | |||
!organization.canUpdateProjectsVisibilityToPrivate && | |||
{!organization.canUpdateProjectsVisibilityToPrivate && | |||
<div className="spacer-top"> | |||
<UpgradeOrganizationBox organization={organization.key} /> | |||
</div>} |
@@ -0,0 +1,105 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 Modal from 'react-modal'; | |||
import { deleteComponents } from '../../api/components'; | |||
import { translate } from '../../helpers/l10n'; | |||
export interface Props { | |||
onClose: () => void; | |||
onConfirm: () => void; | |||
organization: string; | |||
qualifier: string; | |||
selection: string[]; | |||
} | |||
interface State { | |||
loading: boolean; | |||
} | |||
export default class DeleteModal extends React.PureComponent<Props, State> { | |||
mounted: boolean; | |||
state: State = { loading: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleCancelClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.props.onClose(); | |||
}; | |||
handleConfirmClick = () => { | |||
this.setState({ loading: true }); | |||
deleteComponents(this.props.selection, this.props.organization).then( | |||
() => { | |||
if (this.mounted) { | |||
this.props.onConfirm(); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
}; | |||
render() { | |||
const header = translate('qualifiers.delete', this.props.qualifier); | |||
return ( | |||
<Modal | |||
isOpen={true} | |||
contentLabel={header} | |||
className="modal" | |||
overlayClassName="modal-overlay" | |||
onRequestClose={this.props.onClose}> | |||
<header className="modal-head"> | |||
<h2> | |||
{header} | |||
</h2> | |||
</header> | |||
<div className="modal-body"> | |||
{translate('qualifiers.delete_confirm', this.props.qualifier)} | |||
</div> | |||
<footer className="modal-foot"> | |||
{this.state.loading && <i className="spinner spacer-right" />} | |||
<button | |||
className="button-red" | |||
disabled={this.state.loading} | |||
onClick={this.handleConfirmClick}> | |||
{translate('delete')} | |||
</button> | |||
<a className="js-modal-close" href="#" onClick={this.handleCancelClick}> | |||
{translate('cancel')} | |||
</a> | |||
</footer> | |||
</Modal> | |||
); | |||
} | |||
} |
@@ -17,37 +17,32 @@ | |||
* 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 React from 'react'; | |||
import * as React from 'react'; | |||
import ChangeVisibilityForm from './ChangeVisibilityForm'; | |||
import { Visibility } from './utils'; | |||
import { Organization } from '../../app/types'; | |||
import { translate } from '../../helpers/l10n'; | |||
/*:: import type { Organization } from '../../store/organizations/duck'; */ | |||
/*:: | |||
type Props = {| | |||
hasProvisionPermission: boolean, | |||
onProjectCreate: () => void, | |||
onVisibilityChange: string => void, | |||
organization: Organization | |||
|}; | |||
*/ | |||
export interface Props { | |||
hasProvisionPermission?: boolean; | |||
onProjectCreate: () => void; | |||
onVisibilityChange: (visibility: Visibility) => void; | |||
organization: Organization; | |||
} | |||
/*:: | |||
type State = { | |||
visibilityForm: boolean | |||
}; | |||
*/ | |||
interface State { | |||
visibilityForm: boolean; | |||
} | |||
export default class Header extends React.PureComponent { | |||
/*:: props: Props; */ | |||
state /*: State */ = { visibilityForm: false }; | |||
export default class Header extends React.PureComponent<Props, State> { | |||
state: State = { visibilityForm: false }; | |||
handleCreateProjectClick = (event /*: Event */) => { | |||
handleCreateProjectClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { | |||
event.preventDefault(); | |||
this.props.onProjectCreate(); | |||
}; | |||
handleChangeVisibilityClick = (event /*: Event */) => { | |||
handleChangeVisibilityClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
this.setState({ visibilityForm: true }); | |||
}; | |||
@@ -64,12 +59,13 @@ export default class Header extends React.PureComponent { | |||
<h1 className="page-title"> | |||
{translate('projects_management')} | |||
</h1> | |||
<div className="page-actions"> | |||
<span className="big-spacer-right"> | |||
{translate('organization.default_visibility_of_new_projects')}{' '} | |||
<strong>{translate('visibility', organization.projectVisibility)}</strong> | |||
<a | |||
className="spacer-left icon-edit" | |||
className="js-change-visibility spacer-left icon-edit" | |||
href="#" | |||
onClick={this.handleChangeVisibilityClick} | |||
/> | |||
@@ -79,6 +75,7 @@ export default class Header extends React.PureComponent { | |||
{translate('qualifiers.create.TRK')} | |||
</button>} | |||
</div> | |||
<p className="page-description"> | |||
{translate('projects_management.page.description')} | |||
</p> |
@@ -1,7 +1,7 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 | |||
@@ -17,60 +17,42 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import { getComponentPermissionsUrl } from '../../helpers/urls'; | |||
import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView'; | |||
import { Project, Visibility } from './utils'; | |||
import PrivateBadge from '../../components/common/PrivateBadge'; | |||
import Checkbox from '../../components/controls/Checkbox'; | |||
import QualifierIcon from '../../components/shared/QualifierIcon'; | |||
import PrivateBadge from '../../components/common/PrivateBadge'; | |||
import { translate } from '../../helpers/l10n'; | |||
import { getComponentPermissionsUrl } from '../../helpers/urls'; | |||
export default class Projects extends React.PureComponent { | |||
static propTypes = { | |||
projects: PropTypes.array.isRequired, | |||
selection: PropTypes.array.isRequired, | |||
organization: PropTypes.object.isRequired | |||
}; | |||
componentWillMount() { | |||
this.renderProject = this.renderProject.bind(this); | |||
} | |||
onProjectCheck(project, checked) { | |||
if (checked) { | |||
this.props.onProjectSelected(project); | |||
} else { | |||
this.props.onProjectDeselected(project); | |||
} | |||
} | |||
interface Props { | |||
onApplyTemplateClick: (project: Project) => void; | |||
onProjectCheck: (project: Project, checked: boolean) => void; | |||
project: Project; | |||
selected: boolean; | |||
} | |||
onApplyTemplateClick(project, e) { | |||
e.preventDefault(); | |||
e.target.blur(); | |||
new ApplyTemplateView({ | |||
project, | |||
organization: this.props.organization | |||
}).render(); | |||
} | |||
export default class ProjectRow extends React.PureComponent<Props> { | |||
handleProjectCheck = (checked: boolean) => { | |||
this.props.onProjectCheck(this.props.project, checked); | |||
}; | |||
isProjectSelected(project) { | |||
return this.props.selection.indexOf(project.key) !== -1; | |||
} | |||
handleApplyTemplateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.props.onApplyTemplateClick(this.props.project); | |||
}; | |||
renderProject(project) { | |||
const permissionsUrl = getComponentPermissionsUrl(project.key); | |||
render() { | |||
const { project, selected } = this.props; | |||
return ( | |||
<tr key={project.key}> | |||
<tr> | |||
<td className="thin"> | |||
<Checkbox | |||
checked={this.isProjectSelected(project)} | |||
onCheck={this.onProjectCheck.bind(this, project)} | |||
/> | |||
<Checkbox checked={selected} onCheck={this.handleProjectCheck} /> | |||
</td> | |||
<td className="nowrap"> | |||
<Link | |||
to={{ pathname: '/dashboard', query: { id: project.key } }} | |||
@@ -78,14 +60,17 @@ export default class Projects extends React.PureComponent { | |||
<QualifierIcon qualifier={project.qualifier} /> <span>{project.name}</span> | |||
</Link> | |||
</td> | |||
<td className="nowrap"> | |||
<span className="note"> | |||
{project.key} | |||
</span> | |||
</td> | |||
<td className="width-20"> | |||
{project.visibility === 'private' && <PrivateBadge />} | |||
{project.visibility === Visibility.Private && <PrivateBadge />} | |||
</td> | |||
<td className="thin nowrap"> | |||
<div className="dropdown"> | |||
<button className="dropdown-toggle" data-toggle="dropdown"> | |||
@@ -93,12 +78,12 @@ export default class Projects extends React.PureComponent { | |||
</button> | |||
<ul className="dropdown-menu dropdown-menu-right"> | |||
<li> | |||
<Link to={permissionsUrl}> | |||
<Link to={getComponentPermissionsUrl(project.key)}> | |||
{translate('edit_permissions')} | |||
</Link> | |||
</li> | |||
<li> | |||
<a href="#" onClick={this.onApplyTemplateClick.bind(this, project)}> | |||
<a className="js-apply-template" href="#" onClick={this.handleApplyTemplateClick}> | |||
{translate('projects_role.apply_template')} | |||
</a> | |||
</li> | |||
@@ -108,16 +93,4 @@ export default class Projects extends React.PureComponent { | |||
</tr> | |||
); | |||
} | |||
render() { | |||
const className = classNames('data', 'zebra', { 'new-loading': !this.props.ready }); | |||
return ( | |||
<table className={className} id="projects-management-page-projects"> | |||
<tbody> | |||
{this.props.projects.map(this.renderProject)} | |||
</tbody> | |||
</table> | |||
); | |||
} | |||
} |
@@ -0,0 +1,68 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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 classNames from 'classnames'; | |||
import ProjectRow from './ProjectRow'; | |||
import { Project } from './utils'; | |||
import ApplyTemplateView from '../permissions/project/views/ApplyTemplateView'; | |||
import { Organization } from '../../app/types'; | |||
interface Props { | |||
onProjectDeselected: (project: string) => void; | |||
onProjectSelected: (project: string) => void; | |||
organization: Organization; | |||
projects: Project[]; | |||
ready?: boolean; | |||
selection: string[]; | |||
} | |||
export default class Projects extends React.PureComponent<Props> { | |||
onProjectCheck = (project: Project, checked: boolean) => { | |||
if (checked) { | |||
this.props.onProjectSelected(project.key); | |||
} else { | |||
this.props.onProjectDeselected(project.key); | |||
} | |||
}; | |||
onApplyTemplateClick = (project: Project) => { | |||
new ApplyTemplateView({ project, organization: this.props.organization }).render(); | |||
}; | |||
render() { | |||
return ( | |||
<table | |||
className={classNames('data', 'zebra', { 'new-loading': !this.props.ready })} | |||
id="projects-management-page-projects"> | |||
<tbody> | |||
{this.props.projects.map(project => | |||
<ProjectRow | |||
key={project.key} | |||
onApplyTemplateClick={this.onApplyTemplateClick} | |||
onProjectCheck={this.onProjectCheck} | |||
project={project} | |||
selected={this.props.selection.includes(project.key)} | |||
/> | |||
)} | |||
</tbody> | |||
</table> | |||
); | |||
} | |||
} |
@@ -17,38 +17,60 @@ | |||
* 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 * as React from 'react'; | |||
import { sortBy } from 'lodash'; | |||
import { TYPE, QUALIFIERS_ORDER } from './constants'; | |||
import DeleteView from './delete-view'; | |||
import BulkApplyTemplateView from './views/BulkApplyTemplateView'; | |||
import BulkApplyTemplateModal from './BulkApplyTemplateModal'; | |||
import DeleteModal from './DeleteModal'; | |||
import { Type, 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'; | |||
export default class Search extends React.PureComponent { | |||
static propTypes = { | |||
onSearch: PropTypes.func.isRequired | |||
}; | |||
export interface Props { | |||
onAllDeselected: () => void; | |||
onAllSelected: () => void; | |||
onDeleteProjects: () => void; | |||
onQualifierChanged: (qualifier: string) => void; | |||
onSearch: (query: string) => void; | |||
onTypeChanged: (type: Type) => void; | |||
organization: Organization; | |||
projects: Project[]; | |||
qualifiers: string; | |||
query: string; | |||
ready: boolean; | |||
selection: any[]; | |||
topLevelQualifiers: string[]; | |||
total: number; | |||
type: Type; | |||
} | |||
interface State { | |||
bulkApplyTemplateModal: boolean; | |||
deleteModal: boolean; | |||
} | |||
onSubmit = e => { | |||
e.preventDefault(); | |||
export default class Search extends React.PureComponent<Props, State> { | |||
input: HTMLInputElement; | |||
mounted: boolean; | |||
state: State = { bulkApplyTemplateModal: false, deleteModal: false }; | |||
onSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => { | |||
event.preventDefault(); | |||
this.search(); | |||
}; | |||
search = () => { | |||
const q = this.refs.input.value; | |||
search = (event?: React.SyntheticEvent<HTMLInputElement>) => { | |||
const q = event ? event.currentTarget.value : this.input.value; | |||
this.props.onSearch(q); | |||
}; | |||
getTypeOptions = () => { | |||
return [ | |||
{ value: TYPE.ALL, label: 'All' }, | |||
{ value: TYPE.PROVISIONED, label: 'Provisioned' }, | |||
{ value: TYPE.GHOSTS, label: 'Ghosts' } | |||
]; | |||
}; | |||
getTypeOptions = () => [ | |||
{ value: Type.All, label: 'All' }, | |||
{ value: Type.Provisioned, label: 'Provisioned' }, | |||
{ value: Type.Ghosts, label: 'Ghosts' } | |||
]; | |||
getQualifierOptions = () => { | |||
const options = this.props.topLevelQualifiers.map(q => { | |||
@@ -57,7 +79,7 @@ export default class Search extends React.PureComponent { | |||
return sortBy(options, option => QUALIFIERS_ORDER.indexOf(option.value)); | |||
}; | |||
onCheck = checked => { | |||
onCheck = (checked: boolean) => { | |||
if (checked) { | |||
this.props.onAllSelected(); | |||
} else { | |||
@@ -65,20 +87,29 @@ export default class Search extends React.PureComponent { | |||
} | |||
}; | |||
deleteProjects = () => { | |||
new DeleteView({ | |||
deleteProjects: this.props.deleteProjects | |||
}).render(); | |||
handleDeleteClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.setState({ deleteModal: true }); | |||
}; | |||
closeDeleteModal = () => { | |||
this.setState({ deleteModal: false }); | |||
}; | |||
bulkApplyTemplate = () => { | |||
new BulkApplyTemplateView({ | |||
total: this.props.total, | |||
selection: this.props.selection, | |||
query: this.props.query, | |||
qualifier: this.props.qualifier, | |||
organization: this.props.organization | |||
}).render(); | |||
handleDeleteConfirm = () => { | |||
this.closeDeleteModal(); | |||
this.props.onDeleteProjects(); | |||
}; | |||
handleBulkApplyTemplateClick = (event: React.SyntheticEvent<HTMLButtonElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.setState({ bulkApplyTemplateModal: true }); | |||
}; | |||
closeBulkApplyTemplateModal = () => { | |||
this.setState({ bulkApplyTemplateModal: false }); | |||
}; | |||
renderCheckbox = () => { | |||
@@ -93,7 +124,7 @@ export default class Search extends React.PureComponent { | |||
}; | |||
renderGhostsDescription = () => { | |||
if (this.props.type !== TYPE.GHOSTS || !this.props.ready) { | |||
if (this.props.type !== Type.Ghosts || !this.props.ready) { | |||
return null; | |||
} | |||
return ( | |||
@@ -120,8 +151,6 @@ export default class Search extends React.PureComponent { | |||
); | |||
}; | |||
renderSpinner = () => <i className="spinner" />; | |||
render() { | |||
const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0; | |||
return ( | |||
@@ -130,7 +159,7 @@ export default class Search extends React.PureComponent { | |||
<tbody> | |||
<tr> | |||
<td className="thin text-middle"> | |||
{this.props.ready ? this.renderCheckbox() : this.renderSpinner()} | |||
{this.props.ready ? this.renderCheckbox() : <i className="spinner" />} | |||
</td> | |||
{this.renderQualifierFilter()} | |||
<td className="thin nowrap text-middle"> | |||
@@ -149,7 +178,7 @@ export default class Search extends React.PureComponent { | |||
<input | |||
onChange={this.search} | |||
value={this.props.query} | |||
ref="input" | |||
ref={node => (this.input = node!)} | |||
className="search-box-input input-medium" | |||
type="search" | |||
placeholder="Search" | |||
@@ -159,12 +188,12 @@ export default class Search extends React.PureComponent { | |||
<td className="thin nowrap text-middle"> | |||
<button | |||
className="spacer-right js-bulk-apply-permission-template" | |||
onClick={this.bulkApplyTemplate}> | |||
onClick={this.handleBulkApplyTemplateClick}> | |||
{translate('permission_templates.bulk_apply_permission_template')} | |||
</button> | |||
<button | |||
onClick={this.deleteProjects} | |||
className="button-red" | |||
onClick={this.handleDeleteClick} | |||
className="js-delete button-red" | |||
disabled={!isSomethingSelected}> | |||
{translate('delete')} | |||
</button> | |||
@@ -173,6 +202,26 @@ export default class Search extends React.PureComponent { | |||
</tbody> | |||
</table> | |||
{this.renderGhostsDescription()} | |||
{this.state.bulkApplyTemplateModal && | |||
<BulkApplyTemplateModal | |||
onClose={this.closeBulkApplyTemplateModal} | |||
organization={this.props.organization.key} | |||
qualifier={this.props.qualifiers} | |||
query={this.props.query} | |||
selection={this.props.selection} | |||
total={this.props.total} | |||
type={this.props.type} | |||
/>} | |||
{this.state.deleteModal && | |||
<DeleteModal | |||
onClose={this.closeDeleteModal} | |||
onConfirm={this.handleDeleteConfirm} | |||
organization={this.props.organization.key} | |||
qualifier={this.props.qualifiers} | |||
selection={this.props.selection} | |||
/>} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,148 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
jest.mock('lodash', () => { | |||
const lodash = require.requireActual('lodash'); | |||
lodash.debounce = (fn: Function) => (...args: any[]) => fn(args); | |||
return lodash; | |||
}); | |||
jest.mock('../../../api/components', () => ({ | |||
getComponents: jest.fn(), | |||
getProvisioned: jest.fn(() => Promise.resolve({ paging: { total: 0 }, projects: [] })), | |||
getGhosts: jest.fn(() => Promise.resolve({ projects: [], total: 0 })) | |||
})); | |||
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>; | |||
const getGhosts = require('../../../api/components').getGhosts as jest.Mock<any>; | |||
const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; | |||
const defaultSearchParameters = { | |||
organization: 'org', | |||
p: undefined, | |||
ps: 50, | |||
q: undefined | |||
}; | |||
beforeEach(() => { | |||
getComponents | |||
.mockImplementation(() => Promise.resolve({ paging: { total: 0 }, components: [] })) | |||
.mockClear(); | |||
getProvisioned.mockClear(); | |||
getGhosts.mockClear(); | |||
}); | |||
it('fetches all projects on mount', () => { | |||
mountRender(); | |||
expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'TRK' }); | |||
}); | |||
it('changes type', () => { | |||
const wrapper = mountRender(); | |||
wrapper.find('Search').prop<Function>('onTypeChanged')(Type.Provisioned); | |||
expect(getProvisioned).lastCalledWith(defaultSearchParameters); | |||
wrapper.find('Search').prop<Function>('onTypeChanged')(Type.Ghosts); | |||
expect(getGhosts).lastCalledWith(defaultSearchParameters); | |||
}); | |||
it('changes qualifier and resets type', () => { | |||
const wrapper = mountRender(); | |||
wrapper.setState({ type: Type.Provisioned }); | |||
wrapper.find('Search').prop<Function>('onQualifierChanged')('VW'); | |||
expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'VW' }); | |||
}); | |||
it('searches', () => { | |||
const wrapper = mountRender(); | |||
wrapper.find('Search').prop<Function>('onSearch')('foo'); | |||
expect(getComponents).lastCalledWith({ ...defaultSearchParameters, q: 'foo', qualifiers: 'TRK' }); | |||
}); | |||
it('loads more', async () => { | |||
const wrapper = mountRender(); | |||
wrapper.find('ListFooter').prop<Function>('loadMore')(); | |||
expect(getComponents).lastCalledWith({ ...defaultSearchParameters, p: 2, qualifiers: 'TRK' }); | |||
}); | |||
it('selects and deselects projects', async () => { | |||
getComponents.mockImplementation(() => | |||
Promise.resolve({ paging: { total: 2 }, components: [{ key: 'foo' }, { key: 'bar' }] }) | |||
); | |||
const wrapper = mountRender(); | |||
await new Promise(setImmediate); | |||
wrapper.find('Projects').prop<Function>('onProjectSelected')('foo'); | |||
expect(wrapper.state('selection')).toEqual(['foo']); | |||
wrapper.find('Projects').prop<Function>('onProjectSelected')('bar'); | |||
expect(wrapper.state('selection')).toEqual(['foo', 'bar']); | |||
// should not select already selected project | |||
wrapper.find('Projects').prop<Function>('onProjectSelected')('bar'); | |||
expect(wrapper.state('selection')).toEqual(['foo', 'bar']); | |||
wrapper.find('Projects').prop<Function>('onProjectDeselected')('foo'); | |||
expect(wrapper.state('selection')).toEqual(['bar']); | |||
wrapper.find('Search').prop<Function>('onAllDeselected')(); | |||
expect(wrapper.state('selection')).toEqual([]); | |||
wrapper.find('Search').prop<Function>('onAllSelected')(); | |||
expect(wrapper.state('selection')).toEqual(['foo', 'bar']); | |||
}); | |||
it('creates project', () => { | |||
const wrapper = mountRender(); | |||
expect(wrapper.find('CreateProjectForm').exists()).toBeFalsy(); | |||
wrapper.find('Header').prop<Function>('onProjectCreate')(); | |||
expect(wrapper.find('CreateProjectForm').exists()).toBeTruthy(); | |||
wrapper.find('CreateProjectForm').prop<Function>('onProjectCreated')(); | |||
expect(getComponents.mock.calls).toHaveLength(2); | |||
wrapper.find('CreateProjectForm').prop<Function>('onClose')(); | |||
expect(wrapper.find('CreateProjectForm').exists()).toBeFalsy(); | |||
}); | |||
it('changes default project visibility', () => { | |||
const onVisibilityChange = jest.fn(); | |||
const wrapper = mountRender({ onVisibilityChange }); | |||
wrapper.find('Header').prop<Function>('onVisibilityChange')('private'); | |||
expect(onVisibilityChange).toBeCalledWith('private'); | |||
}); | |||
function mountRender(props?: { [P in keyof Props]?: Props[P] }) { | |||
return mount( | |||
<App | |||
hasProvisionPermission={true} | |||
onVisibilityChange={jest.fn()} | |||
organization={organization} | |||
topLevelQualifiers={['TRK', 'VW', 'APP']} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,128 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
jest.mock('../../../api/permissions', () => ({ | |||
applyTemplateToProject: jest.fn(), | |||
bulkApplyTemplate: jest.fn(), | |||
getPermissionTemplates: jest.fn() | |||
})); | |||
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') | |||
.applyTemplateToProject as jest.Mock<any>; | |||
const bulkApplyTemplate = require('../../../api/permissions').bulkApplyTemplate as jest.Mock<any>; | |||
const getPermissionTemplates = require('../../../api/permissions') | |||
.getPermissionTemplates as jest.Mock<any>; | |||
beforeEach(() => { | |||
applyTemplateToProject.mockImplementation(() => Promise.resolve()).mockClear(); | |||
bulkApplyTemplate.mockImplementation(() => Promise.resolve()).mockClear(); | |||
getPermissionTemplates | |||
.mockImplementation(() => Promise.resolve({ permissionTemplates: [] })) | |||
.mockClear(); | |||
}); | |||
it('fetches permission templates on mount', () => { | |||
mount(render()); | |||
expect(getPermissionTemplates).toBeCalledWith('org'); | |||
}); | |||
it('bulk applies template to all results', async () => { | |||
const wrapper = shallow(render()); | |||
(wrapper.instance() as BulkApplyTemplateModal).mounted = true; | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ | |||
loading: false, | |||
permissionTemplate: 'foo', | |||
permissionTemplates: [{ id: 'foo', name: 'Foo' }, { id: 'bar', name: 'Bar' }] | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('button')); | |||
expect(bulkApplyTemplate).toBeCalledWith({ | |||
organization: 'org', | |||
q: 'bla', | |||
qualifier: 'TRK', | |||
templateId: 'foo' | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
await new Promise(setImmediate); | |||
wrapper.update(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('bulk applies template to selected results', async () => { | |||
const wrapper = shallow(render({ selection: ['proj1', 'proj2'] })); | |||
(wrapper.instance() as BulkApplyTemplateModal).mounted = true; | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setState({ | |||
loading: false, | |||
permissionTemplate: 'foo', | |||
permissionTemplates: [{ id: 'foo', name: 'Foo' }, { id: 'bar', name: 'Bar' }] | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('button')); | |||
expect(wrapper).toMatchSnapshot(); | |||
await new Promise(setImmediate); | |||
expect(applyTemplateToProject.mock.calls).toHaveLength(2); | |||
expect(applyTemplateToProject).toBeCalledWith({ | |||
organization: 'org', | |||
projectKey: 'proj1', | |||
templateId: 'foo' | |||
}); | |||
expect(applyTemplateToProject).toBeCalledWith({ | |||
organization: 'org', | |||
projectKey: 'proj2', | |||
templateId: 'foo' | |||
}); | |||
wrapper.update(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('closes', () => { | |||
const onClose = jest.fn(); | |||
const wrapper = shallow(render({ onClose })); | |||
click(wrapper.find('.js-modal-close')); | |||
expect(onClose).toBeCalled(); | |||
}); | |||
function render(props?: { [P in keyof Props]?: Props[P] }) { | |||
return ( | |||
<BulkApplyTemplateModal | |||
onClose={jest.fn()} | |||
organization="org" | |||
qualifier="TRK" | |||
query="bla" | |||
selection={[]} | |||
total={17} | |||
type={Type.All} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,73 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 ChangeVisibilityForm, { Props } from '../ChangeVisibilityForm'; | |||
import { click } from '../../../helpers/testUtils'; | |||
const organization = { | |||
canUpdateProjectsVisibilityToPrivate: true, | |||
key: 'org', | |||
name: 'org', | |||
projectVisibility: 'public' | |||
}; | |||
it('renders disabled', () => { | |||
expect( | |||
shallowRender({ | |||
organization: { ...organization, canUpdateProjectsVisibilityToPrivate: false } | |||
}) | |||
).toMatchSnapshot(); | |||
}); | |||
it('closes', () => { | |||
const onClose = jest.fn(); | |||
const wrapper = shallowRender({ onClose }); | |||
click(wrapper.find('.js-modal-close')); | |||
expect(onClose).toBeCalled(); | |||
}); | |||
it('changes visibility', () => { | |||
const onConfirm = jest.fn(); | |||
const wrapper = shallowRender({ onConfirm }); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('a[data-visibility="private"]'), { | |||
currentTarget: { | |||
blur() {}, | |||
dataset: { visibility: 'private' } | |||
} | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('.js-confirm')); | |||
expect(onConfirm).toBeCalledWith('private'); | |||
}); | |||
function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { | |||
return shallow( | |||
<ChangeVisibilityForm | |||
onClose={jest.fn()} | |||
onConfirm={jest.fn()} | |||
organization={organization} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,72 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
jest.mock('../../../api/components', () => ({ | |||
createProject: jest.fn(({ name }: { name: string }) => | |||
Promise.resolve({ project: { key: name, name } }) | |||
) | |||
})); | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import CreateProjectForm from '../CreateProjectForm'; | |||
import { change, submit } from '../../../helpers/testUtils'; | |||
const createProject = require('../../../api/components').createProject as jest.Mock<any>; | |||
const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; | |||
it('creates project', async () => { | |||
const wrapper = shallow( | |||
<CreateProjectForm | |||
onClose={jest.fn()} | |||
onProjectCreated={jest.fn()} | |||
organization={organization} | |||
/> | |||
); | |||
(wrapper.instance() as CreateProjectForm).mounted = true; | |||
expect(wrapper).toMatchSnapshot(); | |||
change(wrapper.find('input[name="name"]'), 'name', { | |||
currentTarget: { name: 'name', value: 'name' } | |||
}); | |||
change(wrapper.find('input[name="branch"]'), 'branch', { | |||
currentTarget: { name: 'branch', value: 'branch' } | |||
}); | |||
change(wrapper.find('input[name="key"]'), 'key', { | |||
currentTarget: { name: 'key', value: 'key' } | |||
}); | |||
wrapper.find('VisibilitySelector').prop<Function>('onChange')('private'); | |||
wrapper.update(); | |||
expect(wrapper).toMatchSnapshot(); | |||
submit(wrapper.find('form')); | |||
expect(createProject).toBeCalledWith({ | |||
branch: 'branch', | |||
name: 'name', | |||
organization: 'org', | |||
project: 'key', | |||
visibility: 'private' | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
await new Promise(resolve => setImmediate(resolve)); | |||
wrapper.update(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,63 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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. | |||
*/ | |||
jest.mock('../../../api/components', () => ({ | |||
deleteComponents: jest.fn(() => Promise.resolve()) | |||
})); | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import DeleteModal, { Props } from '../DeleteModal'; | |||
import { click } from '../../../helpers/testUtils'; | |||
const deleteComponents = require('../../../api/components').deleteComponents as jest.Mock<any>; | |||
it('deletes projects', async () => { | |||
const onConfirm = jest.fn(); | |||
const wrapper = shallowRender({ onConfirm }); | |||
(wrapper.instance() as DeleteModal).mounted = true; | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('button')); | |||
expect(wrapper).toMatchSnapshot(); | |||
expect(deleteComponents).toBeCalledWith(['foo', 'bar'], 'org'); | |||
await new Promise(setImmediate); | |||
expect(onConfirm).toBeCalled(); | |||
}); | |||
it('closes', () => { | |||
const onClose = jest.fn(); | |||
const wrapper = shallowRender({ onClose }); | |||
click(wrapper.find('.js-modal-close')); | |||
expect(onClose).toBeCalled(); | |||
}); | |||
function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { | |||
return shallow( | |||
<DeleteModal | |||
onClose={jest.fn()} | |||
onConfirm={jest.fn()} | |||
organization="org" | |||
qualifier="TRK" | |||
selection={['foo', 'bar']} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,64 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 Header, { Props } from '../Header'; | |||
import { Visibility } from '../utils'; | |||
import { click } from '../../../helpers/testUtils'; | |||
const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; | |||
it('renders', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('creates project', () => { | |||
const onProjectCreate = jest.fn(); | |||
const wrapper = shallowRender({ onProjectCreate }); | |||
click(wrapper.find('#create-project')); | |||
expect(onProjectCreate).toBeCalledWith(); | |||
}); | |||
it('changes default visibility', () => { | |||
const onVisibilityChange = jest.fn(); | |||
const wrapper = shallowRender({ onVisibilityChange }); | |||
click(wrapper.find('.js-change-visibility')); | |||
const modalWrapper = wrapper.find('ChangeVisibilityForm'); | |||
expect(modalWrapper).toMatchSnapshot(); | |||
modalWrapper.prop<Function>('onConfirm')(Visibility.Private); | |||
expect(onVisibilityChange).toBeCalledWith(Visibility.Private); | |||
modalWrapper.prop<Function>('onClose')(); | |||
expect(wrapper.find('ChangeVisibilityForm').exists()).toBeFalsy(); | |||
}); | |||
function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { | |||
return shallow( | |||
<Header | |||
hasProvisionPermission={true} | |||
onProjectCreate={jest.fn()} | |||
onVisibilityChange={jest.fn()} | |||
organization={organization} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,61 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 ProjectRow from '../ProjectRow'; | |||
import { Visibility } from '../utils'; | |||
import { click } from '../../../helpers/testUtils'; | |||
const project = { | |||
key: 'project', | |||
name: 'Project', | |||
qualifier: 'TRK', | |||
visibility: Visibility.Private | |||
}; | |||
it('renders', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('checks project', () => { | |||
const onProjectCheck = jest.fn(); | |||
const wrapper = shallowRender({ onProjectCheck }); | |||
wrapper.find('Checkbox').prop<Function>('onCheck')(false); | |||
expect(onProjectCheck).toBeCalledWith(project, false); | |||
}); | |||
it('applies permission template', () => { | |||
const onApplyTemplateClick = jest.fn(); | |||
const wrapper = shallowRender({ onApplyTemplateClick }); | |||
click(wrapper.find('.js-apply-template')); | |||
expect(onApplyTemplateClick).toBeCalledWith(project); | |||
}); | |||
function shallowRender(props?: any) { | |||
return shallow( | |||
<ProjectRow | |||
onApplyTemplateClick={jest.fn()} | |||
onProjectCheck={jest.fn()} | |||
project={project} | |||
selected={true} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,67 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 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. | |||
*/ | |||
jest.mock('../../permissions/project/views/ApplyTemplateView'); | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import Projects from '../Projects'; | |||
import { Visibility } from '../utils'; | |||
import ApplyTemplateView from '../../permissions/project/views/ApplyTemplateView'; | |||
const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; | |||
const projects = [ | |||
{ key: 'a', name: 'A', qualifier: 'TRK', visibility: Visibility.Public }, | |||
{ key: 'b', name: 'B', qualifier: 'TRK', visibility: Visibility.Public } | |||
]; | |||
const selection = ['a']; | |||
it('renders list of projects', () => { | |||
expect(shallowRender({ projects, selection })).toMatchSnapshot(); | |||
}); | |||
it('selects and deselects project', () => { | |||
const onProjectDeselected = jest.fn(); | |||
const onProjectSelected = jest.fn(); | |||
const wrapper = shallowRender({ onProjectDeselected, onProjectSelected, projects }); | |||
wrapper.find('ProjectRow').first().prop<Function>('onProjectCheck')(projects[0], true); | |||
expect(onProjectSelected).toBeCalledWith('a'); | |||
wrapper.find('ProjectRow').first().prop<Function>('onProjectCheck')(projects[0], false); | |||
expect(onProjectDeselected).toBeCalledWith('a'); | |||
}); | |||
it('opens modal to apply permission template', () => { | |||
const wrapper = shallowRender({ projects }); | |||
wrapper.find('ProjectRow').first().prop<Function>('onApplyTemplateClick')(projects[0]); | |||
expect(ApplyTemplateView).toBeCalledWith({ organization, project: projects[0] }); | |||
}); | |||
function shallowRender(props?: any) { | |||
return shallow( | |||
<Projects | |||
onProjectDeselected={jest.fn()} | |||
onProjectSelected={jest.fn()} | |||
organization={organization} | |||
selection={[]} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,107 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 Search, { Props } from '../Search'; | |||
import { Type } from '../utils'; | |||
import { change, click } from '../../../helpers/testUtils'; | |||
const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; | |||
it('renders', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
}); | |||
it('render qualifiers filter', () => { | |||
expect(shallowRender({ topLevelQualifiers: ['TRK', 'VW', 'APP'] })).toMatchSnapshot(); | |||
}); | |||
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'); | |||
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('searches', () => { | |||
const onSearch = jest.fn(); | |||
const wrapper = shallowRender({ onSearch }); | |||
change(wrapper.find('input[type="search"]'), 'foo'); | |||
expect(onSearch).toBeCalledWith('foo'); | |||
}); | |||
it('checks all or none projects', () => { | |||
const onAllDeselected = jest.fn(); | |||
const onAllSelected = jest.fn(); | |||
const wrapper = shallowRender({ onAllDeselected, onAllSelected }); | |||
wrapper.find('Checkbox').prop<Function>('onCheck')(true); | |||
expect(onAllSelected).toBeCalled(); | |||
wrapper.find('Checkbox').prop<Function>('onCheck')(false); | |||
expect(onAllDeselected).toBeCalled(); | |||
}); | |||
it('deletes projects', () => { | |||
const onDeleteProjects = jest.fn(); | |||
const wrapper = shallowRender({ onDeleteProjects, selection: ['foo', 'bar'] }); | |||
click(wrapper.find('.js-delete')); | |||
expect(wrapper.find('DeleteModal')).toMatchSnapshot(); | |||
wrapper.find('DeleteModal').prop<Function>('onConfirm')(); | |||
expect(onDeleteProjects).toBeCalled(); | |||
}); | |||
it('bulk applies permission template', () => { | |||
const wrapper = shallowRender({}); | |||
click(wrapper.find('.js-bulk-apply-permission-template')); | |||
expect(wrapper.find('BulkApplyTemplateModal')).toMatchSnapshot(); | |||
wrapper.find('BulkApplyTemplateModal').prop<Function>('onClose')(); | |||
expect(wrapper.find('BulkApplyTemplateModal').exists()).toBeFalsy(); | |||
}); | |||
function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { | |||
return shallow( | |||
<Search | |||
onAllDeselected={jest.fn()} | |||
onAllSelected={jest.fn()} | |||
onDeleteProjects={jest.fn()} | |||
onQualifierChanged={jest.fn()} | |||
onSearch={jest.fn()} | |||
onTypeChanged={jest.fn()} | |||
organization={organization} | |||
projects={[]} | |||
qualifiers="TRK" | |||
query="" | |||
ready={true} | |||
selection={[]} | |||
topLevelQualifiers={['TRK']} | |||
total={0} | |||
type={Type.All} | |||
{...props} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,643 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`bulk applies template to all results 1`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="permission_templates.bulk_apply_permission_template" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
permission_templates.bulk_apply_permission_template | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<i | |||
className="spinner" | |||
/> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`bulk applies template to all results 2`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="permission_templates.bulk_apply_permission_template" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
permission_templates.bulk_apply_permission_template | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="alert alert-warning" | |||
> | |||
permission_templates.bulk_apply_permission_template.apply_to_all.17 | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
template | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<Select | |||
addLabelText="Add \\"{label}\\"?" | |||
arrowRenderer={[Function]} | |||
autosize={true} | |||
backspaceRemoves={true} | |||
backspaceToRemoveMessage="Press backspace to remove {label}" | |||
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} | |||
noResultsText="No results found" | |||
onBlurResetsInput={true} | |||
onChange={[Function]} | |||
onCloseResetsInput={true} | |||
optionComponent={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "Foo", | |||
"value": "foo", | |||
}, | |||
Object { | |||
"label": "Bar", | |||
"value": "bar", | |||
}, | |||
] | |||
} | |||
pageSize={5} | |||
placeholder="Select..." | |||
required={false} | |||
scrollMenuIntoView={true} | |||
searchable={false} | |||
simpleValue={false} | |||
tabSelectsValue={true} | |||
value="foo" | |||
valueComponent={[Function]} | |||
valueKey="value" | |||
/> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
disabled={false} | |||
onClick={[Function]} | |||
> | |||
apply | |||
</button> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`bulk applies template to all results 3`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="permission_templates.bulk_apply_permission_template" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
permission_templates.bulk_apply_permission_template | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="alert alert-warning" | |||
> | |||
permission_templates.bulk_apply_permission_template.apply_to_all.17 | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
template | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<Select | |||
addLabelText="Add \\"{label}\\"?" | |||
arrowRenderer={[Function]} | |||
autosize={true} | |||
backspaceRemoves={true} | |||
backspaceToRemoveMessage="Press backspace to remove {label}" | |||
clearAllText="Clear all" | |||
clearRenderer={[Function]} | |||
clearValueText="Clear value" | |||
clearable={false} | |||
deleteRemoves={true} | |||
delimiter="," | |||
disabled={true} | |||
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} | |||
noResultsText="No results found" | |||
onBlurResetsInput={true} | |||
onChange={[Function]} | |||
onCloseResetsInput={true} | |||
optionComponent={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "Foo", | |||
"value": "foo", | |||
}, | |||
Object { | |||
"label": "Bar", | |||
"value": "bar", | |||
}, | |||
] | |||
} | |||
pageSize={5} | |||
placeholder="Select..." | |||
required={false} | |||
scrollMenuIntoView={true} | |||
searchable={false} | |||
simpleValue={false} | |||
tabSelectsValue={true} | |||
value="foo" | |||
valueComponent={[Function]} | |||
valueKey="value" | |||
/> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<button | |||
disabled={true} | |||
onClick={[Function]} | |||
> | |||
apply | |||
</button> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`bulk applies template to all results 4`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="permission_templates.bulk_apply_permission_template" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
permission_templates.bulk_apply_permission_template | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="alert alert-success" | |||
> | |||
projects_role.apply_template.success | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
close | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`bulk applies template to selected results 1`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="permission_templates.bulk_apply_permission_template" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
permission_templates.bulk_apply_permission_template | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<i | |||
className="spinner" | |||
/> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`bulk applies template to selected results 2`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="permission_templates.bulk_apply_permission_template" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
permission_templates.bulk_apply_permission_template | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="alert alert-info" | |||
> | |||
permission_templates.bulk_apply_permission_template.apply_to_selected.2 | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
template | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<Select | |||
addLabelText="Add \\"{label}\\"?" | |||
arrowRenderer={[Function]} | |||
autosize={true} | |||
backspaceRemoves={true} | |||
backspaceToRemoveMessage="Press backspace to remove {label}" | |||
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} | |||
noResultsText="No results found" | |||
onBlurResetsInput={true} | |||
onChange={[Function]} | |||
onCloseResetsInput={true} | |||
optionComponent={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "Foo", | |||
"value": "foo", | |||
}, | |||
Object { | |||
"label": "Bar", | |||
"value": "bar", | |||
}, | |||
] | |||
} | |||
pageSize={5} | |||
placeholder="Select..." | |||
required={false} | |||
scrollMenuIntoView={true} | |||
searchable={false} | |||
simpleValue={false} | |||
tabSelectsValue={true} | |||
value="foo" | |||
valueComponent={[Function]} | |||
valueKey="value" | |||
/> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
disabled={false} | |||
onClick={[Function]} | |||
> | |||
apply | |||
</button> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`bulk applies template to selected results 3`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="permission_templates.bulk_apply_permission_template" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
permission_templates.bulk_apply_permission_template | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="alert alert-info" | |||
> | |||
permission_templates.bulk_apply_permission_template.apply_to_selected.2 | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
template | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<Select | |||
addLabelText="Add \\"{label}\\"?" | |||
arrowRenderer={[Function]} | |||
autosize={true} | |||
backspaceRemoves={true} | |||
backspaceToRemoveMessage="Press backspace to remove {label}" | |||
clearAllText="Clear all" | |||
clearRenderer={[Function]} | |||
clearValueText="Clear value" | |||
clearable={false} | |||
deleteRemoves={true} | |||
delimiter="," | |||
disabled={true} | |||
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} | |||
noResultsText="No results found" | |||
onBlurResetsInput={true} | |||
onChange={[Function]} | |||
onCloseResetsInput={true} | |||
optionComponent={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "Foo", | |||
"value": "foo", | |||
}, | |||
Object { | |||
"label": "Bar", | |||
"value": "bar", | |||
}, | |||
] | |||
} | |||
pageSize={5} | |||
placeholder="Select..." | |||
required={false} | |||
scrollMenuIntoView={true} | |||
searchable={false} | |||
simpleValue={false} | |||
tabSelectsValue={true} | |||
value="foo" | |||
valueComponent={[Function]} | |||
valueKey="value" | |||
/> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<button | |||
disabled={true} | |||
onClick={[Function]} | |||
> | |||
apply | |||
</button> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`bulk applies template to selected results 4`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="permission_templates.bulk_apply_permission_template" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
permission_templates.bulk_apply_permission_template | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="alert alert-success" | |||
> | |||
projects_role.apply_template.success | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
close | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; |
@@ -0,0 +1,308 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`changes visibility 1`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="modal form" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
organization.change_visibility_form.header | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<p> | |||
<a | |||
className="link-base-color link-no-underline" | |||
data-visibility="public" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<i | |||
className="icon-radio spacer-right is-checked" | |||
/> | |||
visibility.public | |||
</a> | |||
</p> | |||
<p | |||
className="text-muted spacer-top" | |||
style={ | |||
Object { | |||
"paddingLeft": 22, | |||
} | |||
} | |||
> | |||
visibility.public.description.short | |||
</p> | |||
</div> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<p> | |||
<a | |||
className="link-base-color link-no-underline" | |||
data-visibility="private" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<i | |||
className="icon-radio spacer-right" | |||
/> | |||
visibility.private | |||
</a> | |||
</p> | |||
<p | |||
className="text-muted spacer-top" | |||
style={ | |||
Object { | |||
"paddingLeft": 22, | |||
} | |||
} | |||
> | |||
visibility.private.description.short | |||
</p> | |||
</div> | |||
<div | |||
className="alert alert-warning" | |||
> | |||
organization.change_visibility_form.warning | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
className="js-confirm" | |||
onClick={[Function]} | |||
> | |||
organization.change_visibility_form.submit | |||
</button> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`changes visibility 2`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="modal form" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
organization.change_visibility_form.header | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<p> | |||
<a | |||
className="link-base-color link-no-underline" | |||
data-visibility="public" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<i | |||
className="icon-radio spacer-right" | |||
/> | |||
visibility.public | |||
</a> | |||
</p> | |||
<p | |||
className="text-muted spacer-top" | |||
style={ | |||
Object { | |||
"paddingLeft": 22, | |||
} | |||
} | |||
> | |||
visibility.public.description.short | |||
</p> | |||
</div> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<p> | |||
<a | |||
className="link-base-color link-no-underline" | |||
data-visibility="private" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<i | |||
className="icon-radio spacer-right is-checked" | |||
/> | |||
visibility.private | |||
</a> | |||
</p> | |||
<p | |||
className="text-muted spacer-top" | |||
style={ | |||
Object { | |||
"paddingLeft": 22, | |||
} | |||
} | |||
> | |||
visibility.private.description.short | |||
</p> | |||
</div> | |||
<div | |||
className="alert alert-warning" | |||
> | |||
organization.change_visibility_form.warning | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
className="js-confirm" | |||
onClick={[Function]} | |||
> | |||
organization.change_visibility_form.submit | |||
</button> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`renders disabled 1`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="modal form" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
organization.change_visibility_form.header | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<p> | |||
<a | |||
className="link-base-color link-no-underline" | |||
data-visibility="public" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
<i | |||
className="icon-radio spacer-right is-checked" | |||
/> | |||
visibility.public | |||
</a> | |||
</p> | |||
<p | |||
className="text-muted spacer-top" | |||
style={ | |||
Object { | |||
"paddingLeft": 22, | |||
} | |||
} | |||
> | |||
visibility.public.description.short | |||
</p> | |||
</div> | |||
<div | |||
className="big-spacer-bottom" | |||
> | |||
<p> | |||
<span | |||
className="text-muted cursor-not-allowed" | |||
> | |||
<i | |||
className="icon-radio spacer-right" | |||
/> | |||
visibility.private | |||
</span> | |||
</p> | |||
<p | |||
className="text-muted spacer-top" | |||
style={ | |||
Object { | |||
"paddingLeft": 22, | |||
} | |||
} | |||
> | |||
visibility.private.description.short | |||
</p> | |||
</div> | |||
<UpgradeOrganizationBox | |||
organization="org" | |||
/> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
className="js-confirm" | |||
onClick={[Function]} | |||
> | |||
organization.change_visibility_form.submit | |||
</button> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; |
@@ -0,0 +1,468 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`creates project 1`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="modal form" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<form | |||
id="create-project-form" | |||
onSubmit={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
qualifiers.create.TRK | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-name" | |||
> | |||
name | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="create-project-name" | |||
maxLength={2000} | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-branch" | |||
> | |||
branch | |||
</label> | |||
<input | |||
id="create-project-branch" | |||
maxLength={200} | |||
name="branch" | |||
onChange={[Function]} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-key" | |||
> | |||
key | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
id="create-project-key" | |||
maxLength={400} | |||
name="key" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
visibility | |||
</label> | |||
<VisibilitySelector | |||
className="little-spacer-top" | |||
onChange={[Function]} | |||
visibility="public" | |||
/> | |||
<div | |||
className="spacer-top" | |||
> | |||
<UpgradeOrganizationBox | |||
organization="org" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
disabled={false} | |||
id="create-project-submit" | |||
type="submit" | |||
> | |||
create | |||
</button> | |||
<a | |||
href="#" | |||
id="create-project-cancel" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</form> | |||
</Modal> | |||
`; | |||
exports[`creates project 2`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="modal form" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<form | |||
id="create-project-form" | |||
onSubmit={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
qualifiers.create.TRK | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-name" | |||
> | |||
name | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="create-project-name" | |||
maxLength={2000} | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="name" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-branch" | |||
> | |||
branch | |||
</label> | |||
<input | |||
id="create-project-branch" | |||
maxLength={200} | |||
name="branch" | |||
onChange={[Function]} | |||
type="text" | |||
value="branch" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-key" | |||
> | |||
key | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
id="create-project-key" | |||
maxLength={400} | |||
name="key" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="key" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
visibility | |||
</label> | |||
<VisibilitySelector | |||
className="little-spacer-top" | |||
onChange={[Function]} | |||
visibility="private" | |||
/> | |||
<div | |||
className="spacer-top" | |||
> | |||
<UpgradeOrganizationBox | |||
organization="org" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
disabled={false} | |||
id="create-project-submit" | |||
type="submit" | |||
> | |||
create | |||
</button> | |||
<a | |||
href="#" | |||
id="create-project-cancel" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</form> | |||
</Modal> | |||
`; | |||
exports[`creates project 3`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="modal form" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<form | |||
id="create-project-form" | |||
onSubmit={[Function]} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
qualifiers.create.TRK | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-name" | |||
> | |||
name | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
autoFocus={true} | |||
id="create-project-name" | |||
maxLength={2000} | |||
name="name" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="name" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-branch" | |||
> | |||
branch | |||
</label> | |||
<input | |||
id="create-project-branch" | |||
maxLength={200} | |||
name="branch" | |||
onChange={[Function]} | |||
type="text" | |||
value="branch" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label | |||
htmlFor="create-project-key" | |||
> | |||
key | |||
<em | |||
className="mandatory" | |||
> | |||
* | |||
</em> | |||
</label> | |||
<input | |||
id="create-project-key" | |||
maxLength={400} | |||
name="key" | |||
onChange={[Function]} | |||
required={true} | |||
type="text" | |||
value="key" | |||
/> | |||
</div> | |||
<div | |||
className="modal-field" | |||
> | |||
<label> | |||
visibility | |||
</label> | |||
<VisibilitySelector | |||
className="little-spacer-top" | |||
onChange={[Function]} | |||
visibility="private" | |||
/> | |||
<div | |||
className="spacer-top" | |||
> | |||
<UpgradeOrganizationBox | |||
organization="org" | |||
/> | |||
</div> | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<button | |||
disabled={true} | |||
id="create-project-submit" | |||
type="submit" | |||
> | |||
create | |||
</button> | |||
<a | |||
href="#" | |||
id="create-project-cancel" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</form> | |||
</Modal> | |||
`; | |||
exports[`creates project 4`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="modal form" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<div> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
qualifiers.create.TRK | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
<div | |||
className="alert alert-success" | |||
> | |||
Project | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"id": "name", | |||
}, | |||
} | |||
} | |||
> | |||
name | |||
</Link> | |||
has been successfully created. | |||
</div> | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<a | |||
href="#" | |||
id="create-project-close" | |||
onClick={[Function]} | |||
> | |||
close | |||
</a> | |||
</footer> | |||
</div> | |||
</Modal> | |||
`; |
@@ -0,0 +1,98 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`deletes projects 1`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="qualifiers.delete.TRK" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
qualifiers.delete.TRK | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
qualifiers.delete_confirm.TRK | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<button | |||
className="button-red" | |||
disabled={false} | |||
onClick={[Function]} | |||
> | |||
delete | |||
</button> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; | |||
exports[`deletes projects 2`] = ` | |||
<Modal | |||
ariaHideApp={true} | |||
bodyOpenClassName="ReactModal__Body--open" | |||
className="modal" | |||
closeTimeoutMS={0} | |||
contentLabel="qualifiers.delete.TRK" | |||
isOpen={true} | |||
onRequestClose={[Function]} | |||
overlayClassName="modal-overlay" | |||
parentSelector={[Function]} | |||
portalClassName="ReactModalPortal" | |||
shouldCloseOnOverlayClick={true} | |||
> | |||
<header | |||
className="modal-head" | |||
> | |||
<h2> | |||
qualifiers.delete.TRK | |||
</h2> | |||
</header> | |||
<div | |||
className="modal-body" | |||
> | |||
qualifiers.delete_confirm.TRK | |||
</div> | |||
<footer | |||
className="modal-foot" | |||
> | |||
<i | |||
className="spinner spacer-right" | |||
/> | |||
<button | |||
className="button-red" | |||
disabled={true} | |||
onClick={[Function]} | |||
> | |||
delete | |||
</button> | |||
<a | |||
className="js-modal-close" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
cancel | |||
</a> | |||
</footer> | |||
</Modal> | |||
`; |
@@ -0,0 +1,56 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`changes default visibility 1`] = ` | |||
<ChangeVisibilityForm | |||
onClose={[Function]} | |||
onConfirm={[Function]} | |||
organization={ | |||
Object { | |||
"key": "org", | |||
"name": "org", | |||
"projectVisibility": "public", | |||
} | |||
} | |||
/> | |||
`; | |||
exports[`renders 1`] = ` | |||
<header | |||
className="page-header" | |||
> | |||
<h1 | |||
className="page-title" | |||
> | |||
projects_management | |||
</h1> | |||
<div | |||
className="page-actions" | |||
> | |||
<span | |||
className="big-spacer-right" | |||
> | |||
organization.default_visibility_of_new_projects | |||
<strong> | |||
visibility.public | |||
</strong> | |||
<a | |||
className="js-change-visibility spacer-left icon-edit" | |||
href="#" | |||
onClick={[Function]} | |||
/> | |||
</span> | |||
<button | |||
id="create-project" | |||
onClick={[Function]} | |||
> | |||
qualifiers.create.TRK | |||
</button> | |||
</div> | |||
<p | |||
className="page-description" | |||
> | |||
projects_management.page.description | |||
</p> | |||
</header> | |||
`; |
@@ -0,0 +1,101 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<tr> | |||
<td | |||
className="thin" | |||
> | |||
<Checkbox | |||
checked={true} | |||
onCheck={[Function]} | |||
thirdState={false} | |||
/> | |||
</td> | |||
<td | |||
className="nowrap" | |||
> | |||
<Link | |||
className="link-with-icon" | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/dashboard", | |||
"query": Object { | |||
"id": "project", | |||
}, | |||
} | |||
} | |||
> | |||
<QualifierIcon | |||
qualifier="TRK" | |||
/> | |||
<span> | |||
Project | |||
</span> | |||
</Link> | |||
</td> | |||
<td | |||
className="nowrap" | |||
> | |||
<span | |||
className="note" | |||
> | |||
project | |||
</span> | |||
</td> | |||
<td | |||
className="width-20" | |||
> | |||
<PrivateBadge /> | |||
</td> | |||
<td | |||
className="thin nowrap" | |||
> | |||
<div | |||
className="dropdown" | |||
> | |||
<button | |||
className="dropdown-toggle" | |||
data-toggle="dropdown" | |||
> | |||
actions | |||
<i | |||
className="icon-dropdown" | |||
/> | |||
</button> | |||
<ul | |||
className="dropdown-menu dropdown-menu-right" | |||
> | |||
<li> | |||
<Link | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to={ | |||
Object { | |||
"pathname": "/project_roles", | |||
"query": Object { | |||
"id": "project", | |||
}, | |||
} | |||
} | |||
> | |||
edit_permissions | |||
</Link> | |||
</li> | |||
<li> | |||
<a | |||
className="js-apply-template" | |||
href="#" | |||
onClick={[Function]} | |||
> | |||
projects_role.apply_template | |||
</a> | |||
</li> | |||
</ul> | |||
</div> | |||
</td> | |||
</tr> | |||
`; |
@@ -0,0 +1,37 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders list of projects 1`] = ` | |||
<table | |||
className="data zebra new-loading" | |||
id="projects-management-page-projects" | |||
> | |||
<tbody> | |||
<ProjectRow | |||
onApplyTemplateClick={[Function]} | |||
onProjectCheck={[Function]} | |||
project={ | |||
Object { | |||
"key": "a", | |||
"name": "A", | |||
"qualifier": "TRK", | |||
"visibility": "public", | |||
} | |||
} | |||
selected={true} | |||
/> | |||
<ProjectRow | |||
onApplyTemplateClick={[Function]} | |||
onProjectCheck={[Function]} | |||
project={ | |||
Object { | |||
"key": "b", | |||
"name": "B", | |||
"qualifier": "TRK", | |||
"visibility": "public", | |||
} | |||
} | |||
selected={false} | |||
/> | |||
</tbody> | |||
</table> | |||
`; |
@@ -0,0 +1,234 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`bulk applies permission template 1`] = ` | |||
<BulkApplyTemplateModal | |||
onClose={[Function]} | |||
organization="org" | |||
qualifier="TRK" | |||
query="" | |||
selection={Array []} | |||
total={0} | |||
type="ALL" | |||
/> | |||
`; | |||
exports[`deletes projects 1`] = ` | |||
<DeleteModal | |||
onClose={[Function]} | |||
onConfirm={[Function]} | |||
organization="org" | |||
qualifier="TRK" | |||
selection={ | |||
Array [ | |||
"foo", | |||
"bar", | |||
] | |||
} | |||
/> | |||
`; | |||
exports[`render qualifiers filter 1`] = ` | |||
<div | |||
className="panel panel-vertical bordered-bottom spacer-bottom" | |||
> | |||
<table | |||
className="data" | |||
> | |||
<tbody> | |||
<tr> | |||
<td | |||
className="thin text-middle" | |||
> | |||
<Checkbox | |||
checked={false} | |||
onCheck={[Function]} | |||
thirdState={false} | |||
/> | |||
</td> | |||
<td | |||
className="thin nowrap text-middle" | |||
> | |||
<RadioToggle | |||
disabled={false} | |||
name="projects-qualifier" | |||
onCheck={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "qualifiers.TRK", | |||
"value": "TRK", | |||
}, | |||
Object { | |||
"label": "qualifiers.VW", | |||
"value": "VW", | |||
}, | |||
Object { | |||
"label": "qualifiers.APP", | |||
"value": "APP", | |||
}, | |||
] | |||
} | |||
value="TRK" | |||
/> | |||
</td> | |||
<td | |||
className="thin nowrap text-middle" | |||
> | |||
<RadioToggle | |||
disabled={false} | |||
name="projects-type" | |||
onCheck={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "All", | |||
"value": "ALL", | |||
}, | |||
Object { | |||
"label": "Provisioned", | |||
"value": "PROVISIONED", | |||
}, | |||
Object { | |||
"label": "Ghosts", | |||
"value": "GHOSTS", | |||
}, | |||
] | |||
} | |||
value="ALL" | |||
/> | |||
</td> | |||
<td | |||
className="text-middle" | |||
> | |||
<form | |||
className="search-box" | |||
onSubmit={[Function]} | |||
> | |||
<button | |||
className="search-box-submit button-clean" | |||
> | |||
<i | |||
className="icon-search" | |||
/> | |||
</button> | |||
<input | |||
className="search-box-input input-medium" | |||
onChange={[Function]} | |||
placeholder="Search" | |||
type="search" | |||
value="" | |||
/> | |||
</form> | |||
</td> | |||
<td | |||
className="thin nowrap text-middle" | |||
> | |||
<button | |||
className="spacer-right js-bulk-apply-permission-template" | |||
onClick={[Function]} | |||
> | |||
permission_templates.bulk_apply_permission_template | |||
</button> | |||
<button | |||
className="js-delete button-red" | |||
disabled={true} | |||
onClick={[Function]} | |||
> | |||
delete | |||
</button> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</div> | |||
`; | |||
exports[`renders 1`] = ` | |||
<div | |||
className="panel panel-vertical bordered-bottom spacer-bottom" | |||
> | |||
<table | |||
className="data" | |||
> | |||
<tbody> | |||
<tr> | |||
<td | |||
className="thin text-middle" | |||
> | |||
<Checkbox | |||
checked={false} | |||
onCheck={[Function]} | |||
thirdState={false} | |||
/> | |||
</td> | |||
<td | |||
className="thin nowrap text-middle" | |||
> | |||
<RadioToggle | |||
disabled={false} | |||
name="projects-type" | |||
onCheck={[Function]} | |||
options={ | |||
Array [ | |||
Object { | |||
"label": "All", | |||
"value": "ALL", | |||
}, | |||
Object { | |||
"label": "Provisioned", | |||
"value": "PROVISIONED", | |||
}, | |||
Object { | |||
"label": "Ghosts", | |||
"value": "GHOSTS", | |||
}, | |||
] | |||
} | |||
value="ALL" | |||
/> | |||
</td> | |||
<td | |||
className="text-middle" | |||
> | |||
<form | |||
className="search-box" | |||
onSubmit={[Function]} | |||
> | |||
<button | |||
className="search-box-submit button-clean" | |||
> | |||
<i | |||
className="icon-search" | |||
/> | |||
</button> | |||
<input | |||
className="search-box-input input-medium" | |||
onChange={[Function]} | |||
placeholder="Search" | |||
type="search" | |||
value="" | |||
/> | |||
</form> | |||
</td> | |||
<td | |||
className="thin nowrap text-middle" | |||
> | |||
<button | |||
className="spacer-right js-bulk-apply-permission-template" | |||
onClick={[Function]} | |||
> | |||
permission_templates.bulk_apply_permission_template | |||
</button> | |||
<button | |||
className="js-delete button-red" | |||
disabled={true} | |||
onClick={[Function]} | |||
> | |||
delete | |||
</button> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</div> | |||
`; |
@@ -21,8 +21,20 @@ export const PAGE_SIZE = 50; | |||
export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP', 'DEV']; | |||
export const TYPE = { | |||
ALL: 'ALL', | |||
PROVISIONED: 'PROVISIONED', | |||
GHOSTS: 'GHOSTS' | |||
}; | |||
export enum Type { | |||
All = 'ALL', | |||
Provisioned = 'PROVISIONED', | |||
Ghosts = 'GHOSTS' | |||
} | |||
export interface Project { | |||
key: string; | |||
name: string; | |||
qualifier: string; | |||
visibility: Visibility; | |||
} | |||
export enum Visibility { | |||
Public = 'public', | |||
Private = 'private' | |||
} |
@@ -17,20 +17,17 @@ | |||
* 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 React from 'react'; | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import Tooltip from '../controls/Tooltip'; | |||
import { translate } from '../../helpers/l10n'; | |||
/*:: | |||
type Props = { | |||
className?: string, | |||
tooltipPlacement?: string | |||
}; | |||
*/ | |||
interface Props { | |||
className?: string; | |||
tooltipPlacement?: string; | |||
} | |||
export default function PrivateBadge({ className, tooltipPlacement = 'bottom' } /*: Props */) { | |||
export default function PrivateBadge({ className, tooltipPlacement = 'bottom' }: Props) { | |||
return ( | |||
<Tooltip overlay={translate('visibility.private.description')} placement={tooltipPlacement}> | |||
<div className={classNames('outline-badge', className)}> |
@@ -17,30 +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. | |||
*/ | |||
// @flow | |||
import React from 'react'; | |||
import classNames from 'classnames'; | |||
import * as React from 'react'; | |||
import * as classNames from 'classnames'; | |||
import { translate } from '../../helpers/l10n'; | |||
/*:: | |||
type Props = {| | |||
canTurnToPrivate: boolean, | |||
className?: string, | |||
onChange: string => void, | |||
visibility: string | |||
|}; | |||
*/ | |||
export default class VisibilitySelector extends React.PureComponent { | |||
/*:: props: Props; */ | |||
interface Props { | |||
canTurnToPrivate?: boolean; | |||
className?: string; | |||
onChange: (x: string) => void; | |||
visibility: string; | |||
} | |||
handlePublicClick = (event /*: Event & { currentTarget: HTMLElement } */) => { | |||
export default class VisibilitySelector extends React.PureComponent<Props> { | |||
handlePublicClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.props.onChange('public'); | |||
}; | |||
handlePrivateClick = (event /*: Event & { currentTarget: HTMLElement } */) => { | |||
handlePrivateClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { | |||
event.preventDefault(); | |||
event.currentTarget.blur(); | |||
this.props.onChange('private'); |
@@ -1,7 +1,7 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2017 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 | |||
@@ -17,15 +17,10 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import ModalForm from '../../components/common/modal-form'; | |||
import Template from './templates/projects-delete.hbs'; | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import PrivateBadge from '../PrivateBadge'; | |||
export default ModalForm.extend({ | |||
template: Template, | |||
onFormSubmit() { | |||
ModalForm.prototype.onFormSubmit.apply(this, arguments); | |||
this.options.deleteProjects(); | |||
this.destroy(); | |||
} | |||
it('renders', () => { | |||
expect(shallow(<PrivateBadge />)).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,48 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2016 SonarSource SA | |||
* mailto:contact 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 VisibilitySelector from '../VisibilitySelector'; | |||
import { click } from '../../../helpers/testUtils'; | |||
it('changes visibility', () => { | |||
const onChange = jest.fn(); | |||
const wrapper = shallow( | |||
<VisibilitySelector canTurnToPrivate={true} onChange={onChange} visibility="public" /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('#visibility-private')); | |||
expect(onChange).toBeCalledWith('private'); | |||
wrapper.setProps({ visibility: 'private' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('#visibility-public')); | |||
expect(onChange).toBeCalledWith('public'); | |||
}); | |||
it('renders disabled', () => { | |||
expect( | |||
shallow( | |||
<VisibilitySelector canTurnToPrivate={false} onChange={jest.fn()} visibility="public" /> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -0,0 +1,14 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<Tooltip | |||
overlay="visibility.private.description" | |||
placement="bottom" | |||
> | |||
<div | |||
className="outline-badge" | |||
> | |||
visibility.private | |||
</div> | |||
</Tooltip> | |||
`; |
@@ -0,0 +1,104 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`changes visibility 1`] = ` | |||
<div> | |||
<a | |||
className="link-base-color link-no-underline" | |||
href="#" | |||
id="visibility-public" | |||
onClick={[Function]} | |||
> | |||
<i | |||
className="icon-radio is-checked" | |||
/> | |||
<span | |||
className="spacer-left" | |||
> | |||
visibility.public | |||
</span> | |||
</a> | |||
<a | |||
className="link-base-color link-no-underline huge-spacer-left" | |||
href="#" | |||
id="visibility-private" | |||
onClick={[Function]} | |||
> | |||
<i | |||
className="icon-radio" | |||
/> | |||
<span | |||
className="spacer-left" | |||
> | |||
visibility.private | |||
</span> | |||
</a> | |||
</div> | |||
`; | |||
exports[`changes visibility 2`] = ` | |||
<div> | |||
<a | |||
className="link-base-color link-no-underline" | |||
href="#" | |||
id="visibility-public" | |||
onClick={[Function]} | |||
> | |||
<i | |||
className="icon-radio" | |||
/> | |||
<span | |||
className="spacer-left" | |||
> | |||
visibility.public | |||
</span> | |||
</a> | |||
<a | |||
className="link-base-color link-no-underline huge-spacer-left" | |||
href="#" | |||
id="visibility-private" | |||
onClick={[Function]} | |||
> | |||
<i | |||
className="icon-radio is-checked" | |||
/> | |||
<span | |||
className="spacer-left" | |||
> | |||
visibility.private | |||
</span> | |||
</a> | |||
</div> | |||
`; | |||
exports[`renders disabled 1`] = ` | |||
<div> | |||
<a | |||
className="link-base-color link-no-underline" | |||
href="#" | |||
id="visibility-public" | |||
onClick={[Function]} | |||
> | |||
<i | |||
className="icon-radio is-checked" | |||
/> | |||
<span | |||
className="spacer-left" | |||
> | |||
visibility.public | |||
</span> | |||
</a> | |||
<span | |||
className="huge-spacer-left text-muted cursor-not-allowed" | |||
id="visibility-private" | |||
> | |||
<i | |||
className="icon-radio" | |||
/> | |||
<span | |||
className="spacer-left" | |||
> | |||
visibility.private | |||
</span> | |||
</span> | |||
</div> | |||
`; |
@@ -17,38 +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 React from 'react'; | |||
import PropTypes from 'prop-types'; | |||
import * as React from 'react'; | |||
export default class RadioToggle extends React.PureComponent { | |||
static propTypes = { | |||
value: PropTypes.string, | |||
options: PropTypes.arrayOf( | |||
PropTypes.shape({ | |||
value: PropTypes.string.isRequired, | |||
label: PropTypes.string.isRequired | |||
}) | |||
).isRequired, | |||
name: PropTypes.string.isRequired, | |||
onCheck: PropTypes.func.isRequired | |||
}; | |||
interface Props { | |||
name: string; | |||
onCheck: (value: string) => void; | |||
options: Array<{ label: string; value: string }>; | |||
value?: string; | |||
} | |||
export default class RadioToggle extends React.PureComponent<Props> { | |||
static defaultProps = { | |||
disabled: false, | |||
value: null | |||
}; | |||
componentWillMount() { | |||
this.renderOption = this.renderOption.bind(this); | |||
this.handleChange = this.handleChange.bind(this); | |||
} | |||
handleChange(e) { | |||
handleChange = (e: React.SyntheticEvent<HTMLInputElement>) => { | |||
const newValue = e.currentTarget.value; | |||
this.props.onCheck(newValue); | |||
} | |||
}; | |||
renderOption(option) { | |||
renderOption = (option: { label: string; value: string }) => { | |||
const checked = option.value === this.props.value; | |||
const htmlId = this.props.name + '__' + option.value; | |||
return ( | |||
@@ -67,7 +56,7 @@ export default class RadioToggle extends React.PureComponent { | |||
</label> | |||
</li> | |||
); | |||
} | |||
}; | |||
render() { | |||
return ( |
@@ -17,25 +17,23 @@ | |||
* 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 React from 'react'; | |||
import RadioToggle from '../RadioToggle'; | |||
import { change } from '../../../helpers/testUtils'; | |||
function getSample(props) { | |||
const options = [{ value: 'one', label: 'first' }, { value: 'two', label: 'second' }]; | |||
return <RadioToggle options={options} name="sample" onCheck={() => true} {...props} />; | |||
} | |||
it('should render', () => { | |||
const radioToggle = shallow(getSample()); | |||
expect(radioToggle.find('input[type="radio"]').length).toBe(2); | |||
expect(radioToggle.find('label').length).toBe(2); | |||
it('renders', () => { | |||
expect(shallow(getSample())).toMatchSnapshot(); | |||
}); | |||
it('should call onCheck', () => { | |||
it('calls onCheck', () => { | |||
const onCheck = jest.fn(); | |||
const radioToggle = shallow(getSample({ onCheck })); | |||
change(radioToggle.find('input[value="two"]'), 'two'); | |||
const wrapper = shallow(getSample({ onCheck })); | |||
change(wrapper.find('input[value="two"]'), 'two'); | |||
expect(onCheck).toBeCalledWith('two'); | |||
}); | |||
function getSample(props?: any) { | |||
const options = [{ value: 'one', label: 'first' }, { value: 'two', label: 'second' }]; | |||
return <RadioToggle options={options} name="sample" onCheck={() => true} {...props} />; | |||
} |
@@ -0,0 +1,38 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`renders 1`] = ` | |||
<ul | |||
className="radio-toggle" | |||
> | |||
<li> | |||
<input | |||
checked={false} | |||
id="sample__one" | |||
name="sample" | |||
onChange={[Function]} | |||
type="radio" | |||
value="one" | |||
/> | |||
<label | |||
htmlFor="sample__one" | |||
> | |||
first | |||
</label> | |||
</li> | |||
<li> | |||
<input | |||
checked={false} | |||
id="sample__two" | |||
name="sample" | |||
onChange={[Function]} | |||
type="radio" | |||
value="two" | |||
/> | |||
<label | |||
htmlFor="sample__two" | |||
> | |||
second | |||
</label> | |||
</li> | |||
</ul> | |||
`; |
@@ -42,10 +42,11 @@ export function submit(element: ShallowWrapper): void { | |||
}); | |||
} | |||
export function change(element: ShallowWrapper, value: string): void { | |||
export function change(element: ShallowWrapper, value: string, event = {}): void { | |||
element.simulate('change', { | |||
target: { value }, | |||
currentTarget: { value } | |||
currentTarget: { value }, | |||
...event | |||
}); | |||
} | |||
@@ -422,15 +422,21 @@ qualifiers.new.VW=New Portfolio | |||
qualifiers.new.DEV=New Developer | |||
qualifiers.new.APP=New Application | |||
qualifiers.delete.TRK=Delete Project | |||
qualifiers.delete.VW=Delete Portfolio | |||
qualifiers.delete.DEV=Delete Developer | |||
qualifiers.delete.APP=Delete Application | |||
qualifier.delete.TRK=Delete Project | |||
qualifier.delete.VW=Delete Portfolio | |||
qualifier.delete.APP=Delete Application | |||
qualifiers.delete_confirm.TRK=Do you want to delete this project? | |||
qualifiers.delete_confirm.VW=Do you want to delete this portfolio? | |||
qualifiers.delete_confirm.DEV=Do you want to delete this developer? | |||
qualifiers.delete_confirm.APP=Do you want to delete this application? | |||
qualifiers.delete.TRK=Delete Projects | |||
qualifiers.delete.VW=Delete Portfolios | |||
qualifiers.delete.APP=Delete Applications | |||
qualifier.delete_confirm.TRK=Do you want to delete this project? | |||
qualifier.delete_confirm.VW=Do you want to delete this portfolio? | |||
qualifier.delete_confirm.APP=Do you want to delete this application? | |||
qualifiers.delete_confirm.TRK=Do you want to delete these projects? | |||
qualifiers.delete_confirm.VW=Do you want to delete these portfolios? | |||
qualifiers.delete_confirm.APP=Do you want to delete these applications? | |||
qualifiers.create.TRK=Create Project | |||
qualifiers.create.VW=Create Portfolio | |||
@@ -1398,6 +1404,7 @@ project_quality_gate.successfully_updated=Quality gate has been successfully upd | |||
#------------------------------------------------------------------------------ | |||
project_deletion.delete_resource_confirmation=Are you sure you want to delete "{0}"? | |||
projects_management.delete_resource_confirmation=Are you sure you want to delete "{0}"? | |||
#------------------------------------------------------------------------------ | |||
@@ -2596,6 +2603,8 @@ permission_templates.project_creators=Project Creators | |||
permission_templates.project_creators.explanation=When a new project is created, the user who creates the project will receive this permission on the project. | |||
permission_templates.grant_permission_to_project_creators=Grant the "{0}" permission to project creators | |||
permission_templates.bulk_apply_permission_template=Bulk Apply Permission Template | |||
permission_templates.bulk_apply_permission_template.apply_to_selected=You're about to apply the selected permission template to {0} selected item(s). | |||
permission_templates.bulk_apply_permission_template.apply_to_all=You're about to apply the selected permission template to {0} item(s). | |||
#------------------------------------------------------------------------------ |
@@ -19,10 +19,9 @@ | |||
*/ | |||
package org.sonarqube.pageobjects; | |||
import com.codeborne.selenide.CollectionCondition; | |||
import static com.codeborne.selenide.Condition.exist; | |||
import static com.codeborne.selenide.Condition.text; | |||
import static com.codeborne.selenide.Condition.visible; | |||
import static com.codeborne.selenide.Selenide.$; | |||
import static com.codeborne.selenide.Selenide.$$; | |||
@@ -52,13 +51,11 @@ public class ProjectsManagementPage { | |||
} | |||
public ProjectsManagementPage bulkApplyPermissionTemplate(String template) { | |||
$(".js-bulk-apply-permission-template").should(exist).click(); | |||
$(".modal .select2-choice").should(exist).click(); | |||
$$(".select2-results li") | |||
.shouldHave(CollectionCondition.sizeGreaterThan(0)) | |||
.findBy(text("foo-template")).should(exist).click(); | |||
$(".modal .js-apply").should(exist).click(); | |||
$(".modal-body .alert-success").should(exist); | |||
$(".js-bulk-apply-permission-template").click(); | |||
$(".modal .Select-value").click(); | |||
$$(".modal .Select-option").findBy(text(template)).click(); | |||
$(".modal-foot button").click(); | |||
$(".modal-body .alert-success").shouldBe(visible); | |||
return this; | |||
} | |||
} |
@@ -95,7 +95,7 @@ public class ProvisioningPermissionTest { | |||
* SONAR-4709 | |||
*/ | |||
@Test | |||
public void organization_administrator_cannot_provision_project_if_he_doesnt_have_provisioning_permission() { | |||
public void organization_administrator_cannot_provision_project_if_he_does_not_have_provisioning_permission() { | |||
runSelenese(orchestrator, "/authorisation/ProvisioningPermissionTest/should-not-be-able-to-provision-project.html"); | |||
} | |||
@@ -131,7 +131,7 @@ public class ProvisioningPermissionTest { | |||
* SONAR-4709 | |||
*/ | |||
@Test | |||
public void user_cannot_provision_project_through_ws_if_he_doesnt_have_provisioning_permission() { | |||
public void user_cannot_provision_project_through_ws_if_he_does_not_have_provisioning_permission() { | |||
thrown.expect(HttpException.class); | |||
thrown.expectMessage("403"); | |||
@@ -36,19 +36,14 @@ | |||
</tr> | |||
<tr> | |||
<td>open</td> | |||
<td>/projects_admin</td> | |||
<td>/admin/projects_management</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>css=#projects-type__ALL</td> | |||
<td>css=#create-project</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>assertText</td> | |||
<td>css=.page-actions button</td> | |||
<td>*Create Project*</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</body> |
@@ -35,18 +35,18 @@ | |||
</tr> | |||
<tr> | |||
<td>open</td> | |||
<td>/projects_admin</td> | |||
<td>/admin/projects_management</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>waitForElementPresent</td> | |||
<td>css=#projects-type__ALL</td> | |||
<td>css=#projects-management-page</td> | |||
<td></td> | |||
</tr> | |||
<tr> | |||
<td>assertNotText</td> | |||
<td>css=.page-actions button</td> | |||
<td>*Create Project*</td> | |||
<td>assertElementNotPresent</td> | |||
<td>css=#create-project</td> | |||
<td></td> | |||
</tr> | |||
</tbody> | |||
</table> |