From 3fc77718cf0e14555367dd8f3492205db3f5ce8c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gr=C3=A9goire=20Aubert?= Date: Tue, 18 Dec 2018 11:53:09 +0100 Subject: [PATCH] SONARCLOUD-264 Use same empty organization space in projects page --- .../organization/AutoOrganizationCreate.tsx | 4 +- .../organization/CreateOrganization.tsx | 7 +- .../__tests__/AutoOrganizationCreate-test.tsx | 2 +- .../__tests__/CreateOrganization-test.tsx | 11 +- ...nJustCreated.css => OrganizationEmpty.css} | 4 +- ...nJustCreated.tsx => OrganizationEmpty.tsx} | 8 +- .../components/OrganizationPage.tsx | 21 +- ...ed-test.tsx => OrganizationEmpty-test.tsx} | 8 +- ...x.snap => OrganizationEmpty-test.tsx.snap} | 2 +- .../src/main/js/apps/organizations/routes.ts | 7 +- .../apps/projects/components/AllProjects.tsx | 115 +++++++--- .../components/__tests__/AllProjects-test.tsx | 36 +++- .../__snapshots__/AllProjects-test.tsx.snap | 200 ++++++++++++++++++ 13 files changed, 335 insertions(+), 90 deletions(-) rename server/sonar-web/src/main/js/apps/organizations/components/{OrganizationJustCreated.css => OrganizationEmpty.css} (94%) rename server/sonar-web/src/main/js/apps/organizations/components/{OrganizationJustCreated.tsx => OrganizationEmpty.tsx} (92%) rename server/sonar-web/src/main/js/apps/organizations/components/__tests__/{OrganizationJustCreated-test.tsx => OrganizationEmpty-test.tsx} (92%) rename server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/{OrganizationJustCreated-test.tsx.snap => OrganizationEmpty-test.tsx.snap} (95%) diff --git a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx index 11f6fe3fbc5..7a805ec17ba 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/AutoOrganizationCreate.tsx @@ -48,7 +48,7 @@ interface Props { handleOrgDetailsFinish: (organization: T.Organization) => Promise; handleOrgDetailsStepOpen: () => void; onDone: () => void; - onOrgCreated: (organization: string, justCreated?: boolean) => void; + onOrgCreated: (organization: string) => void; onUpgradeFail: () => void; organization?: T.Organization; step: Step; @@ -72,7 +72,7 @@ export default class AutoOrganizationCreate extends React.PureComponent this.props.onOrgCreated(organization, false)); + }).then(() => this.props.onOrgCreated(organization)); }; handleCreateOrganization = () => { diff --git a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx index 7ccc09db294..e368675861c 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/CreateOrganization.tsx @@ -205,7 +205,7 @@ export class CreateOrganization extends React.PureComponent { + handleOrgCreated = (organization: string) => { this.props.skipOnboarding(); if (this.isStoredTimestampValid(ORGANIZATION_IMPORT_REDIRECT_TO_PROJECT_TIMESTAMP)) { this.props.router.push({ @@ -213,10 +213,7 @@ export class CreateOrganization extends React.PureComponent { organization: 'foo' }); await waitAndUpdate(wrapper); - expect(onOrgCreated).toHaveBeenCalledWith('foo', false); + expect(onOrgCreated).toHaveBeenCalledWith('foo'); }); function shallowRender(props: Partial = {}) { diff --git a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx index 4da19584a2e..ddd5ebcd85f 100644 --- a/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx +++ b/server/sonar-web/src/main/js/apps/create/organization/__tests__/CreateOrganization-test.tsx @@ -226,16 +226,7 @@ it('should redirect to organization page after creation', async () => { wrapper.setState({ organization: boundOrganization }); wrapper.instance().handleOrgCreated('foo'); - expect(push).toHaveBeenCalledWith({ - pathname: '/organizations/foo', - state: { justCreated: true } - }); - - wrapper.instance().handleOrgCreated('foo', false); - expect(push).toHaveBeenCalledWith({ - pathname: '/organizations/foo', - state: { justCreated: false } - }); + expect(push).toHaveBeenCalledWith({ pathname: '/organizations/foo' }); }); it('should redirect to projects creation page after creation', async () => { diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.css similarity index 94% rename from server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css rename to server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.css index 2779ccbce5c..55f794f1fc5 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.css +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.css @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -.organization-just-created { - margin: 120px auto 0; +.organization-empty { + margin: 100px auto 0; width: 800px; } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx similarity index 92% rename from server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx rename to server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx index 89f1d217de5..d47f5675f1c 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationJustCreated.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationEmpty.tsx @@ -25,7 +25,7 @@ import { translate } from '../../../helpers/l10n'; import { OnboardingContextShape } from '../../../app/components/OnboardingContext'; import { withRouter, Router } from '../../../components/hoc/withRouter'; import '../../tutorials/styles.css'; -import './OrganizationJustCreated.css'; +import './OrganizationEmpty.css'; interface Props { openProjectOnboarding: OnboardingContextShape; @@ -33,7 +33,7 @@ interface Props { router: Pick; } -export class OrganizationJustCreated extends React.PureComponent { +export class OrganizationEmpty extends React.PureComponent { handleNewProjectClick = () => { this.props.openProjectOnboarding(this.props.organization); }; @@ -45,7 +45,7 @@ export class OrganizationJustCreated extends React.PureComponent { render() { return ( -
+

{translate('onboarding.create_organization.ready')}

); } diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationJustCreated-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEmpty-test.tsx similarity index 92% rename from server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationJustCreated-test.tsx rename to server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEmpty-test.tsx index 0bcc3631ed5..257fa9e9e27 100644 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationJustCreated-test.tsx +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationEmpty-test.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { shallow } from 'enzyme'; -import { OrganizationJustCreated } from '../OrganizationJustCreated'; +import { OrganizationEmpty } from '../OrganizationEmpty'; import { click } from '../../../../helpers/testUtils'; const organization: T.Organization = { key: 'foo', name: 'Foo' }; @@ -27,7 +27,7 @@ const organization: T.Organization = { key: 'foo', name: 'Foo' }; it('should render', () => { expect( shallow( - { it('should create new project', () => { const openProjectOnboarding = jest.fn(); const wrapper = shallow( - { it('should add members', () => { const router = { push: jest.fn() }; const wrapper = shallow( -

; organization: T.Organization | undefined; - organizationsEnabled?: boolean; router: Pick; storageOptionsSuffix?: string; } interface State { facets?: Facets; + initialLoading: boolean; loading: boolean; pageIndex?: number; projects?: Project[]; @@ -70,7 +72,7 @@ export class AllProjects extends React.PureComponent { constructor(props: Props) { super(props); - this.state = { loading: true, query: {} }; + this.state = { initialLoading: true, loading: true, query: {} }; } componentDidMount() { @@ -81,38 +83,37 @@ export class AllProjects extends React.PureComponent { return; } this.handleQueryChange(true); - addSideBarFooterClass(); + this.updateFooterClass(); } componentDidUpdate(prevProps: Props) { if (prevProps.location.query !== this.props.location.query) { this.handleQueryChange(false); } + + if ( + prevProps.organization && + this.props.organization && + prevProps.organization.key !== this.props.organization.key + ) { + this.setState({ initialLoading: true }); + } + + this.updateFooterClass(); } componentWillUnmount() { this.mounted = false; - removeSideBarFooterClass(); + removeSideBarClass(); } - getView = () => this.state.query.view || 'overall'; - - getVisualization = () => this.state.query.visualization || 'risk'; - - getSort = () => this.state.query.sort || 'name'; - - stopLoading = () => { - if (this.mounted) { - this.setState({ loading: false }); - } - }; - fetchProjects = (query: any) => { this.setState({ loading: true, query }); fetchProjects(query, this.props.isFavorite, this.props.organization).then(response => { if (this.mounted) { this.setState({ facets: response.facets, + initialLoading: false, loading: false, pageIndex: 1, projects: response.projects, @@ -141,6 +142,8 @@ export class AllProjects extends React.PureComponent { } }; + getSort = () => this.state.query.sort || 'name'; + getStorageOptions = () => { const { storageOptionsSuffix } = this.props; const options: { @@ -160,6 +163,14 @@ export class AllProjects extends React.PureComponent { return options; }; + getView = () => this.state.query.view || 'overall'; + + getVisualization = () => this.state.query.visualization || 'risk'; + + handleClearAll = () => { + this.props.router.push({ pathname: this.props.location.pathname }); + }; + handlePerspectiveChange = ({ view, visualization }: { view: string; visualization?: string }) => { const { storageOptionsSuffix } = this.props; const query: { @@ -188,12 +199,6 @@ export class AllProjects extends React.PureComponent { save(PROJECTS_VISUALIZATION, visualization, storageOptionsSuffix); }; - handleSortChange = (sort: string, desc: boolean) => { - const asString = (desc ? '-' : '') + sort; - this.updateLocationQuery({ sort: asString }); - save(PROJECTS_SORT, asString, this.props.storageOptionsSuffix); - }; - handleQueryChange(initialMount: boolean) { const query = parseUrlQuery(this.props.location.query); const savedOptions = this.getStorageOptions(); @@ -207,13 +212,34 @@ export class AllProjects extends React.PureComponent { } } + handleSortChange = (sort: string, desc: boolean) => { + const asString = (desc ? '-' : '') + sort; + this.updateLocationQuery({ sort: asString }); + save(PROJECTS_SORT, asString, this.props.storageOptionsSuffix); + }; + + stopLoading = () => { + if (this.mounted) { + this.setState({ initialLoading: false, loading: false }); + } + }; + updateLocationQuery = (newQuery: RawQuery) => { const query = omitBy({ ...this.props.location.query, ...newQuery }, x => !x); this.props.router.push({ pathname: this.props.location.pathname, query }); }; - handleClearAll = () => { - this.props.router.push({ pathname: this.props.location.pathname }); + updateFooterClass = () => { + const { organization } = this.props; + const { initialLoading, projects } = this.state; + const isOrganizationContext = isSonarCloud() && organization; + const isEmpty = projects && projects.length === 0; + + if (isOrganizationContext && (initialLoading || isEmpty)) { + removeSideBarClass(); + } else { + addSideBarClass(); + } }; renderSide = () => ( @@ -270,7 +296,7 @@ export class AllProjects extends React.PureComponent {
{this.state.projects && ( { }; render() { + const { organization } = this.props; + const { projects } = this.state; + const isOrganizationContext = isSonarCloud() && organization; + const initialLoading = isOrganizationContext && this.state.initialLoading; + const organizationEmpty = isOrganizationContext && projects && projects.length === 0; + return (
- {this.renderSide()} + {initialLoading ? ( +
+ +
+ ) : ( + <> + {!organizationEmpty && this.renderSide()} -
- {this.renderHeader()} - {this.renderMain()} -
+
+ {organizationEmpty && organization ? ( + + {openProjectOnboarding => ( + + )} + + ) : ( + <> + {this.renderHeader()} + {this.renderMain()} + + )} +
+ + )}
); } diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx index 12e7e5d81e5..d1ac2e535f0 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/AllProjects-test.tsx @@ -22,6 +22,8 @@ import * as React from 'react'; import { shallow } from 'enzyme'; import { AllProjects } from '../AllProjects'; import { get, save } from '../../../../helpers/storage'; +import { isSonarCloud } from '../../../../helpers/system'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; jest.mock('../ProjectsList', () => ({ // eslint-disable-next-line @@ -55,11 +57,14 @@ jest.mock('../../../../helpers/storage', () => ({ save: jest.fn() })); -const fetchProjects = require('../../utils').fetchProjects as jest.Mock; +jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn() })); + +const fetchProjects = require('../../utils').fetchProjects as jest.Mock; beforeEach(() => { - (get as jest.Mock).mockImplementation(() => null); - (save as jest.Mock).mockClear(); + (get as jest.Mock).mockImplementation(() => null); + (save as jest.Mock).mockClear(); + (isSonarCloud as jest.Mock).mockReturnValue(false); fetchProjects.mockClear(); }); @@ -100,7 +105,7 @@ it('fetches projects', () => { }); it('redirects to the saved search', () => { - (get as jest.Mock).mockImplementation( + (get as jest.Mock).mockImplementation( (key: string) => (key === 'sonarqube.projects.view' ? 'leak' : null) ); const replace = jest.fn(); @@ -161,6 +166,28 @@ it('changes perspective to risk visualization', () => { expect(save).toHaveBeenCalledWith('sonarqube.projects.visualization', 'risk', undefined); }); +it('renders correctly empty organization', async () => { + (isSonarCloud as jest.Mock).mockReturnValue(true); + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + await waitAndUpdate(wrapper); + expect(wrapper).toMatchSnapshot(); + wrapper.setState({ + loading: false, + projects: [{ key: 'foo', measures: {}, name: 'Foo' }], + total: 0 + }); + expect(wrapper).toMatchSnapshot(); +}); + function shallowRender( props: Partial = {}, push = jest.fn(), @@ -172,7 +199,6 @@ function shallowRender( isFavorite={false} location={{ pathname: '/projects', query: {} }} organization={undefined} - organizationsEnabled={false} router={{ push, replace }} {...props} /> diff --git a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap index e0873b4d900..1c09c537190 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/AllProjects-test.tsx.snap @@ -231,3 +231,203 @@ exports[`renders 2`] = `

`; + +exports[`renders correctly empty organization 1`] = ` +
+ + +
+ +
+
+`; + +exports[`renders correctly empty organization 2`] = ` +
+ + +
+ + + +
+
+`; + +exports[`renders correctly empty organization 3`] = ` +
+ + + + + +
+
+
+
+ +
+
+
+ +
+ + +
+
+
+
+`; -- 2.39.5