Ogi 1 год назад
Родитель
Сommit
8c6d328e6e

+ 24 - 3
static/app/utils/metrics/useMetricsTags.tsx

@@ -1,13 +1,19 @@
 import type {MRI, PageFilters} from 'sentry/types';
 import {getUseCaseFromMRI} from 'sentry/utils/metrics/mri';
 import type {MetricTag} from 'sentry/utils/metrics/types';
+import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
 import {useApiQuery} from 'sentry/utils/queryClient';
 import useOrganization from 'sentry/utils/useOrganization';
 
-export function useMetricsTags(mri: MRI | undefined, projects: PageFilters['projects']) {
+export function useMetricsTags(
+  mri: MRI | undefined,
+  projects: PageFilters['projects'],
+  filterBlockedTags = true
+) {
   const {slug} = useOrganization();
-  const useCase = getUseCaseFromMRI(mri);
-  return useApiQuery<MetricTag[]>(
+  const useCase = getUseCaseFromMRI(mri) ?? 'custom';
+
+  const tagsQuery = useApiQuery<MetricTag[]>(
     [
       `/organizations/${slug}/metrics/tags/`,
       {query: {metric: mri, useCase, project: projects}},
@@ -17,4 +23,19 @@ export function useMetricsTags(mri: MRI | undefined, projects: PageFilters['proj
       staleTime: Infinity,
     }
   );
+
+  const metricMeta = useMetricsMeta(projects, [useCase], false);
+  const blockedTags =
+    metricMeta.data
+      ?.find(meta => meta.mri === mri)
+      ?.blockingStatus?.flatMap(s => s.blockedTags) ?? [];
+
+  if (!filterBlockedTags) {
+    return tagsQuery;
+  }
+
+  return {
+    ...tagsQuery,
+    data: tagsQuery.data?.filter(tag => !blockedTags.includes(tag.key)) ?? [],
+  };
 }

+ 44 - 0
static/app/views/settings/projectMetrics/access.tsx

@@ -0,0 +1,44 @@
+import type {Project, Scope, Team} from 'sentry/types';
+import useOrganization from 'sentry/utils/useOrganization';
+import {useUser} from 'sentry/utils/useUser';
+
+import {hasEveryAccess} from '../../../components/acl/access';
+
+type Props = {
+  /**
+   * List of required access levels
+   */
+  access?: Scope[];
+  /**
+   * Optional: To be used when you need to check for access to the Project
+   *
+   * E.g. On the project settings page, the user will need project:write.
+   * An "org-member" does not have project:write but if they are "team-admin" for
+   * of a parent team, they will have appropriate scopes.
+   */
+  project?: Project | null | undefined;
+  /**
+   * Optional: To be used when you need to check for access to the Team
+   *
+   * E.g. On the team settings page, the user will need team:write.
+   * An "org-member" does not have team:write but if they are "team-admin" for
+   * the team, they will have appropriate scopes.
+   */
+  team?: Team | null | undefined;
+};
+
+export function useAccess({access = [], team, project}: Props) {
+  const user = useUser();
+  const organization = useOrganization();
+
+  team = team ?? undefined;
+  project = project ?? undefined;
+
+  const hasAccess = hasEveryAccess(access, {organization, team, project});
+  const hasSuperuser = !!(user && user.isSuperuser);
+
+  return {
+    hasAccess,
+    hasSuperuser,
+  };
+}

+ 27 - 14
static/app/views/settings/projectMetrics/blockButton.tsx

@@ -1,32 +1,45 @@
 import type {BaseButtonProps} from 'sentry/components/button';
 import {Button} from 'sentry/components/button';
 import Confirm from 'sentry/components/confirm';
+import {Tooltip} from 'sentry/components/tooltip';
 import {IconNot, IconPlay} from 'sentry/icons';
 import {t} from 'sentry/locale';
 
-export interface BlockButtonProps extends BaseButtonProps {
+import type {OpenConfirmOptions} from '../../../components/confirm';
+
+export interface BlockButtonProps
+  extends BaseButtonProps,
+    Pick<OpenConfirmOptions, 'message'> {
+  hasAccess: boolean;
   isBlocked: boolean;
   onConfirm: () => void;
 }
 
-export function BlockMetricButton({isBlocked, onConfirm, ...props}: BlockButtonProps) {
+export function BlockButton({isBlocked, onConfirm, ...props}: BlockButtonProps) {
+  const button = (
+    <Button
+      {...props}
+      icon={isBlocked ? <IconPlay size="xs" /> : <IconNot size="xs" />}
+      disabled={!props.hasAccess || props.disabled}
+    >
+      {isBlocked ? t('Unblock') : t('Block')}
+    </Button>
+  );
+
   return (
     <Confirm
       priority="danger"
       onConfirm={onConfirm}
-      confirmText={isBlocked ? t('Unblock Metric') : t('Block Metric')}
-      message={
-        isBlocked
-          ? t('Are you sure you want to unblock this metric?')
-          : t('Are you sure you want to block this metric?')
-      }
+      message={props.message}
+      confirmText={isBlocked ? t('Unblock') : t('Block')}
     >
-      <Button
-        icon={isBlocked ? <IconPlay size="xs" /> : <IconNot size="xs" />}
-        {...props}
-      >
-        {isBlocked ? t('Unblock') : t('Block')}
-      </Button>
+      {props.hasAccess ? (
+        button
+      ) : (
+        <Tooltip title={t('You do not have permissions to edit metrics.')}>
+          {button}
+        </Tooltip>
+      )}
     </Confirm>
   );
 }

+ 63 - 37
static/app/views/settings/projectMetrics/projectMetrics.tsx

@@ -29,7 +29,8 @@ import {useMetricsOnboardingSidebar} from 'sentry/views/ddm/ddmOnboarding/useMet
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
 import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
-import {BlockMetricButton} from 'sentry/views/settings/projectMetrics/blockButton';
+import {useAccess} from 'sentry/views/settings/projectMetrics/access';
+import {BlockButton} from 'sentry/views/settings/projectMetrics/blockButton';
 
 type Props = {
   organization: Organization;
@@ -152,14 +153,22 @@ interface MetricsTableProps {
 
 function MetricsTable({metrics, isLoading, query, project}: MetricsTableProps) {
   const blockMetricMutation = useBlockMetric(project);
+  const {hasAccess} = useAccess({access: ['project:write']});
 
   return (
     <StyledPanelTable
       headers={[
         t('Metric'),
-        <RightAligned key="type"> {t('Type')}</RightAligned>,
-        <RightAligned key="unit">{t('Unit')}</RightAligned>,
-        <RightAligned key="actions">{t('Actions')}</RightAligned>,
+        <Cell right key="type">
+          {' '}
+          {t('Type')}
+        </Cell>,
+        <Cell right key="unit">
+          {t('Unit')}
+        </Cell>,
+        <Cell right key="actions">
+          {t('Actions')}
+        </Cell>,
       ]}
       emptyMessage={
         query
@@ -169,36 +178,50 @@ function MetricsTable({metrics, isLoading, query, project}: MetricsTableProps) {
       isEmpty={metrics.length === 0}
       isLoading={isLoading}
     >
-      {metrics.map(({mri, type, unit, blockingStatus}) => (
-        <Fragment key={mri}>
-          <Link
-            to={`/settings/projects/${project.slug}/metrics/${encodeURIComponent(mri)}`}
-          >
-            {middleEllipsis(formatMRI(mri), 65, /\.|-|_/)}
-          </Link>
-          <RightAligned>
-            <Tag>{getReadableMetricType(type)}</Tag>
-          </RightAligned>
-          <RightAligned>
-            <Tag>{unit}</Tag>
-          </RightAligned>
-          <RightAligned>
-            <BlockMetricButton
-              size="xs"
-              isBlocked={blockingStatus[0]?.isBlocked}
-              aria-label={t('Block Metric')}
-              onConfirm={() => {
-                blockMetricMutation.mutate({
-                  mri,
-                  operationType: blockingStatus[0]?.isBlocked
-                    ? 'unblockMetric'
-                    : 'blockMetric',
-                });
-              }}
-            />
-          </RightAligned>
-        </Fragment>
-      ))}
+      {metrics.map(({mri, type, unit, blockingStatus}) => {
+        const isBlocked = blockingStatus[0]?.isBlocked;
+        return (
+          <Fragment key={mri}>
+            <Cell>
+              <Link
+                to={`/settings/projects/${project.slug}/metrics/${encodeURIComponent(
+                  mri
+                )}`}
+              >
+                {middleEllipsis(formatMRI(mri), 65, /\.|-|_/)}
+              </Link>
+            </Cell>
+            <Cell right>
+              <Tag>{getReadableMetricType(type)}</Tag>
+            </Cell>
+            <Cell right>
+              <Tag>{unit}</Tag>
+            </Cell>
+            <Cell right>
+              <BlockButton
+                size="xs"
+                hasAccess={hasAccess}
+                disabled={blockMetricMutation.isLoading}
+                isBlocked={isBlocked}
+                aria-label={t('Block Metric')}
+                message={
+                  isBlocked
+                    ? t('Are you sure you want to unblock this metric?')
+                    : t(
+                        'Are you sure you want to block this metric? It will no longer be ingested, and will not be available for use in Metrics, Alerts, or Dashboards.'
+                      )
+                }
+                onConfirm={() => {
+                  blockMetricMutation.mutate({
+                    mri,
+                    operationType: isBlocked ? 'unblockMetric' : 'blockMetric',
+                  });
+                }}
+              />
+            </Cell>
+          </Fragment>
+        );
+      })}
     </StyledPanelTable>
   );
 }
@@ -213,11 +236,14 @@ const SearchWrapper = styled('div')`
 
 const StyledPanelTable = styled(PanelTable)`
   grid-template-columns: 1fr repeat(3, minmax(115px, min-content));
-  align-items: center;
+
 `;
 
-const RightAligned = styled('div')`
-  text-align: right;
+const Cell = styled('div')<{right?: boolean}>`
+  display: flex;
+  align-items: center;
+  align-self: stretch;
+  justify-content: ${p => (p.right ? 'flex-end' : 'flex-start')};
 `;
 
 export default ProjectMetrics;

+ 15 - 31
static/app/views/settings/projectMetrics/projectMetricsDetails.tsx

@@ -2,9 +2,8 @@ import {Fragment, useCallback} from 'react';
 import type {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
 
-import {Button, LinkButton} from 'sentry/components/button';
+import {LinkButton} from 'sentry/components/button';
 import MiniBarChart from 'sentry/components/charts/miniBarChart';
-import Confirm from 'sentry/components/confirm';
 import EmptyMessage from 'sentry/components/emptyMessage';
 import FieldGroup from 'sentry/components/forms/fieldGroup';
 import Panel from 'sentry/components/panels/panel';
@@ -14,7 +13,6 @@ import PanelTable from 'sentry/components/panels/panelTable';
 import Placeholder from 'sentry/components/placeholder';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {CHART_PALETTE} from 'sentry/constants/chartPalette';
-import {IconNot, IconPlay} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {
@@ -34,10 +32,8 @@ import {useMetricsTags} from 'sentry/utils/metrics/useMetricsTags';
 import routeTitleGen from 'sentry/utils/routeTitle';
 import {CodeLocations} from 'sentry/views/ddm/codeLocations';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
-import {
-  type BlockButtonProps,
-  BlockMetricButton,
-} from 'sentry/views/settings/projectMetrics/blockButton';
+import {useAccess} from 'sentry/views/settings/projectMetrics/access';
+import {BlockButton} from 'sentry/views/settings/projectMetrics/blockButton';
 import {TextAlignRight} from 'sentry/views/starfish/components/textAlign';
 
 import {useProjectMetric} from '../../../utils/metrics/useMetricsMeta';
@@ -75,6 +71,7 @@ function ProjectMetricsDetails({project, params, organization}: Props) {
 
   const isBlockedMetric = blockingStatus?.isBlocked ?? false;
   const blockMetricMutation = useBlockMetric(project);
+  const {hasAccess} = useAccess({access: ['project:write']});
 
   const {type, name, unit} = parseMRI(mri) ?? {};
   const operation = getSettingsOperationForType(type ?? 'c');
@@ -133,8 +130,9 @@ function ProjectMetricsDetails({project, params, organization}: Props) {
         title={t('Metric Details')}
         action={
           <Controls>
-            <BlockMetricButton
+            <BlockButton
               size="sm"
+              hasAccess={hasAccess}
               disabled={blockMetricMutation.isLoading}
               isBlocked={isBlockedMetric}
               onConfirm={handleMetricBlockToggle}
@@ -232,12 +230,20 @@ function ProjectMetricsDetails({project, params, organization}: Props) {
             <Fragment key={key}>
               <div key={key}>{key}</div>
               <TextAlignRight key={key}>
-                <BlockTagButton
+                <BlockButton
                   size="xs"
+                  hasAccess={hasAccess}
                   disabled={blockMetricMutation.isLoading || isBlockedMetric}
                   isBlocked={isBlockedTag}
                   onConfirm={() => handleMetricTagBlockToggle(key)}
                   aria-label={t('Block tag')}
+                  message={
+                    isBlockedTag
+                      ? t('Are you sure you want to unblock this tag?')
+                      : t(
+                          'Are you sure you want to block this tag? It will no longer be ingested, and will not be available for use in Metrics, Alerts, or Dashboards.'
+                        )
+                  }
                 />
               </TextAlignRight>
             </Fragment>
@@ -255,28 +261,6 @@ function ProjectMetricsDetails({project, params, organization}: Props) {
   );
 }
 
-function BlockTagButton({isBlocked, onConfirm, ...props}: BlockButtonProps) {
-  return (
-    <Confirm
-      priority="danger"
-      onConfirm={onConfirm}
-      confirmText={isBlocked ? t('Unblock Tag') : t('Block Tag')}
-      message={
-        isBlocked
-          ? t('Are you sure you want to unblock this tag?')
-          : t('Are you sure you want to block this tag?')
-      }
-    >
-      <Button
-        icon={isBlocked ? <IconPlay size="xs" /> : <IconNot size="xs" />}
-        {...props}
-      >
-        {isBlocked ? t('Unblock') : t('Block')}
-      </Button>
-    </Confirm>
-  );
-}
-
 const TableHeading = styled('div')`
   color: ${p => p.theme.textColor};
 `;