import {Fragment, useEffect, useMemo, useState} from 'react';
import LazyLoad, {forceCheck} from 'react-lazyload';
import styled from '@emotion/styled';
import {withProfiler} from '@sentry/react';
import debounce from 'lodash/debounce';
import uniqBy from 'lodash/uniqBy';
import {LinkButton} from 'sentry/components/button';
import ButtonBar from 'sentry/components/buttonBar';
import * as Layout from 'sentry/components/layouts/thirds';
import LoadingError from 'sentry/components/loadingError';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import NoProjectMessage from 'sentry/components/noProjectMessage';
import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
import {canCreateProject} from 'sentry/components/projects/canCreateProject';
import SearchBar from 'sentry/components/searchBar';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
import {IconAdd, IconUser} from 'sentry/icons';
import {t} from 'sentry/locale';
import ProjectsStatsStore from 'sentry/stores/projectsStatsStore';
import {space} from 'sentry/styles/space';
import type {Team} from 'sentry/types/organization';
import type {Project, TeamWithProjects} from 'sentry/types/project';
import {
onRenderCallback,
Profiler,
setGroupedEntityTag,
} from 'sentry/utils/performanceForSentry';
import {sortProjects} from 'sentry/utils/project/sortProjects';
import {useLocation} from 'sentry/utils/useLocation';
import {useNavigate} from 'sentry/utils/useNavigate';
import useOrganization from 'sentry/utils/useOrganization';
import useProjects from 'sentry/utils/useProjects';
import {useTeamsById} from 'sentry/utils/useTeamsById';
import {useUser} from 'sentry/utils/useUser';
import {useUserTeams} from 'sentry/utils/useUserTeams';
import TeamFilter from 'sentry/views/alerts/list/rules/teamFilter';
import ProjectCard from './projectCard';
import Resources from './resources';
import {getTeamParams} from './utils';
function ProjectCardList({projects}: {projects: Project[]}) {
const organization = useOrganization();
const hasProjectAccess = organization.access.includes('project:read');
// By default react-lazyload will only check for intesecting components on scroll
// This forceCheck call is necessary to recalculate when filtering projects
useEffect(() => {
forceCheck();
}, [projects]);
return (
{sortProjects(projects).map(project => (
))}
);
}
function addProjectsToTeams(teams: Team[], projects: Project[]): TeamWithProjects[] {
return teams.map(team => ({
...team,
projects: projects.filter(project => project.teams.some(tm => tm.id === team.id)),
}));
}
function Dashboard() {
const navigate = useNavigate();
const location = useLocation();
const organization = useOrganization();
useEffect(() => {
return function cleanup() {
ProjectsStatsStore.reset();
};
}, []);
const {teams: userTeams, isLoading: loadingTeams, isError} = useUserTeams();
const isAllTeams = location.query.team === '';
const selectedTeams = getTeamParams(location.query.team ?? 'myteams');
const {teams: allTeams} = useTeamsById({
ids: selectedTeams.filter(team => team !== 'myteams'),
});
const user = useUser();
const [projectQuery, setProjectQuery] = useState('');
const debouncedSearchQuery = useMemo(
() => debounce(handleSearch, DEFAULT_DEBOUNCE_DURATION),
[]
);
const {projects, fetching, fetchError} = useProjects();
const showNonMemberProjects = useMemo(() => {
const isOrgAdminOrManager =
organization.orgRole === 'owner' || organization.orgRole === 'manager';
const isOpenMembership = organization.features.includes('open-membership');
return user.isSuperuser || isOrgAdminOrManager || isOpenMembership;
}, [user, organization.orgRole, organization.features]);
if (loadingTeams || fetching) {
return ;
}
if (isError || fetchError) {
return ;
}
const includeMyTeams = isAllTeams || selectedTeams.some(team => team === 'myteams');
const hasOtherTeams = selectedTeams.some(team => team !== 'myteams');
const myTeams = includeMyTeams ? userTeams : [];
const otherTeams = isAllTeams
? allTeams
: hasOtherTeams
? allTeams.filter(team => selectedTeams.includes(`${team.id}`))
: [];
const filteredTeams = [...myTeams, ...otherTeams].filter(team => {
if (showNonMemberProjects) {
return true;
}
return team.isMember;
});
const filteredTeamsWithProjects = addProjectsToTeams(filteredTeams, projects);
const currentProjects = uniqBy(
filteredTeamsWithProjects.flatMap(team => team.projects),
'id'
);
setGroupedEntityTag('projects.total', 1000, projects.length);
const filteredProjects = currentProjects.filter(project =>
project.slug.includes(projectQuery)
);
const showResources = projects.length === 1 && !projects[0]!.firstEvent;
const canJoinTeam = organization.access.includes('team:read');
const canUserCreateProject = canCreateProject(organization);
function handleSearch(searchQuery: string) {
setProjectQuery(searchQuery);
}
function handleChangeFilter(activeFilters: string[]) {
navigate({
pathname: location.pathname,
query: {
...location.query,
team: activeFilters.length > 0 ? activeFilters : '',
},
});
}
return (
{t('Projects')}
}
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')}
}
data-test-id="create-project"
>
{t('Create Project')}
{showResources && }
);
}
function OrganizationDashboard() {
const organization = useOrganization();
return (
);
}
const SearchAndSelectorWrapper = styled('div')`
display: flex;
gap: ${space(2)};
justify-content: flex-end;
align-items: flex-end;
margin-bottom: ${space(2)};
@media (max-width: ${p => p.theme.breakpoints.small}) {
display: block;
}
@media (min-width: ${p => p.theme.breakpoints.xlarge}) {
display: flex;
}
`;
const StyledSearchBar = styled(SearchBar)`
flex-grow: 1;
@media (max-width: ${p => p.theme.breakpoints.small}) {
margin-top: ${space(1)};
}
`;
const ProjectCards = styled('div')`
display: grid;
gap: ${space(3)};
grid-template-columns: repeat(auto-fill, minmax(1fr, 400px));
@media (min-width: ${p => p.theme.breakpoints.small}) {
grid-template-columns: repeat(auto-fill, minmax(470px, 1fr));
}
@media (min-width: ${p => p.theme.breakpoints.medium}) {
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
}
`;
export default withProfiler(OrganizationDashboard);