Browse Source

feat(roles): Add upsell icon for team roles (#52850)

Depends on https://github.com/getsentry/sentry/pull/52328

### Organization Teams
![Screenshot 2023-07-13 at 8 35 21
PM](https://github.com/getsentry/sentry/assets/1748388/4261af86-b886-45a3-93dc-35ed79840fa0)

### Team Settings
![Screenshot 2023-07-13 at 8 35 45
PM](https://github.com/getsentry/sentry/assets/1748388/76053470-8195-46da-92a1-f24c429968e2)

### Member Details
![Screenshot 2023-07-13 at 8 35 57
PM](https://github.com/getsentry/sentry/assets/1748388/e9024267-1bbe-49a4-b074-97527b06310a)
Danny Lee 1 year ago
parent
commit
b4a8627f34

+ 13 - 0
static/app/components/teamRoleUtils.tsx

@@ -0,0 +1,13 @@
+import {Fragment} from 'react';
+
+import HookOrDefault from 'sentry/components/hookOrDefault';
+import {t} from 'sentry/locale';
+
+const LabelHook = HookOrDefault({
+  hookName: 'sidebar:item-label',
+  defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
+});
+
+export function TeamRoleColumnLabel() {
+  return <LabelHook id="team-roles-upsell">{t('Team Roles')}</LabelHook>;
+}

+ 51 - 45
static/app/views/settings/components/teamSelect/teamSelectForMember.tsx

@@ -12,6 +12,7 @@ import PanelBody from 'sentry/components/panels/panelBody';
 import PanelHeader from 'sentry/components/panels/panelHeader';
 import PanelItem from 'sentry/components/panels/panelItem';
 import TeamRoleSelect from 'sentry/components/teamRoleSelect';
+import {TeamRoleColumnLabel} from 'sentry/components/teamRoleUtils';
 import {IconSubtract} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
@@ -110,21 +111,27 @@ function TeamSelect({
 
   return (
     <Panel>
-      <PanelHeader hasButtons>
-        {t('Team')}
-        <DropdownAddTeam
-          disabled={disabled}
-          isLoadingTeams={isLoadingTeams}
-          isAddingTeamToMember
-          canCreateTeam={false}
-          onSearch={onSearch}
-          onSelect={onAddTeam}
-          onCreateTeam={onCreateTeam}
-          organization={organization}
-          selectedTeams={selectedTeams.map(tm => tm.slug)}
-          teams={teams}
-        />
-      </PanelHeader>
+      <TeamPanelHeader>
+        <div>{t('Team')}</div>
+        <div />
+        <div>
+          <TeamRoleColumnLabel />
+        </div>
+        <div>
+          <DropdownAddTeam
+            disabled={disabled}
+            isLoadingTeams={isLoadingTeams}
+            isAddingTeamToMember
+            canCreateTeam={false}
+            onSearch={onSearch}
+            onSelect={onAddTeam}
+            onCreateTeam={onCreateTeam}
+            organization={organization}
+            selectedTeams={selectedTeams.map(tm => tm.slug)}
+            teams={teams}
+          />
+        </div>
+      </TeamPanelHeader>
 
       <PanelBody>{loadingTeams ? <LoadingIndicator /> : renderBody()}</PanelBody>
     </Panel>
@@ -156,15 +163,15 @@ function TeamRow({
 
   return (
     <TeamPanelItem data-test-id="team-row-for-member">
-      <TeamPanelItemLeft>
+      <div>
         <Link to={`/settings/${organization.slug}/teams/${team.slug}/`}>
           <TeamBadge team={team} />
         </Link>
-      </TeamPanelItemLeft>
+      </div>
 
-      <TeamOrgRole>{orgRoleFromTeam}</TeamOrgRole>
+      <div>{orgRoleFromTeam}</div>
 
-      <RoleSelectWrapper>
+      <div>
         <TeamRoleSelect
           disabled={disabled}
           size="xs"
@@ -173,41 +180,40 @@ function TeamRow({
           member={member}
           onChangeTeamRole={newRole => onChangeTeamRole(team.slug, newRole)}
         />
-      </RoleSelectWrapper>
-
-      <Button
-        size="xs"
-        icon={<IconSubtract isCircled size="xs" />}
-        title={buttonHelpText}
-        disabled={isRemoveDisabled}
-        onClick={() => onRemoveTeam(team.slug)}
-      >
-        {t('Remove')}
-      </Button>
+      </div>
+
+      <div>
+        <Button
+          size="xs"
+          icon={<IconSubtract isCircled size="xs" />}
+          title={buttonHelpText}
+          disabled={isRemoveDisabled}
+          onClick={() => onRemoveTeam(team.slug)}
+        >
+          {t('Remove')}
+        </Button>
+      </div>
     </TeamPanelItem>
   );
 }
 
-const TeamPanelItem = styled(PanelItem)`
-  padding: ${space(2)};
-  align-items: center;
-  justify-content: space-between;
-`;
+const GRID_TEMPLATE = `
+  display: grid;
+  grid-template-columns: minmax(100px, 1fr) minmax(0px, 100px) 200px 95px;
+  gap: ${space(1)};
 
-const TeamPanelItemLeft = styled('div')`
-  flex-grow: 4;
+  > div:last-child {
+    margin-left: auto;
+  }
 `;
 
-const TeamOrgRole = styled('div')`
-  min-width: 90px;
-  flex-grow: 1;
-  display: flex;
-  justify-content: center;
+const TeamPanelHeader = styled(PanelHeader)`
+  ${GRID_TEMPLATE}
 `;
 
-const RoleSelectWrapper = styled('div')`
-  min-width: 200px;
-  margin-right: ${space(2)};
+const TeamPanelItem = styled(PanelItem)`
+  ${GRID_TEMPLATE}
+  padding: ${space(2)};
 `;
 
 export default TeamSelect;

+ 7 - 12
static/app/views/settings/organizationTeams/allTeamsRow.tsx

@@ -282,24 +282,19 @@ const TeamLink = styled(Link)`
 export {AllTeamsRow};
 export default withApi(AllTeamsRow);
 
-const TeamPanelItem = styled(PanelItem)`
+export const GRID_TEMPLATE = `
   display: grid;
-  grid-template-columns: minmax(150px, 4fr) min-content;
-  grid-template-rows: auto min-content;
-  gap: ${space(2)};
+  grid-template-columns: minmax(150px, 4fr) minmax(0px, 100px) 125px 110px;
+  gap: ${space(1)};
+`;
+
+const TeamPanelItem = styled(PanelItem)`
+  ${GRID_TEMPLATE}
   align-items: center;
 
   > div:last-child {
     margin-left: auto;
   }
-
-  @media (min-width: ${p => p.theme.breakpoints.small}) {
-    grid-template-columns: minmax(150px, 3fr) minmax(90px, 1fr) minmax(90px, 1fr) min-content;
-    grid-template-rows: auto;
-    > div:empty {
-      display: block !important;
-    }
-  }
 `;
 
 const DisplayRole = styled('div')<{isHidden: boolean}>`

+ 14 - 1
static/app/views/settings/organizationTeams/organizationTeams.tsx

@@ -12,6 +12,7 @@ import PanelBody from 'sentry/components/panels/panelBody';
 import PanelHeader from 'sentry/components/panels/panelHeader';
 import SearchBar from 'sentry/components/searchBar';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
+import {TeamRoleColumnLabel} from 'sentry/components/teamRoleUtils';
 import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
 import {IconAdd} from 'sentry/icons';
 import {t} from 'sentry/locale';
@@ -22,6 +23,7 @@ import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHea
 import {RoleOverwritePanelAlert} from 'sentry/views/settings/organizationTeams/roleOverwriteWarning';
 
 import AllTeamsList from './allTeamsList';
+import {GRID_TEMPLATE} from './allTeamsRow';
 import OrganizationAccessRequests from './organizationAccessRequests';
 
 type Props = {
@@ -97,7 +99,14 @@ function OrganizationTeams({
         query={teamQuery}
       />
       <Panel>
-        <PanelHeader>{t('Your Teams')}</PanelHeader>
+        <StyledPanelHeader>
+          <div>{t('Your Teams')}</div>
+          <div />
+          <div>
+            <TeamRoleColumnLabel />
+          </div>
+          <div />
+        </StyledPanelHeader>
         <PanelBody>
           <RoleOverwritePanelAlert
             orgRole={orgRole}
@@ -144,6 +153,10 @@ const StyledSearchBar = styled(SearchBar)`
   margin-bottom: ${space(2)};
 `;
 
+const StyledPanelHeader = styled(PanelHeader)`
+  ${GRID_TEMPLATE}
+`;
+
 const LoadMoreWrapper = styled('div')`
   display: grid;
   gap: ${space(2)};

+ 13 - 3
static/app/views/settings/organizationTeams/teamMembers.tsx

@@ -21,6 +21,7 @@ import LoadingError from 'sentry/components/loadingError';
 import Pagination from 'sentry/components/pagination';
 import Panel from 'sentry/components/panels/panel';
 import PanelHeader from 'sentry/components/panels/panelHeader';
+import {TeamRoleColumnLabel} from 'sentry/components/teamRoleUtils';
 import {IconUser} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
@@ -30,7 +31,9 @@ import withConfig from 'sentry/utils/withConfig';
 import withOrganization from 'sentry/utils/withOrganization';
 import DeprecatedAsyncView, {AsyncViewState} from 'sentry/views/deprecatedAsyncView';
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
-import TeamMembersRow from 'sentry/views/settings/organizationTeams/teamMembersRow';
+import TeamMembersRow, {
+  GRID_TEMPLATE,
+} from 'sentry/views/settings/organizationTeams/teamMembersRow';
 import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
 
 import {getButtonHelpText} from './utils';
@@ -327,10 +330,13 @@ class TeamMembers extends DeprecatedAsyncView<Props, State> {
         />
 
         <Panel>
-          <PanelHeader hasButtons>
+          <StyledPanelHeader hasButtons>
             <div>{t('Members')}</div>
+            <div>
+              <TeamRoleColumnLabel />
+            </div>
             <div style={{textTransform: 'none'}}>{this.renderDropdown(isTeamAdmin)}</div>
-          </PanelHeader>
+          </StyledPanelHeader>
           {this.state.teamMembers.length ? (
             this.state.teamMembers.map(member => {
               return (
@@ -390,4 +396,8 @@ const StyledCreateMemberLink = styled(Link)`
   text-transform: none;
 `;
 
+const StyledPanelHeader = styled(PanelHeader)`
+  ${GRID_TEMPLATE}
+`;
+
 export default withConfig(withApi(withOrganization(TeamMembers)));

+ 7 - 3
static/app/views/settings/organizationTeams/teamMembersRow.tsx

@@ -128,10 +128,14 @@ const RoleSelectWrapper = styled('div')`
   }
 `;
 
-const TeamRolesPanelItem = styled(PanelItem)`
+export const GRID_TEMPLATE = `
   display: grid;
-  grid-template-columns: minmax(120px, 4fr) minmax(120px, 2fr) minmax(100px, 1fr);
-  gap: ${space(2)};
+  grid-template-columns: minmax(100px, 1fr) 200px 95px;
+  gap: ${space(1)};
+`;
+
+const TeamRolesPanelItem = styled(PanelItem)`
+  ${GRID_TEMPLATE};
   align-items: center;
 
   > div:last-child {