Browse Source

feat(visibility): no projects message (#11657)

* add io asset

* update add project graphic

* emptyState => noProjectMessage

* one kinda working that isn't that greatakindaworkinfg

* add it to all of the pertinent top level tab views

* use a parent component to reduce duplicate project checks

* better comment

* fix ugly rebase in organizationEvents/index

* revert to hair on fire person because art director dad says so

* couple small fixes from review

* fix stuff from rebase

* fix bug with user feedback page

* forgotten snapshot test

* add a project to user feedback render test

* add user to team in releases  acceptance tests

* NoProjectsMessage does not take any projects
Chris Clark 6 years ago
parent
commit
33d1ced4d1

+ 29 - 7
src/sentry/static/sentry/app/views/organizationProjectsDashboard/emptyState.jsx → src/sentry/static/sentry/app/components/noProjectMessage.jsx

@@ -1,31 +1,44 @@
 import React from 'react';
 import {Flex} from 'grid-emotion';
 import styled from 'react-emotion';
+import PropTypes from 'prop-types';
 
 import {t} from 'app/locale';
 import Button from 'app/components/button';
+import PageHeading from 'app/components/pageHeading';
 import Tooltip from 'app/components/tooltip';
 import SentryTypes from 'app/sentryTypes';
-import img from '../../../images/dashboard/hair-on-fire.svg';
+import space from 'app/styles/space';
+/* TODO: replace with I/O when finished */
+import img from '../../images/dashboard/hair-on-fire.svg';
 
-export default class EmptyState extends React.Component {
+export default class NoProjectMessage extends React.Component {
   static propTypes = {
+    /* if the user has access to any projects, we show whatever
+    children are included. Otherwise we show the message */
+    children: PropTypes.node,
     organization: SentryTypes.Organization,
+    className: PropTypes.string,
   };
 
   render() {
-    const {organization} = this.props;
+    const {children, organization, className} = this.props;
     const orgId = organization.slug;
     const canCreateProject = organization.access.includes('project:write');
     const canJoinTeam = organization.access.includes('team:read');
+    const hasProjects = organization.projects.some(p => p.isMember && p.hasAccess);
 
-    return (
-      <Flex flex="1" align="center" justify="center">
+    return hasProjects ? (
+      children
+    ) : (
+      <Flex flex="1" align="center" justify="center" className={className}>
         <Wrapper>
           <img src={img} height={350} alt="Nothing to see" />
           <Content direction="column" justify="center">
-            <h2>{t('Remain calm.')}</h2>
-            <p>{t("Sentry's got you covered. To get started:")}</p>
+            <StyledPageHeading>{t('Remain Calm')}</StyledPageHeading>
+            <HelpMessage>
+              {t('You need at least one project to use this view')}
+            </HelpMessage>
             <Flex align="center">
               <CallToAction>
                 <Tooltip
@@ -63,6 +76,11 @@ export default class EmptyState extends React.Component {
   }
 }
 
+const StyledPageHeading = styled(PageHeading)`
+  font-size: 28px;
+  margin-bottom: ${space(1.5)};
+`;
+
 const CallToAction = styled('div')`
   margin-right: 8px;
   &:last-child {
@@ -70,6 +88,10 @@ const CallToAction = styled('div')`
   }
 `;
 
+const HelpMessage = styled('div')`
+  margin-bottom: ${space(2)};
+`;
+
 const Wrapper = styled(Flex)`
   height: 350px;
 `;

+ 17 - 15
src/sentry/static/sentry/app/views/organizationEvents/index.jsx

@@ -8,6 +8,7 @@ import {t} from 'app/locale';
 import BetaTag from 'app/components/betaTag';
 import Feature from 'app/components/acl/feature';
 import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
+import NoProjectMessage from 'app/components/noProjectMessage';
 import SentryTypes from 'app/sentryTypes';
 import PageHeading from 'app/components/pageHeading';
 import withGlobalSelection from 'app/utils/withGlobalSelection';
@@ -49,21 +50,22 @@ class OrganizationEventsContainer extends React.Component {
           resetParamsOnChange={['cursor']}
         />
         <PageContent>
-          <Body>
-            <PageHeader>
-              <HeaderTitle>
-                {t('Events')} <BetaTag />
-              </HeaderTitle>
-              <StyledSearchBar
-                organization={organization}
-                query={(location.query && location.query.query) || ''}
-                placeholder={t('Search for events, users, tags, and everything else.')}
-                onSearch={this.handleSearch}
-              />
-            </PageHeader>
-
-            {children}
-          </Body>
+          <NoProjectMessage organization={organization}>
+            <Body>
+              <PageHeader>
+                <HeaderTitle>
+                  {t('Events')} <BetaTag />
+                </HeaderTitle>
+                <StyledSearchBar
+                  organization={organization}
+                  query={(location.query && location.query.query) || ''}
+                  placeholder={t('Search for events, users, tags, and everything else.')}
+                  onSearch={this.handleSearch}
+                />
+              </PageHeader>
+              {children}
+            </Body>
+          </NoProjectMessage>
         </PageContent>
       </Feature>
     );

+ 2 - 4
src/sentry/static/sentry/app/views/organizationProjectsDashboard/index.jsx

@@ -8,6 +8,7 @@ import styled from 'react-emotion';
 
 import SentryTypes from 'app/sentryTypes';
 import IdBadge from 'app/components/idBadge';
+import NoProjectMessage from 'app/components/noProjectMessage';
 import OrganizationState from 'app/mixins/organizationState';
 import ProjectsStatsStore from 'app/stores/projectsStatsStore';
 import getProjectsByTeams from 'app/utils/getProjectsByTeams';
@@ -18,7 +19,6 @@ import {t} from 'app/locale';
 
 import ProjectNav from './projectNav';
 import TeamSection from './teamSection';
-import EmptyState from './emptyState';
 import Resources from './resources';
 
 class Dashboard extends React.Component {
@@ -90,9 +90,7 @@ class Dashboard extends React.Component {
           );
         })}
         {teamSlugs.length === 0 &&
-          favorites.length === 0 && (
-            <EmptyState projects={projects} teams={teams} organization={organization} />
-          )}
+          favorites.length === 0 && <NoProjectMessage organization={organization} />}
       </React.Fragment>
     );
   }

+ 4 - 1
src/sentry/static/sentry/app/views/organizationStream/container.jsx

@@ -5,6 +5,7 @@ import DocumentTitle from 'react-document-title';
 import {PageContent} from 'app/styles/organization';
 import Feature from 'app/components/acl/feature';
 import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
+import NoProjectMessage from 'app/components/noProjectMessage';
 import SentryTypes from 'app/sentryTypes';
 import withOrganization from 'app/utils/withOrganization';
 
@@ -25,7 +26,9 @@ class OrganizationStreamContainer extends React.Component {
         <Feature features={['sentry10']} renderDisabled>
           <GlobalSelectionHeader organization={organization} />
 
-          <PageContent>{children}</PageContent>
+          <PageContent>
+            <NoProjectMessage organization={organization}>{children}</NoProjectMessage>
+          </PageContent>
         </Feature>
       </DocumentTitle>
     );

+ 20 - 17
src/sentry/static/sentry/app/views/releases/list/organizationReleases/index.jsx

@@ -11,6 +11,7 @@ import Alert from 'app/components/alert';
 import EmptyStateWarning from 'app/components/emptyStateWarning';
 import Feature from 'app/components/acl/feature';
 import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
+import NoProjectMessage from 'app/components/noProjectMessage';
 import AsyncView from 'app/views/asyncView';
 import withOrganization from 'app/utils/withOrganization';
 import withGlobalSelection from 'app/utils/withGlobalSelection';
@@ -157,28 +158,30 @@ class OrganizationReleases extends AsyncView {
   }
 
   renderBody() {
-    const {location} = this.props;
+    const {location, organization} = this.props;
 
     return (
       <PageContent>
-        <PageHeader>
-          <PageHeading>{t('Releases')}</PageHeading>
+        <NoProjectMessage organization={organization}>
+          <PageHeader>
+            <PageHeading>{t('Releases')}</PageHeading>
+            <div>
+              <SearchBar
+                defaultQuery=""
+                placeholder={t('Search for a release')}
+                query={location.query.query}
+                onSearch={this.onSearch}
+              />
+            </div>
+          </PageHeader>
           <div>
-            <SearchBar
-              defaultQuery=""
-              placeholder={t('Search for a release')}
-              query={location.query.query}
-              onSearch={this.onSearch}
-            />
+            <Panel>
+              <ReleaseListHeader />
+              <PanelBody>{this.renderStreamBody()}</PanelBody>
+            </Panel>
+            <Pagination pageLinks={this.state.releaseListPageLinks} />
           </div>
-        </PageHeader>
-        <div>
-          <Panel>
-            <ReleaseListHeader />
-            <PanelBody>{this.renderStreamBody()}</PanelBody>
-          </Panel>
-          <Pagination pageLinks={this.state.releaseListPageLinks} />
-        </div>
+        </NoProjectMessage>
       </PageContent>
     );
   }

+ 10 - 7
src/sentry/static/sentry/app/views/userFeedback/organizationUserFeedback.jsx

@@ -10,6 +10,7 @@ import CompactIssue from 'app/components/compactIssue';
 import EventUserFeedback from 'app/components/events/userFeedback';
 import LoadingIndicator from 'app/components/loadingIndicator';
 import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader';
+import NoProjectMessage from 'app/components/noProjectMessage';
 import AsyncView from 'app/views/asyncView';
 import {PageContent} from 'app/styles/organization';
 
@@ -110,13 +111,15 @@ class OrganizationUserFeedback extends AsyncView {
       >
         <GlobalSelectionHeader organization={organization} />
         <PageContent>
-          <UserFeedbackContainer
-            pageLinks={reportListPageLinks}
-            status={status}
-            location={location}
-          >
-            {this.renderStreamBody()}
-          </UserFeedbackContainer>
+          <NoProjectMessage organization={organization}>
+            <UserFeedbackContainer
+              pageLinks={reportListPageLinks}
+              status={status}
+              location={location}
+            >
+              {this.renderStreamBody()}
+            </UserFeedbackContainer>
+          </NoProjectMessage>
         </PageContent>
       </Feature>
     );

BIN
src/sentry/static/sentry/images/confused-io.png


+ 4 - 1
tests/acceptance/test_organization_releases.py

@@ -12,7 +12,10 @@ class OrganizationReleasesTest(AcceptanceTestCase):
         self.org = self.create_organization(
             owner=self.user, name='Rowdy Tiger')
         self.team = self.create_team(
-            organization=self.org, name='Mariachi Band')
+            organization=self.org,
+            name='Mariachi Band',
+            members=[self.user],
+        )
         self.project = self.create_project(
             organization=self.org,
             teams=[self.team],

+ 3 - 3
tests/js/spec/views/organizationProjectsDashboard/index.spec.jsx

@@ -62,7 +62,7 @@ describe('OrganizationDashboard', function() {
         />,
         TestStubs.routerContext()
       );
-      const emptyState = wrapper.find('EmptyState');
+      const emptyState = wrapper.find('NoProjectMessage');
       expect(emptyState).toHaveLength(1);
     });
 
@@ -111,7 +111,7 @@ describe('OrganizationDashboard', function() {
         />,
         TestStubs.routerContext()
       );
-      const emptyState = wrapper.find('EmptyState');
+      const emptyState = wrapper.find('NoProjectMessage');
       const favorites = wrapper.find('TeamSection[data-test-id="favorites"]');
       const teamSection = wrapper.find('TeamSection');
       expect(emptyState).toHaveLength(0);
@@ -248,7 +248,7 @@ describe('OrganizationDashboard', function() {
       expect(wrapper.find('TeamSection')).toHaveLength(1);
       const projectCards = wrapper.find('ProjectCard');
       expect(projectCards).toHaveLength(1);
-      const emptyState = wrapper.find('EmptyState');
+      const emptyState = wrapper.find('NoProjectMessage');
       expect(emptyState).toHaveLength(0);
     });
   });

+ 6 - 6
tests/js/spec/views/organizationProjectsDashboard/emptyState.spec.jsx → tests/js/spec/views/organizationProjectsDashboard/noProjectMessage.spec.jsx

@@ -1,13 +1,13 @@
 import {shallow} from 'enzyme';
 import React from 'react';
 
-import EmptyState from 'app/views/organizationProjectsDashboard/emptyState';
+import NoProjectMessage from 'app/components/noProjectMessage';
 
-describe('EmptyState', function() {
+describe('NoProjectMessage', function() {
   const org = TestStubs.Organization();
   it('shows "Create Project" button when there are no projects', function() {
     const wrapper = shallow(
-      <EmptyState organization={org} projects={[]} />,
+      <NoProjectMessage organization={org} />,
       TestStubs.routerContext()
     );
     expect(
@@ -17,7 +17,7 @@ describe('EmptyState', function() {
 
   it('"Create Project" is disabled when no access to `project:write`', function() {
     const wrapper = shallow(
-      <EmptyState organization={TestStubs.Organization({access: []})} projects={[]} />,
+      <NoProjectMessage organization={TestStubs.Organization({access: []})} />,
       TestStubs.routerContext()
     );
     expect(
@@ -27,7 +27,7 @@ describe('EmptyState', function() {
 
   it('has "Join a Team" button', function() {
     const wrapper = shallow(
-      <EmptyState organization={org} projects={[]} />,
+      <NoProjectMessage organization={org} />,
       TestStubs.routerContext()
     );
     expect(wrapper.find('Button[to="/settings/org-slug/teams/"]')).toHaveLength(1);
@@ -35,7 +35,7 @@ describe('EmptyState', function() {
 
   it('has a disabled "Join a Team" button if no access to `team:read`', function() {
     const wrapper = shallow(
-      <EmptyState organization={TestStubs.Organization({access: []})} projects={[]} />,
+      <NoProjectMessage organization={TestStubs.Organization({access: []})} />,
       TestStubs.routerContext()
     );
     expect(wrapper.find('Button[to="/settings/org-slug/teams/"]').prop('disabled')).toBe(

Some files were not shown because too many files changed in this diff