import {useEffect, useMemo, useState} from 'react'; import styled from '@emotion/styled'; import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import {Button} from 'sentry/components/button'; 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 {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter'; import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container'; import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter'; import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters'; import {IconChevron} from 'sentry/icons'; import {t} from 'sentry/locale'; import ConfigStore from 'sentry/stores/configStore'; import {space} from 'sentry/styles/space'; import type {Project} from 'sentry/types'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import useProjects from 'sentry/utils/useProjects'; import useRouter from 'sentry/utils/useRouter'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import Header from '../components/header'; import type {Threshold} from '../utils/types'; import useFetchThresholdsListData from '../utils/useFetchThresholdsListData'; import NoThresholdCard from './noThresholdCard'; import ThresholdGroupTable from './thresholdGroupTable'; type Props = {}; function ReleaseThresholdList({}: Props) { const [listError, setListError] = useState(''); const [newProjThresholdsPage, setNewProjThresholdsPage] = useState(0); const PAGE_SIZE = 10; const router = useRouter(); const organization = useOrganization(); useEffect(() => { const hasV2ReleaseUIEnabled = organization.features.includes('releases-v2-internal') || organization.features.includes('releases-v2') || organization.features.includes('releases-v2-st'); if (!hasV2ReleaseUIEnabled) { const redirect = normalizeUrl(`/organizations/${organization.slug}/releases/`); router.replace(redirect); } }, [router, organization]); const {projects} = useProjects(); const {selection} = usePageFilters(); const { data: thresholds = [], error: requestError, isLoading, isError, refetch, } = useFetchThresholdsListData({ selectedProjectIds: selection.projects, selectedEnvs: selection.environments, }); const selectedProjects: Project[] = useMemo(() => { return projects.filter( project => selection.projects.some(id => String(id) === project.id || id === -1) || !selection.projects.length ); }, [projects, selection.projects]); const projectsById: {[key: string]: Project} = useMemo(() => { const byId = {}; selectedProjects.forEach(proj => { byId[proj.id] = proj; }); return byId; }, [selectedProjects]); const getAllEnvironmentNames = useMemo((): string[] => { const selectedProjectIds = selection.projects.map(id => String(id)); const {user} = ConfigStore.getState(); const allEnvSet = new Set(projects.flatMap(project => project.environments)); // NOTE: mostly taken from environmentSelector.tsx const unSortedEnvs = new Set( projects.flatMap(project => { /** * Include environments from: * all projects I can access if -1 is the only selected project. * all member projects if 'my projects' (empty list) is selected. * all projects if the user is a superuser * the requested projects */ const allProjectsSelectedICanAccess = selectedProjectIds.length === 1 && selectedProjectIds[0] === String(ALL_ACCESS_PROJECTS) && project.hasAccess; const myProjectsSelected = selectedProjectIds.length === 0 && project.isMember; const allMemberProjectsIfSuperuser = selectedProjectIds.length === 0 && user.isSuperuser; if ( allProjectsSelectedICanAccess || myProjectsSelected || allMemberProjectsIfSuperuser || selectedProjectIds.includes(project.id) ) { return project.environments; } return []; }) ); const envDiff = new Set([...allEnvSet].filter(x => !unSortedEnvs.has(x))); // bubble the selected projects envs first, then concat the rest of the envs return Array.from(unSortedEnvs) .sort() .concat([...envDiff].sort()); }, [projects, selection.projects]); const getEnvironmentsAvailableToProject = useMemo((): string[] => { const selectedProjectIds = selection.projects.map(id => String(id)); const allEnvSet = new Set(projects.flatMap(project => project.environments)); // NOTE: mostly taken from environmentSelector.tsx const unSortedEnvs = new Set( projects.flatMap(project => { /** * Include environments from: * all projects if -1 is the only selected project. * all member projects if 'my projects' (empty list) is selected. * the requested projects */ const allProjectsSelected = selectedProjectIds.length === 1 && selectedProjectIds[0] === String(ALL_ACCESS_PROJECTS) && project.hasAccess; const myProjectsSelected = selectedProjectIds.length === 0 && project.isMember; if ( allProjectsSelected || myProjectsSelected || selectedProjectIds.includes(project.id) ) { return project.environments; } return []; }) ); const envDiff = new Set([...allEnvSet].filter(x => !unSortedEnvs.has(x))); // bubble the selected projects envs first, then concat the rest of the envs return Array.from(unSortedEnvs) .sort() .concat([...envDiff].sort()); }, [projects, selection.projects]); /** * Thresholds filtered by environment selection * NOTE: currently no way to filter for 'None' environments */ const filteredThresholds = selection.environments.length ? thresholds.filter(threshold => { return threshold.environment?.name ? selection.environments.indexOf(threshold.environment.name) > -1 : !selection.environments.length; }) : thresholds; const thresholdsByProject: {[key: string]: Threshold[]} = useMemo(() => { const byProj = {}; filteredThresholds.forEach(threshold => { const projId = threshold.project.id; if (!byProj[projId]) { byProj[projId] = []; } byProj[projId].push(threshold); }); return byProj; }, [filteredThresholds]); const projectsWithoutThresholds: Project[] = useMemo(() => { // TODO: limit + paginate list return selectedProjects.filter(proj => !thresholdsByProject[proj.id]); }, [thresholdsByProject, selectedProjects]); const setTempError = msg => { setListError(msg); setTimeout(() => setListError(''), 5000); }; if (isError) { return ; } if (isLoading) { return ; } return (
{listError} {thresholdsByProject && Object.entries(thresholdsByProject).map(([projId, thresholdsByProj]) => ( ))} {projectsWithoutThresholds.length > 0 && (
Projects without Thresholds {projectsWithoutThresholds .slice( PAGE_SIZE * newProjThresholdsPage, PAGE_SIZE * newProjThresholdsPage + PAGE_SIZE ) .map(proj => ( ))}
)}
); } export default ReleaseThresholdList; const FilterRow = styled('div')` display: flex; align-items: center; `; const ListError = styled('div')` color: red; margin: 0 ${space(2)}; width: 100%; display: flex; justify-content: center; `; const ReleaseThresholdsPageFilterBar = styled(PageFilterBar)` margin-bottom: ${space(2)}; `; const Paginator = styled('div')` margin: ${space(2)} 0; display: flex; justify-content: flex-end; align-items: center; `; const CurrentPage = styled('div')` margin: 0 ${space(1)}; `;