Skip to content

Commit

Permalink
resolve threshold index rebase (#58116)
Browse files Browse the repository at this point in the history
Separate flagged threshold routing has now been completed in [this
PR](#58202)

Most recent updates resolve rebase discrepancies and re-implement the
thresholds list view with appropriate page filters and redirects



https://github.com/getsentry/sentry/assets/6186377/0cd52029-ed07-45a6-a9ce-cf6b5e6b0924
  • Loading branch information
nhsiehgit authored Oct 18, 2023
1 parent f682586 commit 6010125
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 26 deletions.
8 changes: 4 additions & 4 deletions static/app/views/releases/list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,10 @@ class ReleasesList extends DeprecatedAsyncView<Props, State> {

const endpoints: ReturnType<DeprecatedAsyncView['getEndpoints']> = [
[
'releases',
`/organizations/${organization.slug}/releases/`,
{query},
{disableEntireQuery: true},
'releases', // stateKey
`/organizations/${organization.slug}/releases/`, // endpoint
{query}, // params
{disableEntireQuery: true}, // options
],
];

Expand Down
176 changes: 160 additions & 16 deletions static/app/views/releases/thresholdsList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,180 @@
import {useEffect} from 'react';
import {RouteComponentProps} from 'react-router';
import {useEffect, useMemo} from 'react';
import styled from '@emotion/styled';

// import {Organization, PageFilters, Project} from 'sentry/types';
import GuideAnchor from 'sentry/components/assistant/guideAnchor';
import EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
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 PageFilterBar from 'sentry/components/organizations/pageFilterBar';
import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
import PanelTable from 'sentry/components/panels/panelTable';
import ProjectPageFilter from 'sentry/components/projectPageFilter';
// import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
import {t} from 'sentry/locale';
// import ConfigStore from 'sentry/stores/configStore';
import {space} from 'sentry/styles/space';
// import {Project} from 'sentry/types';
import useOrganization from 'sentry/utils/useOrganization';

// import usePageFilters from 'sentry/utils/usePageFilters';
import usePageFilters from 'sentry/utils/usePageFilters';
// import useProjects from 'sentry/utils/useProjects';
// import useRouter from 'sentry/utils/useRouter';
import useRouter from 'sentry/utils/useRouter';

import Header from '../components/header';
import {Threshold} from '../utils/types';
import useFetchThresholdsListData from '../utils/useFetchThresholdsListData';

type RouteParams = {
orgId: string;
};
import {ThresholdGroupRow} from './thresholdGroupRow';

type Props = RouteComponentProps<RouteParams, {}> & {};
type Props = {};

function ReleaseThresholdList({router}: Props) {
function ReleaseThresholdList({}: Props) {
const router = useRouter();
const organization = useOrganization();
useEffect(() => {
const hasV2ReleaseUIEnabled = organization.features.includes('release-ui-v2');
if (!hasV2ReleaseUIEnabled) {
router.replace('/releases/');
}
}, [router, organization]);
// const {projects, initiallyLoaded: projectsLoaded} = useProjects();
// const {selection, isReady, desyncedFilters} = usePageFilters();
// const {projects} = useProjects();
const {selection} = usePageFilters();
const {
data: thresholds = [],
error,
isLoading,
isError,
refetch,
} = useFetchThresholdsListData({
selectedProjectIds: selection.projects,
});

// const _getAllSelectedProjects = (): Project[] => {
// return projects.filter(project =>
// selection.projects.some(id => String(id) === project.id || id === -1)
// );
// };

// const _getAllEnvironments = (): string[] => {
// const selectedProjects = 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 if the user is a superuser
// * the requested projects
// * all member projects if 'my projects' (empty list) is selected.
// * all projects if -1 is the only selected project.
// */
// if (
// (selectedProjects.length === 1 &&
// selectedProjects[0] === ALL_ACCESS_PROJECTS &&
// project.hasAccess) ||
// (selectedProjects.length === 0 && (project.isMember || user.isSuperuser)) ||
// selectedProjects.includes(project.id)
// ) {
// return project.environments;
// }

// return [];
// })
// );
// const envDiff = new Set([...allEnvSet].filter(x => !unSortedEnvs.has(x)));

// return Array.from(unSortedEnvs)
// .sort()
// .concat([...envDiff].sort());
// };

// NOTE: currently no way to filter for 'None' environments
const filteredThresholds = selection.environments.length
? thresholds.filter(
threshold => selection.environments.indexOf(threshold.environment.name) > -1
)
: thresholds;

const thresholdGroups: {[key: string]: {[key: string]: Threshold[]}} = useMemo(() => {
const byProj = {};
filteredThresholds.forEach(threshold => {
const projId = threshold.project.id;
if (!byProj[projId]) {
byProj[projId] = {};
}
const env = threshold.environment.name;
if (!byProj[projId][env]) {
byProj[projId][env] = [];
}
byProj[projId][env].push(threshold);
});
return byProj;
}, [filteredThresholds]);

if (isError) {
return <LoadingError onRetry={refetch} message={error.message} />;
}
if (isLoading) {
return <LoadingIndicator />;
}

return (
<div>
<Header router={router} hasV2ReleaseUIEnabled />
</div>
<PageFiltersContainer>
<NoProjectMessage organization={organization}>
<Header router={router} hasV2ReleaseUIEnabled />
<Layout.Body>
<Layout.Main fullWidth>
<ReleaseThresholdsPageFilterBar condensed>
<GuideAnchor target="release_projects">
<ProjectPageFilter />
</GuideAnchor>
<EnvironmentPageFilter />
</ReleaseThresholdsPageFilterBar>
<StyledPanelTable
isLoading={isLoading}
isEmpty={filteredThresholds.length === 0 && !isError}
emptyMessage={t('No thresholds found.')}
headers={[
t('Project Name'),
t('Environment'),
t('Window'),
t('Conditions'),
t('Actions'),
]}
>
{thresholdGroups &&
Object.entries(thresholdGroups).map(([projId, byEnv]) => {
return Object.entries(byEnv).map(([envName, thresholdGroup]) => (
<ThresholdGroupRow
key={`${projId}-${envName}`}
thresholds={thresholdGroup}
/>
));
})}
</StyledPanelTable>
</Layout.Main>
</Layout.Body>
</NoProjectMessage>
</PageFiltersContainer>
);
}

export default ReleaseThresholdList;

const StyledPanelTable = styled(PanelTable)`
@media (min-width: ${p => p.theme.breakpoints.small}) {
overflow: initial;
}
grid-template-columns:
minmax(150px, 1fr) minmax(150px, 1fr) minmax(150px, 1fr) minmax(250px, 4fr)
auto;
white-space: nowrap;
font-size: ${p => p.theme.fontSizeMedium};
`;

const ReleaseThresholdsPageFilterBar = styled(PageFilterBar)`
margin-bottom: ${space(2)};
`;
80 changes: 80 additions & 0 deletions static/app/views/releases/thresholdsList/thresholdGroupRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {Fragment} from 'react';
import styled from '@emotion/styled';

import {DropdownMenu, MenuItemProps} from 'sentry/components/dropdownMenu';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import {IconEllipsis} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {getExactDuration} from 'sentry/utils/formatters';

import {Threshold} from '../utils/types';

type Props = {
thresholds: {[key: string]: any};
};

export function ThresholdGroupRow({thresholds}: Props) {
const actions: MenuItemProps[] = [
{
key: 'edit',
label: t('Edit'),
onAction: () => {},
},
{
key: 'delete',
label: t('Delete'),
priority: 'danger',
onAction: () => {
// console.log('oops');
},
},
];

return thresholds.map((threshold: Threshold, idx: number) => (
<Fragment key={idx}>
<FlexCenter>
{idx === 0 ? (
<ProjectBadge
project={threshold.project}
avatarSize={16}
hideOverflow
disableLink
/>
) : (
''
)}
</FlexCenter>
<FlexCenter>{idx === 0 ? threshold.environment.name || 'None' : ''}</FlexCenter>
<FlexCenter>{getExactDuration(threshold.window_in_seconds)}</FlexCenter>
<FlexCenter>
{threshold.trigger_type === 'over' ? '>' : '<'} {threshold.threshold_type}
</FlexCenter>
<ActionsColumn>
<DropdownMenu
items={actions}
position="bottom-end"
triggerProps={{
'aria-label': t('Actions'),
size: 'xs',
icon: <IconEllipsis size="xs" />,
showChevron: false,
}}
// disabledKeys={hasAccess && canEdit ? [] : ['delete']}
/>
</ActionsColumn>
</Fragment>
));
}

const FlexCenter = styled('div')`
display: flex;
align-items: center;
`;

const ActionsColumn = styled('div')`
display: flex;
align-items: center;
justify-content: center;
padding: ${space(1)};
`;
12 changes: 6 additions & 6 deletions static/app/views/releases/utils/useFetchThresholdsListData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import useOrganization from 'sentry/utils/useOrganization';
import {Threshold, ThresholdQuery} from './types';

export type HookProps = {
selectedEnvs: string[];
selectedProjects: number[];
selectedEnvs?: string[];
selectedProjectIds?: number[];
};

export default function useFetchThresholdsListData({
selectedEnvs,
selectedProjects,
selectedEnvs = [],
selectedProjectIds = [],
}: HookProps) {
const organization = useOrganization();

const query: ThresholdQuery = {};
if (selectedProjects.length) {
query.project = selectedProjects;
if (selectedProjectIds.length) {
query.project = selectedProjectIds;
} else {
query.project = [-1];
}
Expand Down

0 comments on commit 6010125

Please sign in to comment.