From 71fec25c4056c1dcfe75769c2041b1d56a89a2e5 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 5 Sep 2017 11:00:00 +0200 Subject: SONAR-9784 rewrite projects management page --- .../src/main/js/apps/projectsManagement/App.tsx | 246 ++++++++ .../js/apps/projectsManagement/AppContainer.tsx | 99 ++++ .../projectsManagement/BulkApplyTemplateModal.tsx | 217 +++++++ .../projectsManagement/ChangeVisibilityForm.tsx | 128 ++++ .../apps/projectsManagement/CreateProjectForm.tsx | 226 ++++++++ .../js/apps/projectsManagement/DeleteModal.tsx | 105 ++++ .../src/main/js/apps/projectsManagement/Header.tsx | 92 +++ .../main/js/apps/projectsManagement/ProjectRow.tsx | 96 +++ .../main/js/apps/projectsManagement/Projects.tsx | 68 +++ .../src/main/js/apps/projectsManagement/Search.tsx | 228 ++++++++ .../apps/projectsManagement/__tests__/App-test.tsx | 148 +++++ .../__tests__/BulkApplyTemplateModal-test.tsx | 128 ++++ .../__tests__/ChangeVisibilityForm-test.tsx | 73 +++ .../__tests__/CreateProjectForm-test.tsx | 72 +++ .../__tests__/DeleteModal-test.tsx | 63 ++ .../projectsManagement/__tests__/Header-test.tsx | 64 ++ .../__tests__/ProjectRow-test.tsx | 61 ++ .../projectsManagement/__tests__/Projects-test.tsx | 67 +++ .../projectsManagement/__tests__/Search-test.tsx | 107 ++++ .../BulkApplyTemplateModal-test.tsx.snap | 643 +++++++++++++++++++++ .../ChangeVisibilityForm-test.tsx.snap | 308 ++++++++++ .../__snapshots__/CreateProjectForm-test.tsx.snap | 468 +++++++++++++++ .../__snapshots__/DeleteModal-test.tsx.snap | 98 ++++ .../__tests__/__snapshots__/Header-test.tsx.snap | 56 ++ .../__snapshots__/ProjectRow-test.tsx.snap | 101 ++++ .../__tests__/__snapshots__/Projects-test.tsx.snap | 37 ++ .../__tests__/__snapshots__/Search-test.tsx.snap | 234 ++++++++ .../src/main/js/apps/projectsManagement/routes.ts | 35 ++ .../src/main/js/apps/projectsManagement/utils.ts | 40 ++ 29 files changed, 4308 insertions(+) create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/App.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/ChangeVisibilityForm.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/CreateProjectForm.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/routes.ts create mode 100644 server/sonar-web/src/main/js/apps/projectsManagement/utils.ts (limited to 'server/sonar-web/src/main/js/apps/projectsManagement') diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx new file mode 100644 index 00000000000..5a860abb5f7 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/App.tsx @@ -0,0 +1,246 @@ +/* + * 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 Helmet from 'react-helmet'; +import { debounce, uniq, without } from 'lodash'; +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, Project } from './utils'; +import { getComponents, getProvisioned, getGhosts } from '../../api/components'; +import { Organization } from '../../app/types'; +import { translate } from '../../helpers/l10n'; + +export interface Props { + hasProvisionPermission?: boolean; + onVisibilityChange: (visibility: string) => void; + organization: Organization; + topLevelQualifiers: string[]; +} + +interface State { + createProjectForm: boolean; + page: number; + projects: Project[]; + qualifiers: string; + query: string; + ready: boolean; + selection: string[]; + total: number; + type: Type; +} + +export default class App extends React.PureComponent { + mounted: boolean; + + constructor(props: Props) { + super(props); + this.state = { + createProjectForm: false, + ready: false, + projects: [], + total: 0, + page: 1, + query: '', + qualifiers: 'TRK', + type: Type.All, + selection: [] + }; + this.requestProjects = debounce(this.requestProjects, 250); + } + + componentDidMount() { + this.mounted = true; + this.requestProjects(); + } + + 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: + this.requestAllProjects(); + break; + case Type.Provisioned: + this.requestProvisioned(); + break; + case Type.Ghosts: + this.requestGhosts(); + break; + } + }; + + requestGhosts = () => { + const data = this.getFilters(); + getGhosts(data).then(r => { + 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 }); + } + }); + }; + + requestProvisioned = () => { + const data = this.getFilters(); + getProvisioned(data).then(r => { + 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 }); + } + }); + }; + + requestAllProjects = () => { + const data = this.getFilters(); + Object.assign(data, { qualifiers: this.state.qualifiers }); + getComponents(data).then(r => { + 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 }); + } + }); + }; + + loadMore = () => { + this.setState({ ready: false, page: this.state.page + 1 }, this.requestProjects); + }; + + onSearch = (query: string) => { + this.setState({ ready: false, page: 1, query, selection: [] }, this.requestProjects); + }; + + onTypeChanged = (newType: Type) => { + this.setState( + { ready: false, page: 1, query: '', type: newType, qualifiers: 'TRK', selection: [] }, + this.requestProjects + ); + }; + + onQualifierChanged = (newQualifier: string) => { + this.setState( + { ready: false, page: 1, query: '', type: Type.All, qualifiers: newQualifier, selection: [] }, + this.requestProjects + ); + }; + + onProjectSelected = (project: string) => { + const newSelection = uniq([...this.state.selection, project]); + this.setState({ selection: newSelection }); + }; + + onProjectDeselected = (project: string) => { + const newSelection = without(this.state.selection, project); + this.setState({ selection: newSelection }); + }; + + onAllSelected = () => { + const newSelection = this.state.projects.map(project => project.key); + this.setState({ selection: newSelection }); + }; + + onAllDeselected = () => { + this.setState({ selection: [] }); + }; + + openCreateProjectForm = () => { + this.setState({ createProjectForm: true }); + }; + + closeCreateProjectForm = () => { + this.setState({ createProjectForm: false }); + }; + + render() { + return ( +
+ + +
+ + + + + + + + {this.state.createProjectForm && + } +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx new file mode 100644 index 00000000000..0b00f3eba76 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/AppContainer.tsx @@ -0,0 +1,99 @@ +/* + * 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 { connect } from 'react-redux'; +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'; + +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 { + 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 + if (!this.props.organization || !this.props.organization.projectVisibility) { + this.props.fetchOrganization(this.props.appState.defaultOrganization); + } + } + + handleVisibilityChange = (visibility: string) => { + if (this.props.organization) { + this.props.onVisibilityChange(this.props.organization, visibility); + } + }; + + render() { + const { organization } = this.props; + + if (!organization) { + return null; + } + + const topLevelQualifiers = organization.isDefault ? this.props.appState.qualifiers : ['TRK']; + + return ( + + ); + } +} + +const mapStateToProps = (state: any, ownProps: Props) => ({ + appState: getAppState(state), + organization: + ownProps.organization || getOrganizationByKey(state, getAppState(state).defaultOrganization) +}); + +const onVisibilityChange = (organization: Organization, visibility: string) => ( + dispatch: Function +) => { + const currentVisibility = organization.projectVisibility; + dispatch(receiveOrganizations([{ ...organization, projectVisibility: visibility }])); + changeProjectVisibility(organization.key, visibility).catch(error => { + onFail(dispatch)(error); + dispatch(receiveOrganizations([{ ...organization, projectVisibility: currentVisibility }])); + }); +}; + +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); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx new file mode 100644 index 00000000000..a5f967fd04a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx @@ -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 { + 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) => { + 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 + ?
+ {translateWithParameters( + 'permission_templates.bulk_apply_permission_template.apply_to_selected', + this.props.selection.length + )} +
+ :
+ {translateWithParameters( + 'permission_templates.bulk_apply_permission_template.apply_to_all', + this.props.total + )} +
; + }; + + renderSelect = () => +
+ + +
+
+ + +
+
+ + +
+
+ + + {!organization.canUpdateProjectsVisibilityToPrivate && +
+ +
} +
+ + + + } + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx new file mode 100644 index 00000000000..7b4a45fc61b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/DeleteModal.tsx @@ -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 { + mounted: boolean; + state: State = { loading: false }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCancelClick = (event: React.SyntheticEvent) => { + 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 ( + +
+

+ {header} +

+
+ +
+ {translate('qualifiers.delete_confirm', this.props.qualifier)} +
+ + +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx new file mode 100644 index 00000000000..ef77181c740 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Header.tsx @@ -0,0 +1,92 @@ +/* + * 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 ChangeVisibilityForm from './ChangeVisibilityForm'; +import { Visibility } from './utils'; +import { Organization } from '../../app/types'; +import { translate } from '../../helpers/l10n'; + +export interface Props { + hasProvisionPermission?: boolean; + onProjectCreate: () => void; + onVisibilityChange: (visibility: Visibility) => void; + organization: Organization; +} + +interface State { + visibilityForm: boolean; +} + +export default class Header extends React.PureComponent { + state: State = { visibilityForm: false }; + + handleCreateProjectClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.props.onProjectCreate(); + }; + + handleChangeVisibilityClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.setState({ visibilityForm: true }); + }; + + closeVisiblityForm = () => { + this.setState({ visibilityForm: false }); + }; + + render() { + const { organization } = this.props; + + return ( +
+

+ {translate('projects_management')} +

+ +
+ + {translate('organization.default_visibility_of_new_projects')}{' '} + {translate('visibility', organization.projectVisibility)} + + + {this.props.hasProvisionPermission && + } +
+ +

+ {translate('projects_management.page.description')} +

+ + {this.state.visibilityForm && + } +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx new file mode 100644 index 00000000000..60f951dd32c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx @@ -0,0 +1,96 @@ +/* + * 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 { Link } from 'react-router'; +import { Project, Visibility } from './utils'; +import PrivateBadge from '../../components/common/PrivateBadge'; +import Checkbox from '../../components/controls/Checkbox'; +import QualifierIcon from '../../components/shared/QualifierIcon'; +import { translate } from '../../helpers/l10n'; +import { getComponentPermissionsUrl } from '../../helpers/urls'; + +interface Props { + onApplyTemplateClick: (project: Project) => void; + onProjectCheck: (project: Project, checked: boolean) => void; + project: Project; + selected: boolean; +} + +export default class ProjectRow extends React.PureComponent { + handleProjectCheck = (checked: boolean) => { + this.props.onProjectCheck(this.props.project, checked); + }; + + handleApplyTemplateClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.props.onApplyTemplateClick(this.props.project); + }; + + render() { + const { project, selected } = this.props; + + return ( + + + + + + + + {project.name} + + + + + + {project.key} + + + + + {project.visibility === Visibility.Private && } + + + + + + + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx new file mode 100644 index 00000000000..ff6264dce90 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx @@ -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 { + 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 ( + + + {this.props.projects.map(project => + + )} + +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx new file mode 100644 index 00000000000..914883f8359 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/Search.tsx @@ -0,0 +1,228 @@ +/* + * 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 { sortBy } from 'lodash'; +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 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; +} + +export default class Search extends React.PureComponent { + input: HTMLInputElement; + mounted: boolean; + state: State = { bulkApplyTemplateModal: false, deleteModal: false }; + + onSubmit = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.search(); + }; + + search = (event?: React.SyntheticEvent) => { + const q = event ? event.currentTarget.value : this.input.value; + this.props.onSearch(q); + }; + + getTypeOptions = () => [ + { value: Type.All, label: 'All' }, + { value: Type.Provisioned, label: 'Provisioned' }, + { value: Type.Ghosts, label: 'Ghosts' } + ]; + + getQualifierOptions = () => { + const options = this.props.topLevelQualifiers.map(q => { + return { value: q, label: translate('qualifiers', q) }; + }); + return sortBy(options, option => QUALIFIERS_ORDER.indexOf(option.value)); + }; + + onCheck = (checked: boolean) => { + if (checked) { + this.props.onAllSelected(); + } else { + this.props.onAllDeselected(); + } + }; + + handleDeleteClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ deleteModal: true }); + }; + + closeDeleteModal = () => { + this.setState({ deleteModal: false }); + }; + + handleDeleteConfirm = () => { + this.closeDeleteModal(); + this.props.onDeleteProjects(); + }; + + handleBulkApplyTemplateClick = (event: React.SyntheticEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + this.setState({ bulkApplyTemplateModal: true }); + }; + + closeBulkApplyTemplateModal = () => { + this.setState({ bulkApplyTemplateModal: false }); + }; + + renderCheckbox = () => { + const isAllChecked = + this.props.projects.length > 0 && this.props.selection.length === this.props.projects.length; + const thirdState = + this.props.projects.length > 0 && + this.props.selection.length > 0 && + this.props.selection.length < this.props.projects.length; + const checked = isAllChecked || thirdState; + return ; + }; + + renderGhostsDescription = () => { + if (this.props.type !== Type.Ghosts || !this.props.ready) { + return null; + } + return ( +
+ {translate('bulk_deletion.ghosts.description')} +
+ ); + }; + + renderQualifierFilter = () => { + const options = this.getQualifierOptions(); + if (options.length < 2) { + return null; + } + return ( + + + + ); + }; + + render() { + const isSomethingSelected = this.props.projects.length > 0 && this.props.selection.length > 0; + return ( +
+ + + + + {this.renderQualifierFilter()} + + + + + +
+ {this.props.ready ? this.renderCheckbox() : } + + + +
+ + (this.input = node!)} + className="search-box-input input-medium" + type="search" + placeholder="Search" + /> + +
+ + +
+ {this.renderGhostsDescription()} + + {this.state.bulkApplyTemplateModal && + } + + {this.state.deleteModal && + } +
+ ); + } +} 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 new file mode 100644 index 00000000000..1c9569d5700 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/App-test.tsx @@ -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; +const getProvisioned = require('../../../api/components').getProvisioned as jest.Mock; +const getGhosts = require('../../../api/components').getGhosts as jest.Mock; + +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('onTypeChanged')(Type.Provisioned); + expect(getProvisioned).lastCalledWith(defaultSearchParameters); + wrapper.find('Search').prop('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('onQualifierChanged')('VW'); + expect(getComponents).lastCalledWith({ ...defaultSearchParameters, qualifiers: 'VW' }); +}); + +it('searches', () => { + const wrapper = mountRender(); + wrapper.find('Search').prop('onSearch')('foo'); + expect(getComponents).lastCalledWith({ ...defaultSearchParameters, q: 'foo', qualifiers: 'TRK' }); +}); + +it('loads more', async () => { + const wrapper = mountRender(); + wrapper.find('ListFooter').prop('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('onProjectSelected')('foo'); + expect(wrapper.state('selection')).toEqual(['foo']); + + wrapper.find('Projects').prop('onProjectSelected')('bar'); + expect(wrapper.state('selection')).toEqual(['foo', 'bar']); + + // should not select already selected project + wrapper.find('Projects').prop('onProjectSelected')('bar'); + expect(wrapper.state('selection')).toEqual(['foo', 'bar']); + + wrapper.find('Projects').prop('onProjectDeselected')('foo'); + expect(wrapper.state('selection')).toEqual(['bar']); + + wrapper.find('Search').prop('onAllDeselected')(); + expect(wrapper.state('selection')).toEqual([]); + + wrapper.find('Search').prop('onAllSelected')(); + expect(wrapper.state('selection')).toEqual(['foo', 'bar']); +}); + +it('creates project', () => { + const wrapper = mountRender(); + expect(wrapper.find('CreateProjectForm').exists()).toBeFalsy(); + + wrapper.find('Header').prop('onProjectCreate')(); + expect(wrapper.find('CreateProjectForm').exists()).toBeTruthy(); + + wrapper.find('CreateProjectForm').prop('onProjectCreated')(); + expect(getComponents.mock.calls).toHaveLength(2); + + wrapper.find('CreateProjectForm').prop('onClose')(); + expect(wrapper.find('CreateProjectForm').exists()).toBeFalsy(); +}); + +it('changes default project visibility', () => { + const onVisibilityChange = jest.fn(); + const wrapper = mountRender({ onVisibilityChange }); + wrapper.find('Header').prop('onVisibilityChange')('private'); + expect(onVisibilityChange).toBeCalledWith('private'); +}); + +function mountRender(props?: { [P in keyof Props]?: Props[P] }) { + return mount( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx new file mode 100644 index 00000000000..1c8399e6eda --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/BulkApplyTemplateModal-test.tsx @@ -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; +const bulkApplyTemplate = require('../../../api/permissions').bulkApplyTemplate as jest.Mock; +const getPermissionTemplates = require('../../../api/permissions') + .getPermissionTemplates as jest.Mock; + +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 ( + + ); +} 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 new file mode 100644 index 00000000000..61dce6dbfe4 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ChangeVisibilityForm-test.tsx @@ -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( + + ); +} 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 new file mode 100644 index 00000000000..0212d094e8e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/CreateProjectForm-test.tsx @@ -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; + +const organization = { key: 'org', name: 'org', projectVisibility: 'public' }; + +it('creates project', async () => { + const wrapper = shallow( + + ); + (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('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(); +}); diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx new file mode 100644 index 00000000000..d24f30e9cdf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/DeleteModal-test.tsx @@ -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; + +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( + + ); +} 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 new file mode 100644 index 00000000000..726c6b186cb --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Header-test.tsx @@ -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('onConfirm')(Visibility.Private); + expect(onVisibilityChange).toBeCalledWith(Visibility.Private); + + modalWrapper.prop('onClose')(); + expect(wrapper.find('ChangeVisibilityForm').exists()).toBeFalsy(); +}); + +function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { + return shallow( +
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx new file mode 100644 index 00000000000..7b7bb09d5ae --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/ProjectRow-test.tsx @@ -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('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( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx new file mode 100644 index 00000000000..b1f165cde7a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Projects-test.tsx @@ -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('onProjectCheck')(projects[0], true); + expect(onProjectSelected).toBeCalledWith('a'); + + wrapper.find('ProjectRow').first().prop('onProjectCheck')(projects[0], false); + expect(onProjectDeselected).toBeCalledWith('a'); +}); + +it('opens modal to apply permission template', () => { + const wrapper = shallowRender({ projects }); + wrapper.find('ProjectRow').first().prop('onApplyTemplateClick')(projects[0]); + expect(ApplyTemplateView).toBeCalledWith({ organization, project: projects[0] }); +}); + +function shallowRender(props?: any) { + return shallow( + + ); +} 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 new file mode 100644 index 00000000000..950b78e1624 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/Search-test.tsx @@ -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('onCheck')('VW'); + expect(onQualifierChanged).toBeCalledWith('VW'); +}); + +it('updates type', () => { + const onTypeChanged = jest.fn(); + const wrapper = shallowRender({ onTypeChanged }); + wrapper.find('RadioToggle[name="projects-type"]').prop('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('onCheck')(true); + expect(onAllSelected).toBeCalled(); + + wrapper.find('Checkbox').prop('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('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('onClose')(); + expect(wrapper.find('BulkApplyTemplateModal').exists()).toBeFalsy(); +}); + +function shallowRender(props?: { [P in keyof Props]?: Props[P] }) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap new file mode 100644 index 00000000000..bc3e39d7f64 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/BulkApplyTemplateModal-test.tsx.snap @@ -0,0 +1,643 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`bulk applies template to all results 1`] = ` + +
+

+ permission_templates.bulk_apply_permission_template +

+
+
+ +
+ +
+`; + +exports[`bulk applies template to all results 2`] = ` + +
+

+ permission_templates.bulk_apply_permission_template +

+
+
+
+ permission_templates.bulk_apply_permission_template.apply_to_all.17 +
+
+ + +
+
+ +
+`; + +exports[`bulk applies template to all results 4`] = ` + +
+

+ permission_templates.bulk_apply_permission_template +

+
+
+
+ projects_role.apply_template.success +
+
+ +
+`; + +exports[`bulk applies template to selected results 1`] = ` + +
+

+ permission_templates.bulk_apply_permission_template +

+
+
+ +
+ +
+`; + +exports[`bulk applies template to selected results 2`] = ` + +
+

+ permission_templates.bulk_apply_permission_template +

+
+
+
+ permission_templates.bulk_apply_permission_template.apply_to_selected.2 +
+
+ + +
+
+ +
+`; + +exports[`bulk applies template to selected results 4`] = ` + +
+

+ permission_templates.bulk_apply_permission_template +

+
+
+
+ projects_role.apply_template.success +
+
+ +
+`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap new file mode 100644 index 00000000000..d46e88f161f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ChangeVisibilityForm-test.tsx.snap @@ -0,0 +1,308 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`changes visibility 1`] = ` + +
+

+ organization.change_visibility_form.header +

+
+
+
+

+ + + visibility.public + +

+

+ visibility.public.description.short +

+
+
+

+ + + visibility.private + +

+

+ visibility.private.description.short +

+
+
+ organization.change_visibility_form.warning +
+
+
+ + + cancel + +
+
+`; + +exports[`changes visibility 2`] = ` + +
+

+ organization.change_visibility_form.header +

+
+
+
+

+ + + visibility.public + +

+

+ visibility.public.description.short +

+
+
+

+ + + visibility.private + +

+

+ visibility.private.description.short +

+
+
+ organization.change_visibility_form.warning +
+
+
+ + + cancel + +
+
+`; + +exports[`renders disabled 1`] = ` + +
+

+ organization.change_visibility_form.header +

+
+
+
+

+ + + visibility.public + +

+

+ visibility.public.description.short +

+
+
+

+ + + visibility.private + +

+

+ visibility.private.description.short +

+
+ +
+
+ + + cancel + +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap new file mode 100644 index 00000000000..fad19337758 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/CreateProjectForm-test.tsx.snap @@ -0,0 +1,468 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`creates project 1`] = ` + +
+
+

+ qualifiers.create.TRK +

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+`; + +exports[`creates project 2`] = ` + +
+
+

+ qualifiers.create.TRK +

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+`; + +exports[`creates project 3`] = ` + +
+
+

+ qualifiers.create.TRK +

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ + +
+`; + +exports[`creates project 4`] = ` + +
+
+

+ qualifiers.create.TRK +

+
+
+
+ Project + + name + + + has been successfully created. +
+
+ +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap new file mode 100644 index 00000000000..e57788fbc92 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/DeleteModal-test.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`deletes projects 1`] = ` + +
+

+ qualifiers.delete.TRK +

+
+
+ qualifiers.delete_confirm.TRK +
+ +
+`; + +exports[`deletes projects 2`] = ` + +
+

+ qualifiers.delete.TRK +

+
+
+ qualifiers.delete_confirm.TRK +
+ +
+`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap new file mode 100644 index 00000000000..9fb16cfbed5 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Header-test.tsx.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`changes default visibility 1`] = ` + +`; + +exports[`renders 1`] = ` +
+

+ projects_management +

+
+ + organization.default_visibility_of_new_projects + + + visibility.public + + + + +
+

+ projects_management.page.description +

+
+`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap new file mode 100644 index 00000000000..b306b2fe020 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders 1`] = ` + + + + + + + + + + Project + + + + + + project + + + + + + + + + +`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap new file mode 100644 index 00000000000..14bb03d1ec6 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Projects-test.tsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders list of projects 1`] = ` + + + + + +
+`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap new file mode 100644 index 00000000000..84838c7bc2e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/Search-test.tsx.snap @@ -0,0 +1,234 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`bulk applies permission template 1`] = ` + +`; + +exports[`deletes projects 1`] = ` + +`; + +exports[`render qualifiers filter 1`] = ` +
+ + + + + + + + + + +
+ + + + + + +
+ + + +
+ + +
+
+`; + +exports[`renders 1`] = ` +
+ + + + + + + + + +
+ + + + +
+ + + +
+ + +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts b/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts new file mode 100644 index 00000000000..447c6ae73dd --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/routes.ts @@ -0,0 +1,35 @@ +/* + * 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 { RouterState, IndexRouteProps } from 'react-router'; + +const routes = [ + { + getIndexRoute(_: RouterState, callback: (err: any, route: IndexRouteProps) => any) { + Promise.all([ + import('./AppContainer').then(i => i.default), + import('../organizations/forSingleOrganization').then(i => i.default) + ]).then(([AppContainer, forSingleOrganization]) => + callback(null, { component: forSingleOrganization(AppContainer) }) + ); + } + } +]; + +export default routes; diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts new file mode 100644 index 00000000000..4e3f01888b3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectsManagement/utils.ts @@ -0,0 +1,40 @@ +/* + * 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. + */ +export const PAGE_SIZE = 50; + +export const QUALIFIERS_ORDER = ['TRK', 'VW', 'APP', 'DEV']; + +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' +} -- cgit v1.2.3