Browse Source

feat(workflow): Switch Team Selector to use updated component (#33510)

* feat(workflow): Add search bar to projects page

Part of the projects page redesign - add a projects search bar to the projects page.

FIXES WOR-1758

* switch to useCallback instead of useMemo

* feat(workflow): Switch Team Selector to use updated component

Switch Team Selector to use the updated component.

FIXES WOR-1767

* update for tests/update tests

* update styles and text

* fix showMyTeamsDescription prop

* use Layout component for different screen sizes and update styles
Kelly Carino 2 years ago
parent
commit
f981e5809a

+ 8 - 2
static/app/views/alerts/rules/filter.tsx

@@ -58,9 +58,15 @@ type Props = {
   dropdownSections: DropdownSection[];
   header: React.ReactElement;
   onFilterChange: (section: string, filterSelection: Set<string>) => void;
+  showMyTeamsDescription?: boolean;
 };
 
-function Filter({onFilterChange, header, dropdownSections}: Props) {
+function Filter({
+  onFilterChange,
+  header,
+  dropdownSections,
+  showMyTeamsDescription,
+}: Props) {
   function toggleFilter(sectionId: string, value: string) {
     const section = dropdownSections.find(
       dropdownSection => dropdownSection.id === sectionId
@@ -99,7 +105,7 @@ function Filter({onFilterChange, header, dropdownSections}: Props) {
 
   const activeFilters = getActiveFilters();
 
-  let filterDescription = t('All Teams');
+  let filterDescription = showMyTeamsDescription ? t('My Teams') : t('All Teams');
   if (activeFilters.length > 0) {
     filterDescription = activeFilters[0].label;
   }

+ 32 - 9
static/app/views/alerts/rules/teamFilter.tsx

@@ -7,6 +7,7 @@ import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
+import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
 import useTeams from 'sentry/utils/useTeams';
 
 import Filter from './filter';
@@ -15,6 +16,18 @@ type Props = {
   handleChangeFilter: (sectionId: string, activeFilters: Set<string>) => void;
   selectedTeams: Set<string>;
   selectedStatus?: Set<string>;
+  /**
+   * only show teams user is a member of
+   */
+  showIsMemberTeams?: boolean;
+  /**
+   * show My Teams and Unassigned options
+   */
+  showMyTeamsAndUnassigned?: boolean;
+  /**
+   * show My Teams as the default dropdown description
+   */
+  showMyTeamsDescription?: boolean;
   showStatus?: boolean;
 };
 
@@ -23,10 +36,14 @@ function TeamFilter({
   showStatus = false,
   selectedStatus = new Set(),
   handleChangeFilter,
+  showIsMemberTeams = false,
+  showMyTeamsAndUnassigned = true,
+  showMyTeamsDescription = false,
 }: Props) {
   const {teams, onSearch, fetching} = useTeams();
   const debouncedSearch = debounce(onSearch, DEFAULT_DEBOUNCE_DURATION);
   const [teamFilterSearch, setTeamFilterSearch] = useState<string | undefined>();
+  const isSuperuser = isActiveSuperuser();
 
   const statusOptions = [
     {
@@ -57,17 +74,21 @@ function TeamFilter({
       filtered: false,
     },
   ];
-  const teamItems = teams.map(({id, slug}) => ({
-    label: slug,
-    value: id,
-    filtered: teamFilterSearch
-      ? !slug.toLowerCase().includes(teamFilterSearch.toLowerCase())
-      : false,
-    checked: selectedTeams.has(id),
-  }));
+  const isMemberTeams = teams.filter(team => team.isMember);
+  const teamItems = (isSuperuser ? teams : showIsMemberTeams ? isMemberTeams : teams).map(
+    ({id, slug}) => ({
+      label: slug,
+      value: id,
+      filtered: teamFilterSearch
+        ? !slug.toLowerCase().includes(teamFilterSearch.toLowerCase())
+        : false,
+      checked: selectedTeams.has(id),
+    })
+  );
 
   return (
     <Filter
+      showMyTeamsDescription={showMyTeamsDescription}
       header={
         <InputWrapper>
           <StyledInput
@@ -100,7 +121,9 @@ function TeamFilter({
         {
           id: 'teams',
           label: t('Teams'),
-          items: [...additionalOptions, ...teamItems],
+          items: showMyTeamsAndUnassigned
+            ? [...additionalOptions, ...teamItems]
+            : [...teamItems],
         },
       ]}
     />

+ 164 - 162
static/app/views/projectsDashboard/index.tsx

@@ -1,7 +1,6 @@
 import {Fragment, useCallback, useEffect, useState} from 'react';
 import LazyLoad from 'react-lazyload';
 import {RouteComponentProps} from 'react-router';
-import {useTheme} from '@emotion/react';
 import styled from '@emotion/styled';
 import {withProfiler} from '@sentry/react';
 import debounce from 'lodash/debounce';
@@ -10,8 +9,8 @@ import uniqBy from 'lodash/uniqBy';
 
 import {Client} from 'sentry/api';
 import Button from 'sentry/components/button';
-import TeamSelector from 'sentry/components/forms/teamSelector';
 import IdBadge from 'sentry/components/idBadge';
+import * as Layout from 'sentry/components/layouts/thirds';
 import Link from 'sentry/components/links/link';
 import LoadingError from 'sentry/components/loadingError';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
@@ -26,14 +25,15 @@ import ProjectsStatsStore from 'sentry/stores/projectsStatsStore';
 import space from 'sentry/styles/space';
 import {Organization, TeamWithProjects} from 'sentry/types';
 import {sortProjects} from 'sentry/utils';
-import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
 import withApi from 'sentry/utils/withApi';
 import withOrganization from 'sentry/utils/withOrganization';
 import withTeamsForUser from 'sentry/utils/withTeamsForUser';
+import TeamFilter from 'sentry/views/alerts/rules/teamFilter';
 
 import ProjectCard from './projectCard';
 import Resources from './resources';
 import TeamSection from './teamSection';
+import {getTeamParams} from './utils';
 
 type Props = {
   api: Client;
@@ -43,14 +43,21 @@ type Props = {
   teams: TeamWithProjects[];
 } & RouteComponentProps<{orgId: string}, {}>;
 
-function Dashboard({teams, params, organization, loadingTeams, error}: Props) {
+function Dashboard({
+  teams,
+  params,
+  organization,
+  loadingTeams,
+  error,
+  router,
+  location,
+}: Props) {
   useEffect(() => {
     return function cleanup() {
       ProjectsStatsStore.reset();
     };
   }, []);
   const [projectQuery, setProjectQuery] = useState('');
-  const [currentTeam, setCurrentTeam] = useState('');
   const debouncedSearchQuery = useCallback(
     debounce(handleSearch, DEFAULT_DEBOUNCE_DURATION),
     []
@@ -64,25 +71,31 @@ function Dashboard({teams, params, organization, loadingTeams, error}: Props) {
     return <LoadingError message={t('An error occurred while fetching your projects')} />;
   }
 
-  const theme = useTheme();
-  const isSuperuser = isActiveSuperuser();
+  const canCreateProjects = organization.access.includes('project:admin');
+  const canJoinTeam = organization.access.includes('team:read');
+  const hasTeamAdminAccess = organization.access.includes('team:admin');
+  const hasProjectAccess = organization.access.includes('project:read');
+  const hasProjectRedesign = organization.features.includes('projects-page-redesign');
 
-  const filteredTeams = teams.filter(team => team.projects.length);
-  filteredTeams.sort((team1, team2) => team1.slug.localeCompare(team2.slug));
+  const selectedTeams = new Set(getTeamParams(location ? location.query.team : ''));
+  const filteredTeams = teams.filter(team =>
+    hasProjectRedesign ? selectedTeams.has(team.id) : team.projects.length
+  );
 
+  if (!hasProjectRedesign) {
+    filteredTeams.sort((team1, team2) => team1.slug.localeCompare(team2.slug));
+  }
+  const filteredTeamProjects = uniqBy(
+    flatten((filteredTeams ?? teams).map(team => team.projects)),
+    'id'
+  );
   const projects = uniqBy(flatten(teams.map(teamObj => teamObj.projects)), 'id');
-  const currentProjects = filteredTeams.find(team => team.id === currentTeam)?.projects;
+  const currentProjects = !selectedTeams.size ? projects : filteredTeamProjects;
   const filteredProjects = (currentProjects ?? projects).filter(project =>
     project.slug.includes(projectQuery)
   );
   const favorites = projects.filter(project => project.isBookmarked);
 
-  const canCreateProjects = organization.access.includes('project:admin');
-  const canJoinTeam = organization.access.includes('team:read');
-  const hasTeamAdminAccess = organization.access.includes('team:admin');
-  const hasProjectAccess = organization.access.includes('project:read');
-  const hasProjectRedesign = organization.features.includes('projects-page-redesign');
-
   const showEmptyMessage = projects.length === 0 && favorites.length === 0;
   const showResources = projects.length === 1 && !projects[0].firstEvent;
 
@@ -90,9 +103,21 @@ function Dashboard({teams, params, organization, loadingTeams, error}: Props) {
     setProjectQuery(searchQuery);
   }
 
-  function handleChange(newValue) {
-    const updatedTeam = newValue ? newValue.actor.id : '';
-    setCurrentTeam(updatedTeam);
+  function handleChangeFilter(sectionId: string, activeFilters: Set<string>) {
+    const {...currentQuery} = location.query;
+
+    let team = currentQuery.team;
+    if (sectionId === 'teams') {
+      team = activeFilters.size ? [...activeFilters] : '';
+    }
+
+    router.push({
+      pathname: location.pathname,
+      query: {
+        ...currentQuery,
+        team: team.length === 0 ? '' : team,
+      },
+    });
   }
 
   if (showEmptyMessage) {
@@ -107,144 +132,104 @@ function Dashboard({teams, params, organization, loadingTeams, error}: Props) {
       {projects.length > 0 && (
         <Fragment>
           <ProjectsHeader>
-            <PageHeading>{t('Projects')}</PageHeading>
-            <ButtonContainer>
-              {hasProjectRedesign && (
+            <Title>
+              <PageHeading>{t('Projects')}</PageHeading>
+            </Title>
+            <Layout.HeaderActions>
+              <ButtonContainer>
+                {hasProjectRedesign && (
+                  <Button
+                    icon={<IconUser size="xs" />}
+                    title={
+                      canJoinTeam
+                        ? undefined
+                        : t('You do not have permission to join a team.')
+                    }
+                    disabled={!canJoinTeam}
+                    to={`/settings/${organization.slug}/teams/`}
+                    data-test-id="join-team"
+                  >
+                    {t('Join a Team')}
+                  </Button>
+                )}
                 <Button
-                  size="small"
-                  icon={<IconUser size="xs" />}
+                  priority={hasProjectRedesign ? 'primary' : 'default'}
+                  disabled={!canCreateProjects}
                   title={
-                    canJoinTeam
-                      ? undefined
-                      : t('You do not have permission to join a team.')
+                    !canCreateProjects
+                      ? t('You do not have permission to create projects')
+                      : undefined
                   }
-                  disabled={!canJoinTeam}
-                  to={`/settings/${organization.slug}/teams/`}
-                  data-test-id="join-team"
+                  to={`/organizations/${organization.slug}/projects/new/`}
+                  icon={<IconAdd size="xs" isCircled />}
+                  data-test-id="create-project"
                 >
-                  {t('Join a Team')}
+                  {t('Create Project')}
                 </Button>
-              )}
-              <Button
-                size="small"
-                priority={hasProjectRedesign ? 'primary' : 'default'}
-                disabled={!canCreateProjects}
-                title={
-                  !canCreateProjects
-                    ? t('You do not have permission to create projects')
-                    : undefined
-                }
-                to={`/organizations/${organization.slug}/projects/new/`}
-                icon={<IconAdd size="xs" isCircled />}
-                data-test-id="create-project"
-              >
-                {t('Create Project')}
-              </Button>
-            </ButtonContainer>
+              </ButtonContainer>
+            </Layout.HeaderActions>
           </ProjectsHeader>
-          {hasProjectRedesign && (
-            <SearchAndSelectorWrapper>
-              <StyledSearchBar
-                defaultQuery=""
-                placeholder={t('Search for projects by name')}
-                onChange={debouncedSearchQuery}
-                query={projectQuery}
-              />
-              <StyledTeamSelector
-                name="select-team"
-                aria-label="select-team"
-                inFieldLabel={t('Team: ')}
-                placeholder={t('My Teams')}
-                value={currentTeam}
-                onChange={choice => handleChange(choice)}
-                teamFilter={isSuperuser ? undefined : filterTeam => filterTeam.isMember}
-                useId
-                clearable
-                styles={{
-                  placeholder: (provided: any) => ({
-                    ...provided,
-                    paddingLeft: space(0.5),
-                    ':before': {
-                      ...provided[':before'],
-                      color: theme.textColor,
-                    },
-                  }),
-                  singleValue(provided: any) {
-                    const custom = {
-                      display: 'flex',
-                      justifyContent: 'space-between',
-                      alignItems: 'center',
-                      fontSize: theme.fontSizeMedium,
-                      ':before': {
-                        ...provided[':before'],
-                        color: theme.textColor,
-                        marginRight: space(1.5),
-                        marginLeft: space(0.5),
-                      },
-                    };
-                    return {...provided, ...custom};
-                  },
-                  input: (provided: any, state: any) => ({
-                    ...provided,
-                    display: 'grid',
-                    gridTemplateColumns: 'max-content 1fr',
-                    alignItems: 'center',
-                    marginRight: space(0.25),
-                    gridGap: space(1.5),
-                    ':before': {
-                      backgroundColor: state.theme.backgroundSecondary,
-                      height: 24,
-                      width: 38,
-                      borderRadius: 3,
-                      content: '""',
-                      display: 'block',
-                    },
-                  }),
-                }}
-              />
-            </SearchAndSelectorWrapper>
-          )}
+          <Body hasProjectRedesign={hasProjectRedesign}>
+            <Layout.Main fullWidth>
+              {hasProjectRedesign && (
+                <SearchAndSelectorWrapper>
+                  <TeamFilter
+                    selectedTeams={selectedTeams}
+                    handleChangeFilter={handleChangeFilter}
+                    showIsMemberTeams
+                    showMyTeamsAndUnassigned={false}
+                    showMyTeamsDescription
+                  />
+                  <StyledSearchBar
+                    defaultQuery=""
+                    placeholder={t('Search for projects by name')}
+                    onChange={debouncedSearchQuery}
+                    query={projectQuery}
+                  />
+                </SearchAndSelectorWrapper>
+              )}
+              {hasProjectRedesign ? (
+                <LazyLoad once debounce={50} height={300} offset={300}>
+                  <ProjectCards>
+                    {filteredProjects.map(project => (
+                      <ProjectCard
+                        data-test-id={project.slug}
+                        key={project.slug}
+                        project={project}
+                        hasProjectAccess={hasProjectAccess}
+                      />
+                    ))}
+                  </ProjectCards>
+                </LazyLoad>
+              ) : (
+                filteredTeams.map((team, index) => (
+                  <LazyLoad key={team.slug} once debounce={50} height={300} offset={300}>
+                    <TeamSection
+                      orgId={params.orgId}
+                      team={team}
+                      showBorder={index !== teams.length - 1}
+                      title={
+                        hasTeamAdminAccess ? (
+                          <TeamLink
+                            to={`/settings/${organization.slug}/teams/${team.slug}/`}
+                          >
+                            <IdBadge team={team} avatarSize={22} />
+                          </TeamLink>
+                        ) : (
+                          <IdBadge team={team} avatarSize={22} />
+                        )
+                      }
+                      projects={sortProjects(team.projects)}
+                      access={new Set(organization.access)}
+                    />
+                  </LazyLoad>
+                ))
+              )}
+              {showResources && <Resources organization={organization} />}
+            </Layout.Main>
+          </Body>
         </Fragment>
       )}
-
-      {hasProjectRedesign ? (
-        <LazyLoad once debounce={50} height={300} offset={300}>
-          <ProjectCardsContainer>
-            <ProjectCards>
-              {filteredProjects.map(project => (
-                <ProjectCard
-                  data-test-id={project.slug}
-                  key={project.slug}
-                  project={project}
-                  hasProjectAccess={hasProjectAccess}
-                />
-              ))}
-            </ProjectCards>
-          </ProjectCardsContainer>
-        </LazyLoad>
-      ) : (
-        filteredTeams.map((team, index) => (
-          <LazyLoad key={team.slug} once debounce={50} height={300} offset={300}>
-            <TeamSection
-              orgId={params.orgId}
-              team={team}
-              showBorder={index !== teams.length - 1}
-              title={
-                hasTeamAdminAccess ? (
-                  <TeamLink to={`/settings/${organization.slug}/teams/${team.slug}/`}>
-                    <IdBadge team={team} avatarSize={22} />
-                  </TeamLink>
-                ) : (
-                  <IdBadge team={team} avatarSize={22} />
-                )
-              }
-              projects={sortProjects(team.projects)}
-              access={new Set(organization.access)}
-            />
-          </LazyLoad>
-        ))
-      )}
-      {showResources && <Resources organization={organization} />}
     </Fragment>
   );
 }
@@ -260,37 +245,54 @@ const TeamLink = styled(Link)`
   align-items: center;
 `;
 
-const ProjectsHeader = styled('div')`
-  padding: ${space(3)} ${space(4)} 0 ${space(4)};
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
+const ProjectsHeader = styled(Layout.Header)`
+  border-bottom: none;
+`;
+
+const Title = styled(Layout.HeaderContent)`
+  margin-bottom: 0;
+  padding-top: ${space(0.5)};
 `;
 
 const ButtonContainer = styled('div')`
   display: inline-flex;
   gap: ${space(1)};
+  padding-top: ${space(0.5)};
 `;
 
 const SearchAndSelectorWrapper = styled('div')`
   display: flex;
-  gap: 16px;
+  gap: ${space(2)};
   justify-content: flex-end;
   align-items: flex-end;
+  margin-bottom: ${space(2)};
+
+  @media (max-width: ${p => p.theme.breakpoints[0]}) {
+    display: block;
+  }
+
+  @media (min-width: ${p => p.theme.breakpoints[3]}) {
+    display: flex;
+  }
 `;
 
 const StyledSearchBar = styled(SearchBar)`
-  margin-left: 30px;
   flex-grow: 1;
-`;
 
-const StyledTeamSelector = styled(TeamSelector)`
-  margin: ${space(2)} 30px 0 0;
-  width: 300px;
+  @media (max-width: ${p => p.theme.breakpoints[0]}) {
+    margin-top: ${space(1)};
+  }
 `;
 
-const ProjectCardsContainer = styled('div')`
-  padding: ${space(2)} 30px ${space(2)} 30px;
+const Body = styled(Layout.Body)<{hasProjectRedesign: boolean}>`
+  padding-top: ${space(2)} !important;
+  background-color: ${p => p.theme.surface100};
+
+  ${p =>
+    !p.hasProjectRedesign &&
+    `
+    padding: 0 !important;
+  `}
 `;
 
 const ProjectCards = styled('div')`

+ 14 - 0
static/app/views/projectsDashboard/utils.tsx

@@ -0,0 +1,14 @@
+/**
+ * Noramlize a team slug from the query
+ */
+export function getTeamParams(team?: string | string[]): string[] {
+  if (team === '' || team === undefined) {
+    return [];
+  }
+
+  if (Array.isArray(team)) {
+    return team;
+  }
+
+  return [team];
+}