diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-11-29 14:27:13 +0100 |
---|---|---|
committer | Julien Lancelot <julien.lancelot@sonarsource.com> | 2017-12-14 17:03:35 +0100 |
commit | ab2a5de55004f8ac50a99fae9840da26ce125088 (patch) | |
tree | 2c028a982112d1a5e854ca38b676e27a419c663c | |
parent | fa6663db4630550b3c27c71bb62fea4c4b9c262c (diff) | |
download | sonarqube-ab2a5de55004f8ac50a99fae9840da26ce125088.tar.gz sonarqube-ab2a5de55004f8ac50a99fae9840da26ce125088.zip |
Rewrite part of the permissions app to Typescript
20 files changed, 405 insertions, 272 deletions
diff --git a/server/sonar-web/src/main/js/api/permissions.ts b/server/sonar-web/src/main/js/api/permissions.ts index 8d2cf3e5596..39d77276f02 100644 --- a/server/sonar-web/src/main/js/api/permissions.ts +++ b/server/sonar-web/src/main/js/api/permissions.ts @@ -178,12 +178,20 @@ export function removeProjectCreatorFromTemplate( return post('/api/permissions/remove_project_creator_from_template', { templateId, permission }); } +export interface PermissionUser { + login: string; + name: string; + email?: string; + permissions: string[]; + avatar?: string; +} + export function getPermissionsUsersForComponent( projectKey: string, query?: string, permission?: string, organization?: string -): Promise<any> { +): Promise<PermissionUser[]> { const data: RequestData = { projectKey, ps: PAGE_SIZE }; if (query) { data.q = query; @@ -197,12 +205,19 @@ export function getPermissionsUsersForComponent( return getJSON('/api/permissions/users', data).then(r => r.users); } +export interface PermissionGroup { + id: string; + name: string; + description?: string; + permissions: string[]; +} + export function getPermissionsGroupsForComponent( projectKey: string, query: string = '', permission?: string, organization?: string -): Promise<any> { +): Promise<PermissionGroup[]> { const data: RequestData = { projectKey, ps: PAGE_SIZE }; if (query) { data.q = query; @@ -220,7 +235,7 @@ export function getGlobalPermissionsUsers( query?: string, permission?: string, organization?: string -): Promise<any> { +): Promise<PermissionUser[]> { const data: RequestData = { ps: PAGE_SIZE }; if (query) { data.q = query; @@ -238,7 +253,7 @@ export function getGlobalPermissionsGroups( query?: string, permission?: string, organization?: string -): Promise<any> { +): Promise<PermissionGroup[]> { const data: RequestData = { ps: PAGE_SIZE }; if (query) { data.q = query; diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index f18764601e4..a180856c47a 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -108,7 +108,7 @@ export interface Metric { } export interface Organization { - adminPages?: Array<{ key: string; name: string }>; + adminPages?: { key: string; name: string }[]; avatar?: string; canAdmin?: boolean; canDelete?: boolean; @@ -118,8 +118,8 @@ export interface Organization { isDefault?: boolean; key: string; name: string; - pages?: Array<{ key: string; name: string }>; - projectVisibility: string; + pages?: { key: string; name: string }[]; + projectVisibility: Visibility; url?: string; } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPermissions.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPermissions.tsx index cab087b3e55..b2136ea2f35 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPermissions.js +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationPermissions.tsx @@ -17,24 +17,21 @@ * 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 { connect } from 'react-redux'; import GlobalPermissionsApp from '../../permissions/global/components/App'; import { getOrganizationByKey } from '../../../store/rootReducer'; -/*:: import type { Organization } from '../../../store/organizations/duck'; */ +import { Organization } from '../../../app/types'; -/*:: -type Props = { - organization: Organization -}; -*/ +interface Props { + organization: Organization; +} -function OrganizationPermissions(props /*: Props */) { - return <GlobalPermissionsApp organization={props.organization} />; +function OrganizationPermissions({ organization }: Props) { + return <GlobalPermissionsApp organization={organization} />; } -const mapStateToProps = (state, ownProps) => ({ +const mapStateToProps = (state: any, ownProps: any) => ({ organization: getOrganizationByKey(state, ownProps.params.organizationKey) }); diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx index 507d43bbfb0..4cffcc47d80 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.js +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigation.tsx @@ -17,17 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -// @flow -import React from 'react'; +import * as React from 'react'; +import * as classNames from 'classnames'; import { Link } from 'react-router'; -import classNames from 'classnames'; import * as theme from '../../../app/theme'; import { translate } from '../../../helpers/l10n'; import ContextNavBar from '../../../components/nav/ContextNavBar'; import NavBarTabs from '../../../components/nav/NavBarTabs'; import OrganizationAvatar from '../../../components/common/OrganizationAvatar'; import { getQualityGatesUrl } from '../../../helpers/urls'; -/*:: import type { Organization } from '../../../store/organizations/duck'; */ +import { Extension, Organization } from '../../../app/types'; const ADMIN_PATHS = [ 'edit', @@ -38,14 +37,13 @@ const ADMIN_PATHS = [ 'projects_management' ]; -export default class OrganizationNavigation extends React.PureComponent { - /*:: props: { - location: { pathname: string }, - organization: Organization - }; -*/ +interface Props { + location: { pathname: string }; + organization: Organization; +} - renderAdministration(adminActive /*: boolean */) { +export default class OrganizationNavigation extends React.PureComponent<Props> { + renderAdministration(adminActive: boolean) { const { organization } = this.props; return ( @@ -105,7 +103,7 @@ export default class OrganizationNavigation extends React.PureComponent { return extensions.map(this.renderExtension); } - renderExtension = (extension /*: { key: string, name: string } */) => { + renderExtension = (extension: Extension) => { const { organization } = this.props; const pathname = `/organizations/${organization.key}/extension/${extension.key}`; return ( @@ -117,7 +115,7 @@ export default class OrganizationNavigation extends React.PureComponent { ); }; - renderExtensions(moreActive /*: boolean */) { + renderExtensions(moreActive: boolean) { const extensions = this.props.organization.pages || []; if (extensions.length > 0) { return ( diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.js b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx index 3f337c032d5..e4884f83545 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.js +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/OrganizationNavigation-test.tsx @@ -17,46 +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. */ -import React from 'react'; +import * as React from 'react'; import { shallow } from 'enzyme'; import OrganizationNavigation from '../OrganizationNavigation'; +import { Visibility } from '../../../../app/types'; jest.mock('../../../issues/utils', () => ({ isMySet: () => false })); +const organization = { + key: 'foo', + name: 'Foo', + canAdmin: false, + canDelete: false, + projectVisibility: Visibility.Public +}; + it('regular user', () => { - const organization = { key: 'foo', name: 'Foo', canAdmin: false, canDelete: false }; - expect( - shallow( - <OrganizationNavigation - location={{ pathname: '/organizations/foo' }} - organization={organization} - /> - ) - ).toMatchSnapshot(); + expect(getWrapper()).toMatchSnapshot(); }); it('admin', () => { - const organization = { key: 'foo', name: 'Foo', canAdmin: true, canDelete: true }; expect( - shallow( - <OrganizationNavigation - location={{ pathname: '/organizations/foo' }} - organization={organization} - /> - ) + getWrapper({ organization: { ...organization, canAdmin: true, canDelete: true } }) ).toMatchSnapshot(); }); it('undeletable org', () => { - const organization = { key: 'foo', name: 'Foo', canAdmin: true, canDelete: false }; expect( - shallow( - <OrganizationNavigation - location={{ pathname: '/organizations/foo' }} - organization={organization} - /> - ) + getWrapper({ organization: { ...organization, canAdmin: true, canDelete: false } }) ).toMatchSnapshot(); }); + +function getWrapper(props = {}) { + return shallow( + <OrganizationNavigation + location={{ pathname: '/organizations/foo' }} + organization={organization} + {...props} + /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap index 66184bbc5d4..66184bbc5d4 100644 --- a/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.js.snap +++ b/server/sonar-web/src/main/js/apps/organizations/navigation/__tests__/__snapshots__/OrganizationNavigation-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx index f340d7e27c1..b9ee74940d0 100644 --- a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.js +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersList.tsx @@ -17,40 +17,39 @@ * 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 { connect } from 'react-redux'; +import * as React from 'react'; import SearchForm from '../../shared/components/SearchForm'; import HoldersList from '../../shared/components/HoldersList'; -import { - loadHolders, - grantToUser, - revokeFromUser, - grantToGroup, - revokeFromGroup, - updateFilter, - updateQuery, - selectPermission -} from '../store/actions'; import { translate } from '../../../../helpers/l10n'; -import { - getPermissionsAppUsers, - getPermissionsAppGroups, - getPermissionsAppQuery, - getPermissionsAppFilter, - getPermissionsAppSelectedPermission -} from '../../../../store/rootReducer'; +import { Organization } from '../../../../app/types'; +import { PermissionUser, PermissionGroup } from '../../../../api/permissions'; const PERMISSIONS_ORDER = ['admin', 'profileadmin', 'gateadmin', 'scan', 'provisioning']; - const PERMISSIONS_FOR_CUSTOM_ORG = ['admin', 'profileadmin', 'scan', 'provisioning']; -class AllHoldersList extends React.PureComponent { +interface Props { + filter: string; + grantPermissionToGroup: (groupName: string, permission: string) => void; + grantPermissionToUser: (login: string, permission: string) => void; + groups: PermissionGroup[]; + loadHolders: () => void; + onFilter: (filter: string) => void; + onSearch: (query: string) => void; + onSelectPermission: (permission: string) => void; + organization?: Organization; + query: string; + revokePermissionFromGroup: (groupName: string, permission: string) => void; + revokePermissionFromUser: (login: string, permission: string) => void; + selectedPermission?: string; + users: PermissionUser[]; +} + +export default class AllHoldersList extends React.PureComponent<Props> { componentDidMount() { this.props.loadHolders(); } - handleToggleUser(user, permission) { + handleToggleUser = (user: PermissionUser, permission: string) => { const hasPermission = user.permissions.includes(permission); if (hasPermission) { @@ -58,9 +57,9 @@ class AllHoldersList extends React.PureComponent { } else { this.props.grantPermissionToUser(user.login, permission); } - } + }; - handleToggleGroup(group, permission) { + handleToggleGroup = (group: PermissionGroup, permission: string) => { const hasPermission = group.permissions.includes(permission); if (hasPermission) { @@ -68,7 +67,7 @@ class AllHoldersList extends React.PureComponent { } else { this.props.grantPermissionToGroup(group.name, permission); } - } + }; render() { const order = @@ -91,8 +90,8 @@ class AllHoldersList extends React.PureComponent { users={this.props.users} groups={this.props.groups} onSelectPermission={this.props.onSelectPermission} - onToggleUser={this.handleToggleUser.bind(this)} - onToggleGroup={this.handleToggleGroup.bind(this)}> + onToggleUser={this.handleToggleUser} + onToggleGroup={this.handleToggleGroup}> <SearchForm query={this.props.query} filter={this.props.filter} @@ -103,37 +102,3 @@ class AllHoldersList extends React.PureComponent { ); } } - -const mapStateToProps = state => ({ - users: getPermissionsAppUsers(state), - groups: getPermissionsAppGroups(state), - query: getPermissionsAppQuery(state), - filter: getPermissionsAppFilter(state), - selectedPermission: getPermissionsAppSelectedPermission(state) -}); - -/*:: -type OwnProps = { - organization?: { key: string } -}; -*/ - -const mapDispatchToProps = (dispatch /*: Function */, ownProps /*: OwnProps */) => { - const organizationKey = ownProps.organization ? ownProps.organization.key : undefined; - return { - loadHolders: () => dispatch(loadHolders(organizationKey)), - onSearch: query => dispatch(updateQuery(query, organizationKey)), - onFilter: filter => dispatch(updateFilter(filter, organizationKey)), - onSelectPermission: permission => dispatch(selectPermission(permission, organizationKey)), - grantPermissionToUser: (login, permission) => - dispatch(grantToUser(login, permission, organizationKey)), - revokePermissionFromUser: (login, permission) => - dispatch(revokeFromUser(login, permission, organizationKey)), - grantPermissionToGroup: (groupName, permission) => - dispatch(grantToGroup(groupName, permission, organizationKey)), - revokePermissionFromGroup: (groupName, permission) => - dispatch(revokeFromGroup(groupName, permission, organizationKey)) - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(AllHoldersList); diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersListContainer.tsx b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersListContainer.tsx new file mode 100644 index 00000000000..bb0647549eb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/AllHoldersListContainer.tsx @@ -0,0 +1,95 @@ +/* + * 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 { connect } from 'react-redux'; +import { + loadHolders, + grantToUser, + revokeFromUser, + grantToGroup, + revokeFromGroup, + updateFilter, + updateQuery, + selectPermission +} from '../store/actions'; +import { + getPermissionsAppUsers, + getPermissionsAppGroups, + getPermissionsAppQuery, + getPermissionsAppFilter, + getPermissionsAppSelectedPermission +} from '../../../../store/rootReducer'; +import AllHoldersList from './AllHoldersList'; +import { Organization } from '../../../../app/types'; +import { PermissionUser, PermissionGroup } from '../../../../api/permissions'; + +interface OwnProps { + organization?: Organization; +} + +interface StateToProps { + filter: string; + groups: PermissionGroup[]; + query: string; + selectedPermission?: string; + users: PermissionUser[]; +} + +interface DispatchToProps { + grantPermissionToGroup: (groupName: string, permission: string) => void; + grantPermissionToUser: (login: string, permission: string) => void; + loadHolders: () => void; + onFilter: (filter: string) => void; + onSearch: (query: string) => void; + onSelectPermission: (permission: string) => void; + revokePermissionFromGroup: (groupName: string, permission: string) => void; + revokePermissionFromUser: (login: string, permission: string) => void; +} + +const mapStateToProps = (state: any) => ({ + filter: getPermissionsAppFilter(state), + groups: getPermissionsAppGroups(state), + query: getPermissionsAppQuery(state), + selectedPermission: getPermissionsAppSelectedPermission(state), + users: getPermissionsAppUsers(state) +}); + +const mapDispatchToProps = (dispatch: Function, ownProps: OwnProps) => { + const organizationKey = ownProps.organization ? ownProps.organization.key : undefined; + return { + grantPermissionToGroup: (groupName: string, permission: string) => + dispatch(grantToGroup(groupName, permission, organizationKey)), + grantPermissionToUser: (login: string, permission: string) => + dispatch(grantToUser(login, permission, organizationKey)), + loadHolders: () => dispatch(loadHolders(organizationKey)), + onFilter: (filter: string) => dispatch(updateFilter(filter, organizationKey)), + onSearch: (query: string) => dispatch(updateQuery(query, organizationKey)), + onSelectPermission: (permission: string) => + dispatch(selectPermission(permission, organizationKey)), + revokePermissionFromGroup: (groupName: string, permission: string) => + dispatch(revokeFromGroup(groupName, permission, organizationKey)), + revokePermissionFromUser: (login: string, permission: string) => + dispatch(revokeFromUser(login, permission, organizationKey)) + }; +}; + +export default connect<StateToProps, DispatchToProps, OwnProps>( + mapStateToProps, + mapDispatchToProps +)(AllHoldersList); diff --git a/server/sonar-web/src/main/js/apps/permissions/global/components/App.js b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx index 4b7ea9f15e1..fdd4aec3dcf 100644 --- a/server/sonar-web/src/main/js/apps/permissions/global/components/App.js +++ b/server/sonar-web/src/main/js/apps/permissions/global/components/App.tsx @@ -17,22 +17,26 @@ * 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 PageHeader from './PageHeader'; -import AllHoldersList from './AllHoldersList'; +import AllHoldersListContainer from './AllHoldersListContainer'; import PageError from '../../shared/components/PageError'; import { translate } from '../../../../helpers/l10n'; +import { Organization } from '../../../../app/types'; import '../../styles.css'; -export default function App(props /*: { organization?: {} } */) { +interface Props { + organization?: Organization; +} + +export default function App({ organization }: Props) { return ( <div className="page page-limited"> <Helmet title={translate('global_permissions.permission')} /> - <PageHeader organization={props.organization} /> + <PageHeader organization={organization} /> <PageError /> - <AllHoldersList organization={props.organization} /> + <AllHoldersListContainer organization={organization} /> </div> ); } diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx index 12a9c2af1ba..3fc076c002e 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.js +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/GroupHolder.tsx @@ -17,39 +17,35 @@ * 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 Checkbox from '../../../../components/controls/Checkbox'; import GroupIcon from '../../../../components/icons-components/GroupIcon'; +import { PermissionGroup } from '../../../../api/permissions'; -export default class GroupHolder extends React.PureComponent { - static propTypes = { - group: PropTypes.object.isRequired, - permissions: PropTypes.array.isRequired, - selectedPermission: PropTypes.string, - permissionsOrder: PropTypes.array.isRequired, - onToggle: PropTypes.func.isRequired - }; +interface Props { + group: PermissionGroup; + permissions: string[]; + selectedPermission?: string; + permissionsOrder: string[]; + onToggle: (group: PermissionGroup, permission: string) => void; +} - handleClick(permission, e) { - e.preventDefault(); - e.target.blur(); - this.props.onToggle(this.props.group, permission); - } +export default class GroupHolder extends React.PureComponent<Props> { + handleCheck = (_checked: boolean, permission?: string) => + permission && this.props.onToggle(this.props.group, permission); render() { const { selectedPermission } = this.props; - const permissionCells = this.props.permissionsOrder.map(p => ( + const permissionCells = this.props.permissionsOrder.map(permission => ( <td - key={p.key} + key={permission} className="text-center text-middle" - style={{ backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' }}> - <button className="button-clean" onClick={this.handleClick.bind(this, p.key)}> - {this.props.permissions.includes(p.key) ? ( - <i className="icon-checkbox icon-checkbox-checked" /> - ) : ( - <i className="icon-checkbox" /> - )} - </button> + style={{ backgroundColor: permission === selectedPermission ? '#d9edf7' : 'transparent' }}> + <Checkbox + checked={this.props.permissions.includes(permission)} + id={permission} + onCheck={this.handleCheck} + /> </td> )); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx index da8bc9aa11f..189351ba346 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.js +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/HoldersList.tsx @@ -17,69 +17,35 @@ * 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 UserHolder from './UserHolder'; import GroupHolder from './GroupHolder'; -import Tooltip from '../../../../components/controls/Tooltip'; -import { translate, translateWithParameters } from '../../../../helpers/l10n'; +import PermissionHeader, { Permission } from './PermissionHeader'; +import { PermissionUser, PermissionGroup } from '../../../../api/permissions'; +import { translate } from '../../../../helpers/l10n'; -export default class HoldersList extends React.PureComponent { - static propTypes = { - permissions: PropTypes.array.isRequired, - users: PropTypes.array.isRequired, - groups: PropTypes.array.isRequired, - selectedPermission: PropTypes.string, - showPublicProjectsWarning: PropTypes.bool, - onSelectPermission: PropTypes.func.isRequired, - onToggleUser: PropTypes.func.isRequired, - onToggleGroup: PropTypes.func.isRequired - }; - - static defaultProps = { - showPublicProjectsWarning: false - }; - - handlePermissionClick = event => { - event.preventDefault(); - event.currentTarget.blur(); - this.props.onSelectPermission(event.currentTarget.dataset.key); - }; - - renderTooltip = permission => - this.props.showPublicProjectsWarning && - (permission.key === 'user' || permission.key === 'codeviewer') ? ( - <div> - {permission.description} - <div className="alert alert-warning spacer-top"> - {translate('projects_role.public_projects_warning')} - </div> - </div> - ) : ( - permission.description - ); +interface Props { + permissions: Permission[]; + users: PermissionUser[]; + groups: PermissionGroup[]; + selectedPermission?: string; + showPublicProjectsWarning?: boolean; + onSelectPermission: (permission: string) => void; + onToggleUser: (user: PermissionUser, permission: string) => void; + onToggleGroup: (group: PermissionGroup, permission: string) => void; +} +export default class HoldersList extends React.PureComponent<Props> { renderTableHeader() { - const { selectedPermission } = this.props; + const { onSelectPermission, selectedPermission, showPublicProjectsWarning } = this.props; const cells = this.props.permissions.map(p => ( - <th + <PermissionHeader key={p.key} - className="permission-column text-center" - style={{ - backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' - }}> - <div className="permission-column-inner"> - <Tooltip - overlay={translateWithParameters('global_permissions.filter_by_x_permission', p.name)}> - <a data-key={p.key} href="#" onClick={this.handlePermissionClick}> - {p.name} - </a> - </Tooltip> - <Tooltip overlay={this.renderTooltip(p)}> - <i className="icon-help little-spacer-left" /> - </Tooltip> - </div> - </th> + onSelectPermission={onSelectPermission} + permission={p} + selectedPermission={selectedPermission} + showPublicProjectsWarning={showPublicProjectsWarning} + /> )); return ( <thead> @@ -101,13 +67,15 @@ export default class HoldersList extends React.PureComponent { } render() { + const permissionsOrder = this.props.permissions.map(p => p.key); + const users = this.props.users.map(user => ( <UserHolder key={'user-' + user.login} user={user} permissions={user.permissions} selectedPermission={this.props.selectedPermission} - permissionsOrder={this.props.permissions} + permissionsOrder={permissionsOrder} onToggle={this.props.onToggleUser} /> )); @@ -118,7 +86,7 @@ export default class HoldersList extends React.PureComponent { group={group} permissions={group.permissions} selectedPermission={this.props.selectedPermission} - permissionsOrder={this.props.permissions} + permissionsOrder={permissionsOrder} onToggle={this.props.onToggleGroup} /> )); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.tsx index e3d6266d94b..4f3878329c3 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.js +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/PageError.tsx @@ -17,28 +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 React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; import { connect } from 'react-redux'; import { getPermissionsAppError } from '../../../../store/rootReducer'; -class PageError extends React.PureComponent { - static propTypes = { - message: PropTypes.string - }; - - render() { - const { message } = this.props; - - if (!message) { - return null; - } +interface Props { + message: string; +} - return <div className="alert alert-danger">{message}</div>; +function PageError({ message }: Props) { + if (!message) { + return null; } + + return <div className="alert alert-danger">{message}</div>; } -const mapStateToProps = state => ({ +const mapStateToProps = (state: any) => ({ message: getPermissionsAppError(state) }); diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx new file mode 100644 index 00000000000..96d9e40d1c2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/PermissionHeader.tsx @@ -0,0 +1,87 @@ +/* + * 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 Tooltip from '../../../../components/controls/Tooltip'; +import { translate, translateWithParameters } from '../../../../helpers/l10n'; + +export interface Permission { + key: string; + name: string; + description: string; +} + +interface Props { + onSelectPermission: (permission: string) => void; + permission: Permission; + selectedPermission?: string; + showPublicProjectsWarning?: boolean; +} + +export default class PermissionHeader extends React.PureComponent<Props> { + static defaultProps = { + showPublicProjectsWarning: false + }; + + handlePermissionClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onSelectPermission(this.props.permission.key); + }; + + renderTooltip = (permission: Permission) => { + if (this.props.showPublicProjectsWarning && ['user', 'codeviewer'].includes(permission.key)) { + return ( + <div> + {permission.description} + <div className="alert alert-warning spacer-top"> + {translate('projects_role.public_projects_warning')} + </div> + </div> + ); + } + return permission.description; + }; + + render() { + const { permission, selectedPermission } = this.props; + return ( + <th + className="permission-column text-center" + style={{ + backgroundColor: permission.key === selectedPermission ? '#d9edf7' : 'transparent' + }}> + <div className="permission-column-inner"> + <Tooltip + overlay={translateWithParameters( + 'global_permissions.filter_by_x_permission', + permission.name + )}> + <a href="#" onClick={this.handlePermissionClick}> + {permission.name} + </a> + </Tooltip> + <Tooltip overlay={this.renderTooltip(permission)}> + <i className="icon-help little-spacer-left" /> + </Tooltip> + </div> + </th> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.tsx index b680b491fbd..903d3be562d 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.js +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/SearchForm.tsx @@ -17,13 +17,19 @@ * 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 RadioToggle from '../../../../components/controls/RadioToggle'; import SearchBox from '../../../../components/controls/SearchBox'; -import { translate, translateWithParameters } from '../../../../helpers/l10n'; +import { translate } from '../../../../helpers/l10n'; -export default function SearchForm(props) { +interface Props { + filter: string; + onFilter: (value: string) => void; + onSearch: (value: string) => void; + query: string; +} + +export default function SearchForm(props: Props) { const filterOptions = [ { value: 'all', label: translate('all') }, { value: 'users', label: translate('users.page') }, diff --git a/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx index d0b97ae588b..068c98f67af 100644 --- a/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.js +++ b/server/sonar-web/src/main/js/apps/permissions/shared/components/UserHolder.tsx @@ -17,69 +17,73 @@ * 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 Avatar from '../../../../components/ui/Avatar'; +import Checkbox from '../../../../components/controls/Checkbox'; +import { PermissionUser } from '../../../../api/permissions'; import { translate } from '../../../../helpers/l10n'; -export default class UserHolder extends React.PureComponent { - static propTypes = { - user: PropTypes.object.isRequired, - permissions: PropTypes.array.isRequired, - selectedPermission: PropTypes.string, - permissionsOrder: PropTypes.array.isRequired, - onToggle: PropTypes.func.isRequired - }; +interface Props { + user: PermissionUser; + permissions: string[]; + selectedPermission?: string; + permissionsOrder: string[]; + onToggle: (user: PermissionUser, permission: string) => void; +} - handleClick(permission, e) { - e.preventDefault(); - e.target.blur(); - this.props.onToggle(this.props.user, permission); - } +export default class UserHolder extends React.PureComponent<Props> { + handleCheck = (_checked: boolean, permission?: string) => + permission && this.props.onToggle(this.props.user, permission); render() { const { selectedPermission } = this.props; - const permissionCells = this.props.permissionsOrder.map(p => ( + const permissionCells = this.props.permissionsOrder.map(permission => ( <td - key={p.key} + key={permission} className="text-center text-middle" - style={{ backgroundColor: p.key === selectedPermission ? '#d9edf7' : 'transparent' }}> - <button className="button-clean" onClick={this.handleClick.bind(this, p.key)}> - {this.props.permissions.includes(p.key) ? ( - <i className="icon-checkbox icon-checkbox-checked" /> - ) : ( - <i className="icon-checkbox" /> - )} - </button> + style={{ backgroundColor: permission === selectedPermission ? '#d9edf7' : 'transparent' }}> + <Checkbox + checked={this.props.permissions.includes(permission)} + id={permission} + onCheck={this.handleCheck} + /> </td> )); const { user } = this.props; - - const isCreator = user.login === '<creator>'; + if (user.login === '<creator>') { + return ( + <tr> + <td className="nowrap"> + <div className="display-inline-block text-middle"> + <div> + <strong>{user.name}</strong> + </div> + <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}> + {translate('permission_templates.project_creators.explanation')} + </div> + </div> + </td> + {permissionCells} + </tr> + ); + } return ( <tr> <td className="nowrap"> - {!isCreator && ( - <Avatar - hash={user.avatar} - name={user.name} - size={36} - className="text-middle big-spacer-right" - /> - )} + <Avatar + hash={user.avatar} + name={user.name} + size={36} + className="text-middle big-spacer-right" + /> <div className="display-inline-block text-middle"> <div> <strong>{user.name}</strong> - {!isCreator && <span className="note spacer-left">{user.login}</span>} + <span className="note spacer-left">{user.login}</span> </div> - {!isCreator && <div className="little-spacer-top">{user.email}</div>} - {isCreator && ( - <div className="little-spacer-top" style={{ whiteSpace: 'normal' }}> - {translate('permission_templates.project_creators.explanation')} - </div> - )} + <div className="little-spacer-top">{user.email}</div> </div> </td> {permissionCells} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx index 00014d3b253..e17f7697ac1 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { mount } from 'enzyme'; import App, { Props } from '../App'; +import { Visibility } from '../../../app/types'; jest.mock('react-dom'); @@ -42,7 +43,7 @@ jest.mock('../../../api/components', () => ({ getComponents: jest.fn() })); const getComponents = require('../../../api/components').getComponents as jest.Mock<any>; -const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; +const organization = { key: 'org', name: 'org', projectVisibility: Visibility.Public }; const defaultSearchParameters = { organization: 'org', diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx index 61dce6dbfe4..4d3505afd22 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx @@ -21,12 +21,13 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import ChangeVisibilityForm, { Props } from '../ChangeVisibilityForm'; import { click } from '../../../helpers/testUtils'; +import { Visibility } from '../../../app/types'; const organization = { canUpdateProjectsVisibilityToPrivate: true, key: 'org', name: 'org', - projectVisibility: 'public' + projectVisibility: Visibility.Public }; it('renders disabled', () => { diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx index c514345b78a..2682a771619 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx @@ -28,10 +28,11 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import CreateProjectForm from '../CreateProjectForm'; import { change, submit, click } from '../../../helpers/testUtils'; +import { Visibility } from '../../../app/types'; const createProject = require('../../../api/components').createProject as jest.Mock<any>; -const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; +const organization = { key: 'org', name: 'org', projectVisibility: Visibility.Public }; it('creates project', async () => { const wrapper = shallow( diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx index b7cb62ab9a4..7d5b637949b 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx @@ -23,7 +23,7 @@ import Header, { Props } from '../Header'; import { Visibility } from '../../../app/types'; import { click } from '../../../helpers/testUtils'; -const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; +const organization = { key: 'org', name: 'org', projectVisibility: Visibility.Public }; it('renders', () => { expect(shallowRender()).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx index 824c6ab2a5e..71cf3dcbe80 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx @@ -21,8 +21,9 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import Search, { Props } from '../Search'; import { click } from '../../../helpers/testUtils'; +import { Visibility } from '../../../app/types'; -const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; +const organization = { key: 'org', name: 'org', projectVisibility: Visibility.Public }; it('renders', () => { expect(shallowRender()).toMatchSnapshot(); |