mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-10-30 14:49:14 +00:00
308 lines
11 KiB
TypeScript
308 lines
11 KiB
TypeScript
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<string>('');
|
|
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 <LoadingError onRetry={refetch} message={requestError.message} />;
|
|
}
|
|
if (isLoading) {
|
|
return <LoadingIndicator />;
|
|
}
|
|
|
|
return (
|
|
<PageFiltersContainer>
|
|
<NoProjectMessage organization={organization}>
|
|
<Header router={router} hasV2ReleaseUIEnabled organization={organization} />
|
|
<Layout.Body>
|
|
<Layout.Main fullWidth>
|
|
<FilterRow>
|
|
<ReleaseThresholdsPageFilterBar condensed>
|
|
<GuideAnchor target="release_projects">
|
|
<ProjectPageFilter />
|
|
</GuideAnchor>
|
|
<EnvironmentPageFilter />
|
|
</ReleaseThresholdsPageFilterBar>
|
|
<ListError>{listError}</ListError>
|
|
</FilterRow>
|
|
{thresholdsByProject &&
|
|
Object.entries(thresholdsByProject).map(([projId, thresholdsByProj]) => (
|
|
<ThresholdGroupTable
|
|
key={projId}
|
|
project={projectsById[projId]}
|
|
thresholds={thresholdsByProj}
|
|
isLoading={isLoading}
|
|
isError={isError}
|
|
refetch={refetch}
|
|
setTempError={setTempError}
|
|
allEnvironmentNames={getEnvironmentsAvailableToProject}
|
|
/>
|
|
))}
|
|
{projectsWithoutThresholds.length > 0 && (
|
|
<div>
|
|
<strong>Projects without Thresholds</strong>
|
|
{projectsWithoutThresholds
|
|
.slice(
|
|
PAGE_SIZE * newProjThresholdsPage,
|
|
PAGE_SIZE * newProjThresholdsPage + PAGE_SIZE
|
|
)
|
|
.map(proj => (
|
|
<NoThresholdCard
|
|
key={proj.id}
|
|
project={proj}
|
|
allEnvironmentNames={getAllEnvironmentNames}
|
|
refetch={refetch}
|
|
setTempError={setTempError}
|
|
/>
|
|
))}
|
|
<Paginator>
|
|
<Button
|
|
icon={<IconChevron direction="left" size="sm" />}
|
|
aria-label={t('Previous')}
|
|
size="sm"
|
|
disabled={newProjThresholdsPage === 0}
|
|
onClick={() => {
|
|
setNewProjThresholdsPage(newProjThresholdsPage - 1);
|
|
}}
|
|
/>
|
|
<CurrentPage>
|
|
{newProjThresholdsPage + 1} of{' '}
|
|
{Math.ceil(projectsWithoutThresholds.length / PAGE_SIZE)}
|
|
</CurrentPage>
|
|
<Button
|
|
icon={<IconChevron direction="right" size="sm" />}
|
|
aria-label={t('Next')}
|
|
size="sm"
|
|
disabled={
|
|
PAGE_SIZE * newProjThresholdsPage + PAGE_SIZE >=
|
|
projectsWithoutThresholds.length
|
|
}
|
|
onClick={() => {
|
|
setNewProjThresholdsPage(newProjThresholdsPage + 1);
|
|
}}
|
|
/>
|
|
</Paginator>
|
|
</div>
|
|
)}
|
|
</Layout.Main>
|
|
</Layout.Body>
|
|
</NoProjectMessage>
|
|
</PageFiltersContainer>
|
|
);
|
|
}
|
|
|
|
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)};
|
|
`;
|