Browse Source

ref(ui): Change from withTeams to <Teams> for team key transactions (#29223)

Due to how we fetch teams on the frontend we only load up to 100 into the TeamStore at app start. Only a handful of orgs have over 100 teams, but it still poses a problem for using certain features such as team key transactions. To fix this, we replace withTeams in favor of the Teams utility which handles fetching the correct teams and propagating changes back to the store to prevent duplicate fetching.
David Wang 3 years ago
parent
commit
89e171a2d6

+ 2 - 4
static/app/utils/discover/teamKeyTransactionField.tsx

@@ -7,10 +7,9 @@ import TeamKeyTransaction, {
 import * as TeamKeyTransactionManager from 'app/components/performance/teamKeyTransactionsManager';
 import Tooltip from 'app/components/tooltip';
 import {IconStar} from 'app/icons';
-import {Organization, Project, Team} from 'app/types';
+import {Organization, Project} from 'app/types';
 import {defined} from 'app/utils';
 import withProjects from 'app/utils/withProjects';
-import withTeams from 'app/utils/withTeams';
 
 class TitleStar extends Component<TitleProps> {
   render() {
@@ -34,7 +33,6 @@ class TitleStar extends Component<TitleProps> {
 }
 
 type BaseProps = {
-  teams: Team[];
   organization: Organization;
   isKeyTransaction: boolean;
 };
@@ -112,4 +110,4 @@ function TeamKeyTransactionFieldWrapper({
   );
 }
 
-export default withTeams(withProjects(TeamKeyTransactionFieldWrapper));
+export default withProjects(TeamKeyTransactionFieldWrapper);

+ 21 - 17
static/app/views/performance/landing/content.tsx

@@ -5,18 +5,18 @@ import {Location} from 'history';
 
 import DropdownControl, {DropdownItem} from 'app/components/dropdownControl';
 import SearchBar from 'app/components/events/searchBar';
+import LoadingIndicator from 'app/components/loadingIndicator';
 import * as TeamKeyTransactionManager from 'app/components/performance/teamKeyTransactionsManager';
 import {MAX_QUERY_LENGTH} from 'app/constants';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
-import {Organization, Project, Team} from 'app/types';
+import {Organization, Project} from 'app/types';
 import {trackAnalyticsEvent} from 'app/utils/analytics';
 import EventView from 'app/utils/discover/eventView';
 import {generateAggregateFields} from 'app/utils/discover/fields';
-import {isActiveSuperuser} from 'app/utils/isActiveSuperuser';
 import {decodeScalar} from 'app/utils/queryString';
+import Teams from 'app/utils/teams';
 import {MutableSearch} from 'app/utils/tokenizeSearch';
-import withTeams from 'app/utils/withTeams';
 
 import Charts from '../charts/index';
 import {
@@ -52,7 +52,6 @@ type Props = {
   eventView: EventView;
   location: Location;
   projects: Project[];
-  teams: Team[];
   setError: (msg: string | undefined) => void;
   handleSearch: (searchQuery: string) => void;
 } & WithRouterProps;
@@ -265,14 +264,11 @@ class LandingContent extends Component<Props, State> {
   };
 
   render() {
-    const {organization, location, eventView, projects, teams, handleSearch} = this.props;
+    const {organization, location, eventView, projects, handleSearch} = this.props;
 
     const currentLandingDisplay = getCurrentLandingDisplay(location, projects, eventView);
     const filterString = getTransactionSearchQuery(location, eventView.query);
 
-    const isSuperuser = isActiveSuperuser();
-    const userTeams = teams.filter(({isMember}) => isMember || isSuperuser);
-
     return (
       <Fragment>
         <SearchContainer>
@@ -308,14 +304,22 @@ class LandingContent extends Component<Props, State> {
             ))}
           </DropdownControl>
         </SearchContainer>
-        <TeamKeyTransactionManager.Provider
-          organization={organization}
-          teams={userTeams}
-          selectedTeams={['myteams']}
-          selectedProjects={eventView.project.map(String)}
-        >
-          {this.renderSelectedDisplay(currentLandingDisplay.field)}
-        </TeamKeyTransactionManager.Provider>
+        <Teams provideUserTeams>
+          {({teams, initiallyLoaded}) =>
+            initiallyLoaded ? (
+              <TeamKeyTransactionManager.Provider
+                organization={organization}
+                teams={teams}
+                selectedTeams={['myteams']}
+                selectedProjects={eventView.project.map(String)}
+              >
+                {this.renderSelectedDisplay(currentLandingDisplay.field)}
+              </TeamKeyTransactionManager.Provider>
+            ) : (
+              <LoadingIndicator />
+            )
+          }
+        </Teams>
       </Fragment>
     );
   }
@@ -331,4 +335,4 @@ const SearchContainer = styled('div')`
   }
 `;
 
-export default withRouter(withTeams(LandingContent));
+export default withRouter(LandingContent);

+ 18 - 19
static/app/views/performance/landing/index.tsx

@@ -6,18 +6,18 @@ import Button from 'app/components/button';
 import SearchBar from 'app/components/events/searchBar';
 import GlobalSdkUpdateAlert from 'app/components/globalSdkUpdateAlert';
 import * as Layout from 'app/components/layouts/thirds';
+import LoadingIndicator from 'app/components/loadingIndicator';
 import NavTabs from 'app/components/navTabs';
 import PageHeading from 'app/components/pageHeading';
 import * as TeamKeyTransactionManager from 'app/components/performance/teamKeyTransactionsManager';
 import {MAX_QUERY_LENGTH} from 'app/constants';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
-import {Organization, Project, Team} from 'app/types';
+import {Organization, Project} from 'app/types';
 import EventView from 'app/utils/discover/eventView';
 import {generateAggregateFields} from 'app/utils/discover/fields';
-import {isActiveSuperuser} from 'app/utils/isActiveSuperuser';
 import {OpBreakdownFilterProvider} from 'app/utils/performance/contexts/operationBreakdownFilter';
-import withTeams from 'app/utils/withTeams';
+import useTeams from 'app/utils/useTeams';
 
 import Filter, {SpanOperationBreakdownFilter} from '../transactionSummary/filter';
 import {getTransactionSearchQuery} from '../utils';
@@ -39,7 +39,6 @@ type Props = {
   eventView: EventView;
   location: Location;
   projects: Project[];
-  teams: Team[];
   shouldShowOnboarding: boolean;
   setError: (msg: string | undefined) => void;
   handleSearch: (searchQuery: string) => void;
@@ -54,24 +53,22 @@ const fieldToViewMap: Record<LandingDisplayField, FC<Props>> = {
   [LandingDisplayField.MOBILE]: MobileView,
 };
 
-function _PerformanceLanding(props: Props) {
+export function PerformanceLanding(props: Props) {
   const {
     organization,
     location,
     eventView,
     projects,
-    teams,
     handleSearch,
     handleTrendsClick,
     shouldShowOnboarding,
   } = props;
 
+  const {teams, initiallyLoaded} = useTeams({provideUserTeams: true});
+
   const currentLandingDisplay = getCurrentLandingDisplay(location, projects, eventView);
   const filterString = getTransactionSearchQuery(location, eventView.query);
 
-  const isSuperuser = isActiveSuperuser();
-  const userTeams = teams.filter(({isMember}) => isMember || isSuperuser);
-
   const [spanFilter, setSpanFilter] = useState(SpanOperationBreakdownFilter.None);
   const showOnboarding = shouldShowOnboarding;
 
@@ -136,14 +133,18 @@ function _PerformanceLanding(props: Props) {
                 maxQueryLength={MAX_QUERY_LENGTH}
               />
             </SearchContainerWithFilter>
-            <TeamKeyTransactionManager.Provider
-              organization={organization}
-              teams={userTeams}
-              selectedTeams={['myteams']}
-              selectedProjects={eventView.project.map(String)}
-            >
-              <ViewComponent {...props} />
-            </TeamKeyTransactionManager.Provider>
+            {initiallyLoaded ? (
+              <TeamKeyTransactionManager.Provider
+                organization={organization}
+                teams={teams}
+                selectedTeams={['myteams']}
+                selectedProjects={eventView.project.map(String)}
+              >
+                <ViewComponent {...props} />
+              </TeamKeyTransactionManager.Provider>
+            ) : (
+              <LoadingIndicator />
+            )}
           </OpBreakdownFilterProvider>
         </Layout.Main>
       </Layout.Body>
@@ -151,8 +152,6 @@ function _PerformanceLanding(props: Props) {
   );
 }
 
-export const PerformanceLanding = withTeams(_PerformanceLanding);
-
 const StyledHeading = styled(PageHeading)`
   line-height: 40px;
 `;

+ 8 - 11
static/app/views/performance/transactionSummary/teamKeyTransactionButton.tsx

@@ -8,12 +8,11 @@ import * as TeamKeyTransactionManager from 'app/components/performance/teamKeyTr
 import Tooltip from 'app/components/tooltip';
 import {IconStar} from 'app/icons';
 import {t, tn} from 'app/locale';
-import {Organization, Project, Team} from 'app/types';
+import {Organization, Project} from 'app/types';
 import {defined} from 'app/utils';
 import EventView from 'app/utils/discover/eventView';
-import {isActiveSuperuser} from 'app/utils/isActiveSuperuser';
+import useTeams from 'app/utils/useTeams';
 import withProjects from 'app/utils/withProjects';
-import withTeams from 'app/utils/withTeams';
 
 /**
  * This can't be a function component because `TeamKeyTransaction` uses
@@ -46,7 +45,6 @@ class TitleButton extends Component<TitleProps> {
 type BaseProps = {
   organization: Organization;
   transactionName: string;
-  teams: Team[];
 };
 
 type Props = BaseProps &
@@ -82,10 +80,11 @@ type WrapperProps = BaseProps & {
 function TeamKeyTransactionButtonWrapper({
   eventView,
   organization,
-  teams,
   projects,
   ...props
 }: WrapperProps) {
+  const {teams, initiallyLoaded} = useTeams({provideUserTeams: true});
+
   if (eventView.project.length !== 1) {
     return <TitleButton isOpen={false} disabled keyedTeams={null} />;
   }
@@ -96,21 +95,19 @@ function TeamKeyTransactionButtonWrapper({
     return <TitleButton isOpen={false} disabled keyedTeams={null} />;
   }
 
-  const isSuperuser = isActiveSuperuser();
-  const userTeams = teams.filter(({isMember}) => isMember || isSuperuser);
-
   return (
     <TeamKeyTransactionManager.Provider
       organization={organization}
-      teams={userTeams}
+      teams={teams}
       selectedTeams={['myteams']}
       selectedProjects={[String(projectId)]}
     >
       <TeamKeyTransactionManager.Consumer>
-        {results => (
+        {({isLoading, ...results}) => (
           <TeamKeyTransactionButton
             organization={organization}
             project={project}
+            isLoading={isLoading || !initiallyLoaded}
             {...props}
             {...results}
           />
@@ -120,4 +117,4 @@ function TeamKeyTransactionButtonWrapper({
   );
 }
 
-export default withTeams(withProjects(TeamKeyTransactionButtonWrapper));
+export default withProjects(TeamKeyTransactionButtonWrapper);

+ 29 - 24
static/app/views/performance/vitalDetail/vitalDetailContent.tsx

@@ -11,21 +11,21 @@ import ButtonBar from 'app/components/buttonBar';
 import {CreateAlertFromViewButton} from 'app/components/createAlertButton';
 import SearchBar from 'app/components/events/searchBar';
 import * as Layout from 'app/components/layouts/thirds';
+import LoadingIndicator from 'app/components/loadingIndicator';
 import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
 import * as TeamKeyTransactionManager from 'app/components/performance/teamKeyTransactionsManager';
 import {IconChevron} from 'app/icons';
 import {IconFlag} from 'app/icons/iconFlag';
 import {t} from 'app/locale';
 import space from 'app/styles/space';
-import {Organization, Project, Team} from 'app/types';
+import {Organization, Project} from 'app/types';
 import {generateQueryWithTag} from 'app/utils';
 import EventView from 'app/utils/discover/eventView';
 import {WebVital} from 'app/utils/discover/fields';
-import {isActiveSuperuser} from 'app/utils/isActiveSuperuser';
 import {decodeScalar} from 'app/utils/queryString';
+import Teams from 'app/utils/teams';
 import {MutableSearch} from 'app/utils/tokenizeSearch';
 import withProjects from 'app/utils/withProjects';
-import withTeams from 'app/utils/withTeams';
 
 import Breadcrumb from '../breadcrumb';
 import {getTransactionSearchQuery} from '../utils';
@@ -42,7 +42,6 @@ type Props = {
   eventView: EventView;
   organization: Organization;
   projects: Project[];
-  teams: Team[];
   router: InjectedRouter;
 
   vitalName: WebVital;
@@ -178,7 +177,7 @@ class VitalDetailContent extends React.Component<Props, State> {
   }
 
   render() {
-    const {location, eventView, organization, vitalName, projects, teams} = this.props;
+    const {location, eventView, organization, vitalName, projects} = this.props;
     const {incompatibleAlertNotice} = this.state;
     const query = decodeScalar(location.query.query, '');
 
@@ -188,9 +187,6 @@ class VitalDetailContent extends React.Component<Props, State> {
     const summaryConditions = getSummaryConditions(filterString);
     const description = vitalDescription[vitalName];
 
-    const isSuperuser = isActiveSuperuser();
-    const userTeams = teams.filter(({isMember}) => isMember || isSuperuser);
-
     return (
       <React.Fragment>
         <Layout.Header>
@@ -238,21 +234,30 @@ class VitalDetailContent extends React.Component<Props, State> {
             <StyledVitalInfo>
               <VitalInfo location={location} vital={vital} />
             </StyledVitalInfo>
-            <TeamKeyTransactionManager.Provider
-              organization={organization}
-              teams={userTeams}
-              selectedTeams={['myteams']}
-              selectedProjects={eventView.project.map(String)}
-            >
-              <Table
-                eventView={eventView}
-                projects={projects}
-                organization={organization}
-                location={location}
-                setError={this.setError}
-                summaryConditions={summaryConditions}
-              />
-            </TeamKeyTransactionManager.Provider>
+
+            <Teams provideUserTeams>
+              {({teams, initiallyLoaded}) =>
+                initiallyLoaded ? (
+                  <TeamKeyTransactionManager.Provider
+                    organization={organization}
+                    teams={teams}
+                    selectedTeams={['myteams']}
+                    selectedProjects={eventView.project.map(String)}
+                  >
+                    <Table
+                      eventView={eventView}
+                      projects={projects}
+                      organization={organization}
+                      location={location}
+                      setError={this.setError}
+                      summaryConditions={summaryConditions}
+                    />
+                  </TeamKeyTransactionManager.Provider>
+                ) : (
+                  <LoadingIndicator />
+                )
+              }
+            </Teams>
           </Layout.Main>
         </Layout.Body>
       </React.Fragment>
@@ -273,4 +278,4 @@ const StyledVitalInfo = styled('div')`
   margin-bottom: ${space(3)};
 `;
 
-export default withTeams(withProjects(VitalDetailContent));
+export default withProjects(VitalDetailContent);

+ 2 - 0
tests/js/spec/views/performance/content.spec.jsx

@@ -6,6 +6,7 @@ import {act} from 'sentry-test/reactTestingLibrary';
 
 import * as globalSelection from 'app/actionCreators/globalSelection';
 import ProjectsStore from 'app/stores/projectsStore';
+import TeamStore from 'app/stores/teamStore';
 import {OrganizationContext} from 'app/views/organizationContext';
 import PerformanceContent from 'app/views/performance/content';
 import {DEFAULT_MAX_DURATION} from 'app/views/performance/trends/utils';
@@ -63,6 +64,7 @@ function initializeTrendsData(query, addDefaultQuery = true) {
 
 describe('Performance > Content', function () {
   beforeEach(function () {
+    act(() => void TeamStore.loadInitialData([]));
     browserHistory.push = jest.fn();
     jest.spyOn(globalSelection, 'updateDateTime');
 

+ 3 - 0
tests/js/spec/views/performance/landing/index.spec.tsx

@@ -1,6 +1,8 @@
 import {mountWithTheme} from 'sentry-test/enzyme';
 import {initializeData} from 'sentry-test/performance/initializePerformanceData';
+import {act} from 'sentry-test/reactTestingLibrary';
 
+import TeamStore from 'app/stores/teamStore';
 import EventView from 'app/utils/discover/eventView';
 import {OrganizationContext} from 'app/views/organizationContext';
 import {PerformanceLanding} from 'app/views/performance/landing';
@@ -28,6 +30,7 @@ const WrappedComponent = ({data}) => {
 describe('Performance > Landing > Index', function () {
   let eventStatsMock: any;
   let eventsV2Mock: any;
+  act(() => void TeamStore.loadInitialData([]));
   beforeEach(function () {
     // @ts-expect-error
     MockApiClient.addMockResponse({

+ 2 - 0
tests/js/spec/views/performance/vitalDetail/index.spec.jsx

@@ -5,6 +5,7 @@ import {initializeOrg} from 'sentry-test/initializeOrg';
 import {act} from 'sentry-test/reactTestingLibrary';
 
 import ProjectsStore from 'app/stores/projectsStore';
+import TeamStore from 'app/stores/teamStore';
 import {OrganizationContext} from 'app/views/organizationContext';
 import VitalDetail from 'app/views/performance/vitalDetail/';
 
@@ -39,6 +40,7 @@ const WrappedComponent = ({organization, ...rest}) => {
 
 describe('Performance > VitalDetail', function () {
   beforeEach(function () {
+    act(() => void TeamStore.loadInitialData([]));
     browserHistory.push = jest.fn();
     MockApiClient.addMockResponse({
       url: '/organizations/org-slug/projects/',