Browse Source

Revert revert resolve threshold index (#58371)

reimplements: https://github.com/getsentry/sentry/pull/58116

with proper page filter imports
Nathan Hsieh 1 year ago
parent
commit
e9e52affbc

+ 4 - 4
static/app/views/releases/list/index.tsx

@@ -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
       ],
     ];
 

+ 160 - 16
static/app/views/releases/thresholdsList/index.tsx

@@ -1,21 +1,36 @@
-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 * 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 PanelTable from 'sentry/components/panels/panelTable';
+// 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');
@@ -23,14 +38,143 @@ function ReleaseThresholdList({router}: Props) {
       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 - 0
static/app/views/releases/thresholdsList/thresholdGroupRow.tsx

@@ -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)};
+`;

+ 6 - 6
static/app/views/releases/utils/useFetchThresholdsListData.tsx

@@ -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];
   }