diff options
author | Mathieu Suen <mathieu.suen@sonarsource.com> | 2020-11-12 15:04:38 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-11-18 20:05:41 +0000 |
commit | 1842a8550f4e4ec8ddaf163a10d251509f0019bb (patch) | |
tree | 8797be5060492b1bf4abee4625b2aa945b14f820 | |
parent | abba17fd8109c9bbed5ad2f69315e361d3b54bd7 (diff) | |
download | sonarqube-1842a8550f4e4ec8ddaf163a10d251509f0019bb.tar.gz sonarqube-1842a8550f4e4ec8ddaf163a10d251509f0019bb.zip |
Fix frontend duplication.
18 files changed, 585 insertions, 761 deletions
diff --git a/server/sonar-web/src/main/js/apps/application-console/ApplicationView.tsx b/server/sonar-web/src/main/js/apps/application-console/ApplicationConsoleApp.tsx index 32b1be17275..4313a936f6e 100644 --- a/server/sonar-web/src/main/js/apps/application-console/ApplicationView.tsx +++ b/server/sonar-web/src/main/js/apps/application-console/ApplicationConsoleApp.tsx @@ -18,21 +18,25 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Location } from 'history'; import * as React from 'react'; import { InjectedRouter } from 'react-router'; -import { getApplicationDetails } from '../../api/application'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { + deleteApplication, + editApplication, + getApplicationDetails, + refreshApplication +} from '../../api/application'; +import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; import { Application, ApplicationProject } from '../../types/application'; -import ApplicationDetails from './ApplicationDetails'; +import ApplicationConsoleAppRenderer from './ApplicationConsoleAppRenderer'; import { ApplicationBranch } from './utils'; interface Props { - applicationKey: string; - canRecompute?: boolean; - onDelete: (key: string) => void; - onEdit: (key: string, name: string) => void; - pathname: string; + component: { key: string }; + location: Location; router: Pick<InjectedRouter, 'replace'>; - single?: boolean; } interface State { @@ -44,7 +48,7 @@ export default class ApplicationView extends React.PureComponent<Props, State> { mounted = false; state: State = { - loading: true + loading: false }; componentDidMount() { @@ -53,7 +57,7 @@ export default class ApplicationView extends React.PureComponent<Props, State> { } componentDidUpdate(prevProps: Props) { - if (prevProps.applicationKey !== this.props.applicationKey) { + if (prevProps.component.key !== this.props.component.key) { this.fetchDetails(); } } @@ -62,9 +66,19 @@ export default class ApplicationView extends React.PureComponent<Props, State> { this.mounted = false; } + updateApplicationState = (buildNewFields: (prevApp: Application) => Partial<Application>) => { + this.setState(state => { + if (state.application) { + return { application: { ...state.application, ...buildNewFields(state.application) } }; + } else { + return null; + } + }); + }; + fetchDetails = async () => { try { - const application = await getApplicationDetails(this.props.applicationKey); + const application = await getApplicationDetails(this.props.component.key); if (this.mounted) { this.setState({ application, loading: false }); } @@ -75,94 +89,63 @@ export default class ApplicationView extends React.PureComponent<Props, State> { } }; - handleDelete = (key: string) => { + handleRefreshClick = async () => { + if (this.state.application) { + await refreshApplication(this.state.application.key); + addGlobalSuccessMessage(translate('application_console.refresh_started')); + } + }; + + handleDelete = async () => { if (this.mounted) { - this.props.onDelete(key); - this.props.router.replace(this.props.pathname); + if (this.state.application) { + await deleteApplication(this.state.application.key); + } + this.props.router.replace('/'); } }; - handleEdit = (key: string, name: string, description: string) => { + handleEdit = async (name: string, description: string) => { + if (this.state.application) { + await editApplication(this.state.application.key, name, description); + } + if (this.mounted) { - this.props.onEdit(key, name); - this.setState(state => { - if (state.application) { - return { - application: { - ...state.application, - name, - description - } - }; - } else { - return null; - } - }); + this.updateApplicationState(() => ({ name, description })); } }; handleAddProject = (project: ApplicationProject) => { - this.setState(state => { - if (state.application) { - return { - application: { - ...state.application, - projects: [...state.application.projects, project] - } - }; - } else { - return null; - } - }); + this.updateApplicationState(prevApp => ({ projects: [...prevApp.projects, project] })); }; handleRemoveProject = (projectKey: string) => { - this.setState(state => { - if (state.application) { - return { - application: { - ...state.application, - projects: state.application.projects.filter(p => p.key !== projectKey) - } - }; - } else { - return null; - } - }); + this.updateApplicationState(prevApp => ({ + projects: prevApp.projects.filter(p => p.key !== projectKey) + })); }; handleUpdateBranches = (branches: ApplicationBranch[]) => { - this.setState(state => { - if (state.application) { - return { application: { ...state.application, branches } }; - } else { - return null; - } - }); + this.updateApplicationState(() => ({ branches })); }; render() { - if (this.state.loading) { - return <i className="spinner spacer" />; - } - - const { application } = this.state; + const { application, loading } = this.state; if (!application) { // when application is not found return null; } return ( - <ApplicationDetails + <ApplicationConsoleAppRenderer + loading={loading} application={application} - canRecompute={this.props.canRecompute} onAddProject={this.handleAddProject} onDelete={this.handleDelete} onEdit={this.handleEdit} + onRefresh={this.handleRefreshClick} onRemoveProject={this.handleRemoveProject} onUpdateBranches={this.handleUpdateBranches} - pathname={this.props.pathname} - single={this.props.single} /> ); } diff --git a/server/sonar-web/src/main/js/apps/application-console/ApplicationConsoleAppRenderer.tsx b/server/sonar-web/src/main/js/apps/application-console/ApplicationConsoleAppRenderer.tsx new file mode 100644 index 00000000000..8da536476cf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/application-console/ApplicationConsoleAppRenderer.tsx @@ -0,0 +1,121 @@ +/* + * SonarQube + * Copyright (C) 2009-2020 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 { Button } from 'sonar-ui-common/components/controls/buttons'; +import ConfirmButton from 'sonar-ui-common/components/controls/ConfirmButton'; +import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { Application, ApplicationProject } from '../../types/application'; +import { Branch } from '../../types/branch-like'; +import ApplicationBranches from './ApplicationBranches'; +import ApplicationProjects from './ApplicationProjects'; +import EditForm from './EditForm'; + +export interface ApplicationConsoleAppRendererProps { + loading: boolean; + application: Application; + onAddProject: (project: ApplicationProject) => void; + onDelete: () => void; + onRefresh: () => void; + onEdit: (name: string, description: string) => Promise<void>; + onRemoveProject: (projectKey: string) => void; + onUpdateBranches: (branches: Branch[]) => void; +} + +export default function ApplicationConsoleAppRenderer(props: ApplicationConsoleAppRendererProps) { + const [editing, setEditing] = React.useState(false); + + const { application, loading } = props; + + if (loading) { + return <i className="spinner spacer" />; + } + + return ( + <div className="page page-limited"> + <div className="boxed-group" id="view-details"> + <div className="boxed-group-actions"> + <Button + className="little-spacer-right" + id="view-details-edit" + onClick={() => setEditing(true)}> + {translate('edit')} + </Button> + <Button className="little-spacer-right" onClick={props.onRefresh}> + {translate('application_console.recompute')} + </Button> + + <ConfirmButton + confirmButtonText={translate('delete')} + isDestructive={true} + modalBody={translateWithParameters( + 'application_console.do_you_want_to_delete', + application.name + )} + modalHeader={translate('application_console.delete_application')} + onConfirm={props.onDelete}> + {({ onClick }) => ( + <Button className="button-red" id="view-details-delete" onClick={onClick}> + {translate('delete')} + </Button> + )} + </ConfirmButton> + </div> + + <header className="boxed-group-header" id="view-details-header"> + <h2 className="text-limited" title={application.name}> + {application.name} + </h2> + </header> + + <div className="boxed-group-inner" id="view-details-content"> + <div className="big-spacer-bottom"> + {application.description && ( + <div className="little-spacer-bottom">{application.description}</div> + )} + <div className="subtitle"> + {translate('key')}: {application.key} + </div> + </div> + + <ApplicationProjects + onAddProject={props.onAddProject} + onRemoveProject={props.onRemoveProject} + application={application} + /> + + <ApplicationBranches + application={application} + onUpdateBranches={props.onUpdateBranches} + /> + </div> + + {editing && ( + <EditForm + header={translate('portfolios.edit_application')} + onClose={() => setEditing(false)} + onEdit={props.onEdit} + application={application} + /> + )} + </div> + </div> + ); +} diff --git a/server/sonar-web/src/main/js/apps/application-console/ApplicationDetails.tsx b/server/sonar-web/src/main/js/apps/application-console/ApplicationDetails.tsx deleted file mode 100644 index f5f952684cb..00000000000 --- a/server/sonar-web/src/main/js/apps/application-console/ApplicationDetails.tsx +++ /dev/null @@ -1,180 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 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 { Link } from 'react-router'; -import { Button } from 'sonar-ui-common/components/controls/buttons'; -import ConfirmButton from 'sonar-ui-common/components/controls/ConfirmButton'; -import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; -import { deleteApplication, editApplication, refreshApplication } from '../../api/application'; -import addGlobalSuccessMessage from '../../app/utils/addGlobalSuccessMessage'; -import { Application, ApplicationProject } from '../../types/application'; -import { Branch } from '../../types/branch-like'; -import ApplicationBranches from './ApplicationBranches'; -import ApplicationDetailsProjects from './ApplicationDetailsProjects'; -import EditForm from './EditForm'; - -interface Props { - application: Application; - canRecompute: boolean | undefined; - onAddProject: (project: ApplicationProject) => void; - onDelete: (key: string) => void; - onEdit: (key: string, name: string, description: string) => void; - onRemoveProject: (projectKey: string) => void; - onUpdateBranches: (branches: Branch[]) => void; - pathname: string; - single: boolean | undefined; -} - -interface State { - editing: boolean; - loading: boolean; -} - -export default class ApplicationDetails extends React.PureComponent<Props, State> { - mounted = false; - - state: State = { - editing: false, - loading: false - }; - - componentDidMount() { - this.mounted = true; - } - - componenWillUnmount() { - this.mounted = false; - } - - handleRefreshClick = () => { - this.setState({ loading: true }); - refreshApplication(this.props.application.key).then(() => { - addGlobalSuccessMessage(translate('application_console.refresh_started')); - this.stopLoading(); - }, this.stopLoading); - }; - - handleDelete = async () => { - await deleteApplication(this.props.application.key); - this.props.onDelete(this.props.application.key); - }; - - handleEditClick = () => { - this.setState({ editing: true }); - }; - - handleEditFormClose = () => { - this.setState({ editing: false }); - }; - - stopLoading = () => { - if (this.mounted) { - this.setState({ loading: false }); - } - }; - - render() { - const { loading } = this.state; - const { application } = this.props; - const canDelete = !this.props.single; - return ( - <div className="boxed-group portfolios-console-details" id="view-details"> - <div className="boxed-group-actions"> - <Button - className="little-spacer-right" - id="view-details-edit" - onClick={this.handleEditClick}> - {translate('edit')} - </Button> - {this.props.canRecompute && ( - <Button - className="little-spacer-right" - disabled={loading} - onClick={this.handleRefreshClick}> - {loading && <i className="little-spacer-right spinner" />} - {translate('application_console.recompute')} - </Button> - )} - {canDelete && ( - <ConfirmButton - confirmButtonText={translate('delete')} - isDestructive={true} - modalBody={translateWithParameters( - 'application_console.do_you_want_to_delete', - application.name - )} - modalHeader={translate('application_console.delete_application')} - onConfirm={this.handleDelete}> - {({ onClick }) => ( - <Button className="button-red" id="view-details-delete" onClick={onClick}> - {translate('delete')} - </Button> - )} - </ConfirmButton> - )} - </div> - - <header className="boxed-group-header" id="view-details-header"> - <h2 className="text-limited" title={application.name}> - {application.name} - </h2> - </header> - - <div className="boxed-group-inner" id="view-details-content"> - <div className="big-spacer-bottom"> - {application.description && ( - <div className="little-spacer-bottom">{application.description}</div> - )} - <div className="subtitle"> - {translate('key')}: {application.key} - <Link - className="spacer-left" - to={{ pathname: '/dashboard', query: { id: application.key } }}> - {translate('application_console.open_dashbard')} - </Link> - </div> - </div> - - <ApplicationDetailsProjects - onAddProject={this.props.onAddProject} - onRemoveProject={this.props.onRemoveProject} - application={this.props.application} - /> - - <ApplicationBranches - application={this.props.application} - onUpdateBranches={this.props.onUpdateBranches} - /> - </div> - - {this.state.editing && ( - <EditForm - header={translate('portfolios.edit_application')} - onChange={editApplication} - onClose={this.handleEditFormClose} - onEdit={this.props.onEdit} - application={application} - /> - )} - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/application-console/ApplicationDetailsProjects.tsx b/server/sonar-web/src/main/js/apps/application-console/ApplicationProjects.tsx index 88fa3a2a300..5257d5b2b3f 100644 --- a/server/sonar-web/src/main/js/apps/application-console/ApplicationDetailsProjects.tsx +++ b/server/sonar-web/src/main/js/apps/application-console/ApplicationProjects.tsx @@ -47,7 +47,7 @@ interface State { selectedProjects: string[]; } -export default class ApplicationDetailsProjects extends React.PureComponent<Props, State> { +export default class ApplicationProjects extends React.PureComponent<Props, State> { mounted = false; constructor(props: Props) { diff --git a/server/sonar-web/src/main/js/apps/application-console/ConsoleApplicationApp.tsx b/server/sonar-web/src/main/js/apps/application-console/ConsoleApplicationApp.tsx deleted file mode 100644 index 6ed3851a3f2..00000000000 --- a/server/sonar-web/src/main/js/apps/application-console/ConsoleApplicationApp.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 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 { Location } from 'history'; -import * as React from 'react'; -import { InjectedRouter } from 'react-router'; -import ApplicationView from './ApplicationView'; - -interface Props { - component: { key: string }; - location: Location; - router: InjectedRouter; -} - -export default class ConsoleApplicationApp extends React.PureComponent<Props> { - doNothing = () => {}; - - render() { - return ( - <div className="page page-limited"> - <div className="navigator-content"> - <ApplicationView - applicationKey={this.props.component.key} - canRecompute={true} - onDelete={this.doNothing} - onEdit={this.doNothing} - pathname={this.props.location.pathname} - router={this.props.router} - /> - </div> - </div> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/application-console/EditForm.tsx b/server/sonar-web/src/main/js/apps/application-console/EditForm.tsx index 24c7caa3de8..3f6a8ffa6a6 100644 --- a/server/sonar-web/src/main/js/apps/application-console/EditForm.tsx +++ b/server/sonar-web/src/main/js/apps/application-console/EditForm.tsx @@ -23,20 +23,13 @@ import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/contro import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal'; import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; import { translate } from 'sonar-ui-common/helpers/l10n'; +import { Application } from '../../types/application'; -interface Commons { - desc?: string; - description?: string; - key: string; - name: string; -} - -interface Props<T extends Commons> { +interface Props { header: string; - onChange: (key: string, name: string, description: string) => Promise<void>; onClose: () => void; - onEdit: (key: string, name: string, description: string) => void; - application: T; + onEdit: (name: string, description: string) => Promise<void>; + application: Application; } interface State { @@ -44,11 +37,11 @@ interface State { name: string; } -export default class EditForm<T extends Commons> extends React.PureComponent<Props<T>, State> { - constructor(props: Props<T>) { +export default class EditForm extends React.PureComponent<Props, State> { + constructor(props: Props) { super(props); this.state = { - description: props.application.desc || props.application.description || '', + description: props.application.description || '', name: props.application.name }; } @@ -61,13 +54,9 @@ export default class EditForm<T extends Commons> extends React.PureComponent<Pro this.setState({ description: event.currentTarget.value }); }; - handleFormSubmit = () => { - return this.props - .onChange(this.props.application.key, this.state.name, this.state.description) - .then(() => { - this.props.onEdit(this.props.application.key, this.state.name, this.state.description); - this.props.onClose(); - }); + handleFormSubmit = async () => { + await this.props.onEdit(this.state.name, this.state.description); + this.props.onClose(); }; render() { diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationView-test.tsx b/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationConsoleApp-test.tsx index 5e16858561c..840da580ef1 100644 --- a/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationView-test.tsx +++ b/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationConsoleApp-test.tsx @@ -20,18 +20,51 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; -import { getApplicationDetails } from '../../../api/application'; +import { + deleteApplication, + editApplication, + getApplicationDetails, + refreshApplication +} from '../../../api/application'; +import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage'; import { mockApplication, mockApplicationProject } from '../../../helpers/mocks/application'; -import { mockRouter } from '../../../helpers/testMocks'; +import { mockLocation, mockRouter } from '../../../helpers/testMocks'; import { Application } from '../../../types/application'; -import ApplicationView from '../ApplicationView'; +import ApplicationConsoleApp from '../ApplicationConsoleApp'; jest.mock('../../../api/application', () => ({ - getApplicationDetails: jest.fn().mockResolvedValue({}) + getApplicationDetails: jest.fn().mockResolvedValue({}), + refreshApplication: jest.fn().mockResolvedValue({}), + deleteApplication: jest.fn(), + editApplication: jest.fn().mockResolvedValue({}) })); -it('Should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); +jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ default: jest.fn() })); + +it('Should render correctly', async () => { + expect(shallowRender()).toMatchSnapshot('Empty app'); + const app = mockApplication(); + (getApplicationDetails as jest.Mock<Promise<Application>>).mockResolvedValueOnce(app); + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot('With app'); +}); + +it('Should edit application correctly', async () => { + const app = mockApplication(); + + (getApplicationDetails as jest.Mock<Promise<Application>>).mockRejectedValueOnce(app); + let wrapper = shallowRender({}); + wrapper.instance().handleEdit('NEW_NAME', 'NEW_DESC'); + expect(wrapper.state().application).toBeUndefined(); + + (getApplicationDetails as jest.Mock<Promise<Application>>).mockResolvedValueOnce(app); + wrapper = shallowRender({}); + await waitAndUpdate(wrapper); + await wrapper.instance().handleEdit('NEW_NAME', 'NEW_DESC'); + expect(editApplication).toBeCalledWith(app.key, 'NEW_NAME', 'NEW_DESC'); + expect(wrapper.state().application?.name).toBe('NEW_NAME'); + expect(wrapper.state().application?.description).toBe('NEW_DESC'); }); it('Should add project to application', async () => { @@ -65,44 +98,47 @@ it('Should remove project from application', async () => { expect(wrapper.state().application?.projects.length).toBe(0); }); -it('Should edit application correctly', async () => { +it('Should update branch correctly', async () => { const app = mockApplication(); (getApplicationDetails as jest.Mock<Promise<Application>>).mockRejectedValueOnce(app); let wrapper = shallowRender({}); - wrapper.instance().handleEdit(app.key, 'NEW_NAME', 'NEW_DESC'); + wrapper.instance().handleUpdateBranches([]); expect(wrapper.state().application).toBeUndefined(); (getApplicationDetails as jest.Mock<Promise<Application>>).mockResolvedValueOnce(app); wrapper = shallowRender({}); await waitAndUpdate(wrapper); - wrapper.instance().handleEdit(app.key, 'NEW_NAME', 'NEW_DESC'); - expect(wrapper.state().application?.name).toBe('NEW_NAME'); - expect(wrapper.state().application?.description).toBe('NEW_DESC'); + wrapper.instance().handleUpdateBranches([]); + expect(wrapper.state().application?.branches.length).toBe(0); }); -it('Should update branch correctly', async () => { +it('should handle refreshing', async () => { + const app = mockApplication(); + (getApplicationDetails as jest.Mock<Promise<Application>>).mockResolvedValueOnce(app); + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + wrapper.instance().handleRefreshClick(); + await waitAndUpdate(wrapper); + expect(refreshApplication).toBeCalledWith('foo'); + expect(addGlobalSuccessMessage).toBeCalled(); +}); +it('should handle deleting', async () => { const app = mockApplication(); - - (getApplicationDetails as jest.Mock<Promise<Application>>).mockRejectedValueOnce(app); - let wrapper = shallowRender({}); - wrapper.instance().handleUpdateBranches([]); - expect(wrapper.state().application).toBeUndefined(); (getApplicationDetails as jest.Mock<Promise<Application>>).mockResolvedValueOnce(app); - wrapper = shallowRender({}); + const wrapper = shallowRender({}); await waitAndUpdate(wrapper); - wrapper.instance().handleUpdateBranches([]); - expect(wrapper.state().application?.branches.length).toBe(0); + wrapper.instance().handleDelete(); + await waitAndUpdate(wrapper); + expect(deleteApplication).toBeCalledWith(app.key); }); -function shallowRender(props: Partial<ApplicationView['props']> = {}) { - return shallow<ApplicationView>( - <ApplicationView - applicationKey={'1'} - onDelete={jest.fn()} - onEdit={jest.fn()} - pathname={'test'} +function shallowRender(props: Partial<ApplicationConsoleApp['props']> = {}) { + return shallow<ApplicationConsoleApp>( + <ApplicationConsoleApp + component={{ key: '1' }} + location={mockLocation()} router={mockRouter()} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationDetails-test.tsx b/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationConsoleRender-test.tsx index 0da4e909654..4bb410e28cd 100644 --- a/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationDetails-test.tsx +++ b/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationConsoleRender-test.tsx @@ -20,19 +20,13 @@ import { shallow } from 'enzyme'; import * as React from 'react'; -import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; -import { deleteApplication, refreshApplication } from '../../../api/application'; -import addGlobalSuccessMessage from '../../../app/utils/addGlobalSuccessMessage'; +import { click } from 'sonar-ui-common/helpers/testUtils'; import { mockApplication } from '../../../helpers/mocks/application'; -import ApplicationDetails from '../ApplicationDetails'; +import ApplicationConsoleAppRenderer, { + ApplicationConsoleAppRendererProps +} from '../ApplicationConsoleAppRenderer'; import EditForm from '../EditForm'; -jest.mock('../../../api/application', () => ({ - deleteApplication: jest.fn().mockResolvedValue({}), - editApplication: jest.fn(), - refreshApplication: jest.fn().mockResolvedValue({}) -})); - jest.mock('../../../app/utils/addGlobalSuccessMessage', () => ({ default: jest.fn() })); @@ -41,11 +35,10 @@ it('should render correctly', () => { expect(shallowRender()).toMatchSnapshot('default'); expect( shallowRender({ - application: mockApplication({ description: 'Foo bar', key: 'foo' }), - canRecompute: true, - single: false + application: mockApplication({ description: 'Foo bar', key: 'foo' }) }) ).toMatchSnapshot('can delete and recompute'); + expect(shallowRender({ loading: true })).toMatchSnapshot('is loading'); }); it('should handle editing', () => { @@ -54,37 +47,17 @@ it('should handle editing', () => { expect(wrapper.find(EditForm)).toMatchSnapshot('edit form'); }); -it('should handle deleting', async () => { - const onDelete = jest.fn(); - const wrapper = shallowRender({ onDelete, single: false }); - - wrapper.instance().handleDelete(); - expect(deleteApplication).toBeCalledWith('foo'); - await waitAndUpdate(wrapper); - expect(onDelete).toBeCalledWith('foo'); -}); - -it('should handle refreshing', async () => { - const wrapper = shallowRender({ single: false }); - - wrapper.instance().handleRefreshClick(); - expect(refreshApplication).toBeCalledWith('foo'); - await waitAndUpdate(wrapper); - expect(addGlobalSuccessMessage).toBeCalled(); -}); - -function shallowRender(props: Partial<ApplicationDetails['props']> = {}) { - return shallow<ApplicationDetails>( - <ApplicationDetails +function shallowRender(props: Partial<ApplicationConsoleAppRendererProps> = {}) { + return shallow( + <ApplicationConsoleAppRenderer application={mockApplication({ key: 'foo' })} - canRecompute={false} + loading={false} onAddProject={jest.fn()} onDelete={jest.fn()} onEdit={jest.fn()} + onRefresh={jest.fn()} onRemoveProject={jest.fn()} onUpdateBranches={jest.fn()} - pathname="path/name" - single={true} {...props} /> ); diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationDetailsProjects-test.tsx b/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationProjects-test.tsx index a484ea41d4c..96487400fd6 100644 --- a/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationDetailsProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/application-console/__tests__/ApplicationProjects-test.tsx @@ -28,7 +28,7 @@ import { removeProjectFromApplication } from '../../../api/application'; import { mockApplication } from '../../../helpers/mocks/application'; -import ApplicationDetailsProjects from '../ApplicationDetailsProjects'; +import ApplicationProjects from '../ApplicationProjects'; jest.mock('../../../api/application', () => ({ getApplicationProjects: jest.fn().mockResolvedValue({ @@ -91,8 +91,8 @@ it('should refresh properly if props changes', () => { expect(spy).toHaveBeenCalled(); }); -function shallowRender(props: Partial<ApplicationDetailsProjects['props']> = {}) { - return shallow<ApplicationDetailsProjects>( - <ApplicationDetailsProjects application={mockApplication()} {...props} /> +function shallowRender(props: Partial<ApplicationProjects['props']> = {}) { + return shallow<ApplicationProjects>( + <ApplicationProjects application={mockApplication()} {...props} /> ); } diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/ConsoleApplicationApp-test.tsx b/server/sonar-web/src/main/js/apps/application-console/__tests__/ConsoleApplicationApp-test.tsx deleted file mode 100644 index 5328b5623a4..00000000000 --- a/server/sonar-web/src/main/js/apps/application-console/__tests__/ConsoleApplicationApp-test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2020 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 { shallow } from 'enzyme'; -import * as React from 'react'; -import { mockComponent, mockLocation, mockRouter } from '../../../helpers/testMocks'; -import ConsoleApplicationApp from '../ConsoleApplicationApp'; - -it('Should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); -}); - -function shallowRender(props: Partial<ConsoleApplicationApp['props']> = {}) { - return shallow<ConsoleApplicationApp>( - <ConsoleApplicationApp - component={mockComponent()} - location={mockLocation()} - router={mockRouter()} - {...props} - /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/EditForm-test.tsx b/server/sonar-web/src/main/js/apps/application-console/__tests__/EditForm-test.tsx index 08145c96f3f..7268ca71083 100644 --- a/server/sonar-web/src/main/js/apps/application-console/__tests__/EditForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/application-console/__tests__/EditForm-test.tsx @@ -24,7 +24,6 @@ import * as React from 'react'; import SimpleModal from 'sonar-ui-common/components/controls/SimpleModal'; import { change, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; import { mockApplication } from '../../../helpers/mocks/application'; -import { Application } from '../../../types/application'; import EditForm from '../EditForm'; it('should render correctly', () => { @@ -36,27 +35,24 @@ it('should render correctly', () => { }); it('should correctly submit the new info', async () => { - const onChange = jest.fn().mockResolvedValue({}); const onClose = jest.fn(); const onEdit = jest.fn(); - const wrapper = shallowRender({ onChange, onClose, onEdit }); + const wrapper = shallowRender({ onClose, onEdit }); const modal = wrapper.find(SimpleModal).dive(); change(modal.find('#view-edit-name'), 'New name'); change(modal.find('#view-edit-description'), 'New description'); wrapper.instance().handleFormSubmit(); - expect(onChange).toBeCalledWith('foo', 'New name', 'New description'); await waitAndUpdate(wrapper); - expect(onEdit).toBeCalledWith('foo', 'New name', 'New description'); + expect(onEdit).toBeCalledWith('New name', 'New description'); expect(onClose).toBeCalled(); }); -function shallowRender(props: Partial<EditForm<Application>['props']> = {}) { - return shallow<EditForm<Application>>( +function shallowRender(props: Partial<EditForm['props']> = {}) { + return shallow<EditForm>( <EditForm header="Edit" - onChange={jest.fn()} onClose={jest.fn()} onEdit={jest.fn()} application={mockApplication()} diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationConsoleApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationConsoleApp-test.tsx.snap new file mode 100644 index 00000000000..e6c2b0fe855 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationConsoleApp-test.tsx.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Should render correctly: Empty app 1`] = `""`; + +exports[`Should render correctly: With app 1`] = ` +<ApplicationConsoleAppRenderer + application={ + Object { + "branches": Array [ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-6.7", + }, + ], + "key": "foo", + "name": "Foo", + "projects": Array [ + Object { + "branch": "master", + "isMain": true, + "key": "bar", + "name": "Bar", + }, + ], + "visibility": "private", + } + } + loading={false} + onAddProject={[Function]} + onEdit={[Function]} + onRefresh={[Function]} + onRemoveProject={[Function]} + onUpdateBranches={[Function]} +/> +`; diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationConsoleRender-test.tsx.snap b/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationConsoleRender-test.tsx.snap new file mode 100644 index 00000000000..a648f4c4758 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationConsoleRender-test.tsx.snap @@ -0,0 +1,281 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should handle editing: edit form 1`] = ` +<EditForm + application={ + Object { + "branches": Array [ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-6.7", + }, + ], + "key": "foo", + "name": "Foo", + "projects": Array [ + Object { + "branch": "master", + "isMain": true, + "key": "bar", + "name": "Bar", + }, + ], + "visibility": "private", + } + } + header="portfolios.edit_application" + onClose={[Function]} + onEdit={[MockFunction]} +/> +`; + +exports[`should render correctly: can delete and recompute 1`] = ` +<div + className="page page-limited" +> + <div + className="boxed-group" + id="view-details" + > + <div + className="boxed-group-actions" + > + <Button + className="little-spacer-right" + id="view-details-edit" + onClick={[Function]} + > + edit + </Button> + <Button + className="little-spacer-right" + onClick={[MockFunction]} + > + application_console.recompute + </Button> + <ConfirmButton + confirmButtonText="delete" + isDestructive={true} + modalBody="application_console.do_you_want_to_delete.Foo" + modalHeader="application_console.delete_application" + onConfirm={[MockFunction]} + > + <Component /> + </ConfirmButton> + </div> + <header + className="boxed-group-header" + id="view-details-header" + > + <h2 + className="text-limited" + title="Foo" + > + Foo + </h2> + </header> + <div + className="boxed-group-inner" + id="view-details-content" + > + <div + className="big-spacer-bottom" + > + <div + className="little-spacer-bottom" + > + Foo bar + </div> + <div + className="subtitle" + > + key + : + foo + </div> + </div> + <ApplicationProjects + application={ + Object { + "branches": Array [ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-6.7", + }, + ], + "description": "Foo bar", + "key": "foo", + "name": "Foo", + "projects": Array [ + Object { + "branch": "master", + "isMain": true, + "key": "bar", + "name": "Bar", + }, + ], + "visibility": "private", + } + } + onAddProject={[MockFunction]} + onRemoveProject={[MockFunction]} + /> + <ApplicationBranches + application={ + Object { + "branches": Array [ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-6.7", + }, + ], + "description": "Foo bar", + "key": "foo", + "name": "Foo", + "projects": Array [ + Object { + "branch": "master", + "isMain": true, + "key": "bar", + "name": "Bar", + }, + ], + "visibility": "private", + } + } + onUpdateBranches={[MockFunction]} + /> + </div> + </div> +</div> +`; + +exports[`should render correctly: default 1`] = ` +<div + className="page page-limited" +> + <div + className="boxed-group" + id="view-details" + > + <div + className="boxed-group-actions" + > + <Button + className="little-spacer-right" + id="view-details-edit" + onClick={[Function]} + > + edit + </Button> + <Button + className="little-spacer-right" + onClick={[MockFunction]} + > + application_console.recompute + </Button> + <ConfirmButton + confirmButtonText="delete" + isDestructive={true} + modalBody="application_console.do_you_want_to_delete.Foo" + modalHeader="application_console.delete_application" + onConfirm={[MockFunction]} + > + <Component /> + </ConfirmButton> + </div> + <header + className="boxed-group-header" + id="view-details-header" + > + <h2 + className="text-limited" + title="Foo" + > + Foo + </h2> + </header> + <div + className="boxed-group-inner" + id="view-details-content" + > + <div + className="big-spacer-bottom" + > + <div + className="subtitle" + > + key + : + foo + </div> + </div> + <ApplicationProjects + application={ + Object { + "branches": Array [ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-6.7", + }, + ], + "key": "foo", + "name": "Foo", + "projects": Array [ + Object { + "branch": "master", + "isMain": true, + "key": "bar", + "name": "Bar", + }, + ], + "visibility": "private", + } + } + onAddProject={[MockFunction]} + onRemoveProject={[MockFunction]} + /> + <ApplicationBranches + application={ + Object { + "branches": Array [ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-6.7", + }, + ], + "key": "foo", + "name": "Foo", + "projects": Array [ + Object { + "branch": "master", + "isMain": true, + "key": "bar", + "name": "Bar", + }, + ], + "visibility": "private", + } + } + onUpdateBranches={[MockFunction]} + /> + </div> + </div> +</div> +`; + +exports[`should render correctly: is loading 1`] = ` +<i + className="spinner spacer" +/> +`; diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationDetails-test.tsx.snap b/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationDetails-test.tsx.snap deleted file mode 100644 index c9dd363ca4c..00000000000 --- a/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationDetails-test.tsx.snap +++ /dev/null @@ -1,284 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should handle editing: edit form 1`] = ` -<EditForm - application={ - Object { - "branches": Array [ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-6.7", - }, - ], - "key": "foo", - "name": "Foo", - "projects": Array [ - Object { - "branch": "master", - "isMain": true, - "key": "bar", - "name": "Bar", - }, - ], - "visibility": "private", - } - } - header="portfolios.edit_application" - onChange={[MockFunction]} - onClose={[Function]} - onEdit={[MockFunction]} -/> -`; - -exports[`should render correctly: can delete and recompute 1`] = ` -<div - className="boxed-group portfolios-console-details" - id="view-details" -> - <div - className="boxed-group-actions" - > - <Button - className="little-spacer-right" - id="view-details-edit" - onClick={[Function]} - > - edit - </Button> - <Button - className="little-spacer-right" - disabled={false} - onClick={[Function]} - > - application_console.recompute - </Button> - <ConfirmButton - confirmButtonText="delete" - isDestructive={true} - modalBody="application_console.do_you_want_to_delete.Foo" - modalHeader="application_console.delete_application" - onConfirm={[Function]} - > - <Component /> - </ConfirmButton> - </div> - <header - className="boxed-group-header" - id="view-details-header" - > - <h2 - className="text-limited" - title="Foo" - > - Foo - </h2> - </header> - <div - className="boxed-group-inner" - id="view-details-content" - > - <div - className="big-spacer-bottom" - > - <div - className="little-spacer-bottom" - > - Foo bar - </div> - <div - className="subtitle" - > - key - : - foo - <Link - className="spacer-left" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, - } - } - > - application_console.open_dashbard - </Link> - </div> - </div> - <ApplicationDetailsProjects - application={ - Object { - "branches": Array [ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-6.7", - }, - ], - "description": "Foo bar", - "key": "foo", - "name": "Foo", - "projects": Array [ - Object { - "branch": "master", - "isMain": true, - "key": "bar", - "name": "Bar", - }, - ], - "visibility": "private", - } - } - onAddProject={[MockFunction]} - onRemoveProject={[MockFunction]} - /> - <ApplicationBranches - application={ - Object { - "branches": Array [ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-6.7", - }, - ], - "description": "Foo bar", - "key": "foo", - "name": "Foo", - "projects": Array [ - Object { - "branch": "master", - "isMain": true, - "key": "bar", - "name": "Bar", - }, - ], - "visibility": "private", - } - } - onUpdateBranches={[MockFunction]} - /> - </div> -</div> -`; - -exports[`should render correctly: default 1`] = ` -<div - className="boxed-group portfolios-console-details" - id="view-details" -> - <div - className="boxed-group-actions" - > - <Button - className="little-spacer-right" - id="view-details-edit" - onClick={[Function]} - > - edit - </Button> - </div> - <header - className="boxed-group-header" - id="view-details-header" - > - <h2 - className="text-limited" - title="Foo" - > - Foo - </h2> - </header> - <div - className="boxed-group-inner" - id="view-details-content" - > - <div - className="big-spacer-bottom" - > - <div - className="subtitle" - > - key - : - foo - <Link - className="spacer-left" - onlyActiveOnIndex={false} - style={Object {}} - to={ - Object { - "pathname": "/dashboard", - "query": Object { - "id": "foo", - }, - } - } - > - application_console.open_dashbard - </Link> - </div> - </div> - <ApplicationDetailsProjects - application={ - Object { - "branches": Array [ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-6.7", - }, - ], - "key": "foo", - "name": "Foo", - "projects": Array [ - Object { - "branch": "master", - "isMain": true, - "key": "bar", - "name": "Bar", - }, - ], - "visibility": "private", - } - } - onAddProject={[MockFunction]} - onRemoveProject={[MockFunction]} - /> - <ApplicationBranches - application={ - Object { - "branches": Array [ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-6.7", - }, - ], - "key": "foo", - "name": "Foo", - "projects": Array [ - Object { - "branch": "master", - "isMain": true, - "key": "bar", - "name": "Bar", - }, - ], - "visibility": "private", - } - } - onUpdateBranches={[MockFunction]} - /> - </div> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationDetailsProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationProjects-test.tsx.snap index caedd4c5d95..caedd4c5d95 100644 --- a/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationDetailsProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationProjects-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationView-test.tsx.snap b/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationView-test.tsx.snap deleted file mode 100644 index 84227b75517..00000000000 --- a/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ApplicationView-test.tsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Should render correctly 1`] = ` -<i - className="spinner spacer" -/> -`; diff --git a/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ConsoleApplicationApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ConsoleApplicationApp-test.tsx.snap deleted file mode 100644 index 323f4996dcf..00000000000 --- a/server/sonar-web/src/main/js/apps/application-console/__tests__/__snapshots__/ConsoleApplicationApp-test.tsx.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Should render correctly 1`] = ` -<div - className="page page-limited" -> - <div - className="navigator-content" - > - <ApplicationView - applicationKey="my-project" - canRecompute={true} - onDelete={[Function]} - onEdit={[Function]} - pathname="/path" - router={ - Object { - "createHref": [MockFunction], - "createPath": [MockFunction], - "go": [MockFunction], - "goBack": [MockFunction], - "goForward": [MockFunction], - "isActive": [MockFunction], - "push": [MockFunction], - "replace": [MockFunction], - "setRouteLeaveHook": [MockFunction], - } - } - /> - </div> -</div> -`; diff --git a/server/sonar-web/src/main/js/apps/application-console/routes.ts b/server/sonar-web/src/main/js/apps/application-console/routes.ts index 6ddddead00d..64479ab5a61 100644 --- a/server/sonar-web/src/main/js/apps/application-console/routes.ts +++ b/server/sonar-web/src/main/js/apps/application-console/routes.ts @@ -21,7 +21,7 @@ import { lazyLoadComponent } from 'sonar-ui-common/components/lazyLoadComponent' const routes = [ { - indexRoute: { component: lazyLoadComponent(() => import('./ConsoleApplicationApp')) } + indexRoute: { component: lazyLoadComponent(() => import('./ApplicationConsoleApp')) } } ]; |