diff --git a/eslint.config.ts b/eslint.config.ts index 436c1052305290..5f07bed897c269 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -462,6 +462,7 @@ export default typescript.config([ '@sentry/no-flag-comments': 'error', '@sentry/no-static-translations': 'error', '@sentry/no-styled-shortcut': 'error', + '@sentry/no-unnecessary-use-callback': 'error', }, }, { diff --git a/static/app/components/assistant/guideAnchor.tsx b/static/app/components/assistant/guideAnchor.tsx index 9660f81cfa716b..9c056b1bde5559 100644 --- a/static/app/components/assistant/guideAnchor.tsx +++ b/static/app/components/assistant/guideAnchor.tsx @@ -105,16 +105,6 @@ function BaseGuideAnchor({ [onStepComplete] ); - const handleDismiss = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - if (currentGuide) { - dismissGuide(currentGuide.guide, step, orgId); - } - }, - [currentGuide, orgId, step] - ); - if (!active) { return children ? children : null; } @@ -133,7 +123,10 @@ function BaseGuideAnchor({ stepCount={currentStepCount} stepTotal={totalStepCount} handleDismiss={e => { - handleDismiss(e); + e.stopPropagation(); + if (currentGuide) { + dismissGuide(currentGuide.guide, step, orgId); + } window.location.hash = ''; }} actions={ diff --git a/static/app/components/avatarChooser/useUploader.tsx b/static/app/components/avatarChooser/useUploader.tsx index fcdf1099052ae6..3809a23754d5e7 100644 --- a/static/app/components/avatarChooser/useUploader.tsx +++ b/static/app/components/avatarChooser/useUploader.tsx @@ -41,34 +41,31 @@ export function useUploader({onSelect, minImageSize}: UseUploaderOptions) { [minImageSize] ); - const onSelectFile = useCallback( - async (ev: React.ChangeEvent) => { - const file = ev.target.files?.[0]; + const onSelectFile = async (ev: React.ChangeEvent) => { + const file = ev.target.files?.[0]; - // No file selected (e.g. user clicked "cancel") - if (!file) { - return; - } + // No file selected (e.g. user clicked "cancel") + if (!file) { + return; + } - if (!/^image\//.test(file.type)) { - addErrorMessage(t('That is not a supported file type.')); - return; - } - const url = window.URL.createObjectURL(file); - const {height, width} = await getImageHeightAndWidth(url); - const sizeValidation = validateSize(height, width); + if (!/^image\//.test(file.type)) { + addErrorMessage(t('That is not a supported file type.')); + return; + } + const url = window.URL.createObjectURL(file); + const {height, width} = await getImageHeightAndWidth(url); + const sizeValidation = validateSize(height, width); - if (sizeValidation !== true) { - addErrorMessage(sizeValidation); - return; - } + if (sizeValidation !== true) { + addErrorMessage(sizeValidation); + return; + } - setObjectUrl(url); - onSelect(url); - ev.target.value = ''; - }, - [onSelect, validateSize] - ); + setObjectUrl(url); + onSelect(url); + ev.target.value = ''; + }; const fileInput = ( ) => { - if (!wrapperRef.current) { - throw new Error('Cannot reveal clipped box without a wrapper ref'); - } - - event.stopPropagation(); - - revealAndDisconnectObserver({ - contentRef, - wrapperRef, - revealRef, - observerRef, - clipHeight, - prefersReducedMotion: prefersReducedMotion ?? true, - }); - if (typeof onReveal === 'function') { - onReveal(); + const handleReveal = (event: React.MouseEvent) => { + if (!wrapperRef.current) { + throw new Error('Cannot reveal clipped box without a wrapper ref'); + } + + event.stopPropagation(); + + revealAndDisconnectObserver({ + contentRef, + wrapperRef, + revealRef, + observerRef, + clipHeight, + prefersReducedMotion: prefersReducedMotion ?? true, + }); + if (typeof onReveal === 'function') { + onReveal(); + } + + setClipped(false); + }; + + const handleCollapse = (event: React.MouseEvent) => { + event.stopPropagation(); + + if (wrapperRef.current && contentRef.current) { + if (prefersReducedMotion) { + wrapperRef.current.style.maxHeight = `${clipHeight}px`; + } else { + const currentHeight = + contentRef.current.clientHeight + calculateAddedHeight({wrapperRef}); + wrapperRef.current.style.maxHeight = `${currentHeight}px`; + void wrapperRef.current.offsetHeight; + wrapperRef.current.style.maxHeight = `${clipHeight}px`; } - - setClipped(false); - }, - [clipHeight, onReveal, prefersReducedMotion] - ); - - const handleCollapse = useCallback( - (event: React.MouseEvent) => { - event.stopPropagation(); - - if (wrapperRef.current && contentRef.current) { - if (prefersReducedMotion) { - wrapperRef.current.style.maxHeight = `${clipHeight}px`; - } else { - const currentHeight = - contentRef.current.clientHeight + calculateAddedHeight({wrapperRef}); - wrapperRef.current.style.maxHeight = `${currentHeight}px`; - void wrapperRef.current.offsetHeight; - wrapperRef.current.style.maxHeight = `${clipHeight}px`; - } - } - revealRef.current = false; - setClipped(true); - }, - [clipHeight, prefersReducedMotion] - ); + } + revealRef.current = false; + setClipped(true); + }; const onWrapperRef = useCallback( (node: HTMLDivElement | null) => { diff --git a/static/app/components/core/input/numberDragInput.tsx b/static/app/components/core/input/numberDragInput.tsx index 4d106b844670c0..3a6e3059630579 100644 --- a/static/app/components/core/input/numberDragInput.tsx +++ b/static/app/components/core/input/numberDragInput.tsx @@ -78,40 +78,34 @@ export function NumberDragInput({ document.removeEventListener('pointerup', onPointerUp); }, [onPointerMove]); - const onPointerDown = useCallback( - (event: React.PointerEvent) => { - if (event.button !== 0) { - return; - } - - // Request pointer lock and add move and pointer up handlers - // that release the lock and cleanup handlers - event.currentTarget.requestPointerLock(); - document.addEventListener('pointermove', onPointerMove); - document.addEventListener('pointerup', onPointerUp); - }, - [onPointerMove, onPointerUp] - ); + const onPointerDown = (event: React.PointerEvent) => { + if (event.button !== 0) { + return; + } + + // Request pointer lock and add move and pointer up handlers + // that release the lock and cleanup handlers + event.currentTarget.requestPointerLock(); + document.addEventListener('pointermove', onPointerMove); + document.addEventListener('pointerup', onPointerUp); + }; const onKeyDownProp = props.onKeyDown; - const onKeyDown = useCallback( - (event: React.KeyboardEvent) => { - onKeyDownProp?.(event); - - if (!inputRef.current || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')) { - return; - } - - event.preventDefault(); - const value = parseFloat(inputRef.current.value); - const step = parseFloat(props.step?.toString() ?? '1'); - const min = props.min ?? Number.NEGATIVE_INFINITY; - const max = props.max ?? Number.POSITIVE_INFINITY; - const newValue = clamp(value + (event.key === 'ArrowUp' ? step : -step), min, max); - setInputValueAndDispatchChange(inputRef.current, newValue.toString()); - }, - [onKeyDownProp, props.min, props.max, props.step] - ); + const onKeyDown = (event: React.KeyboardEvent) => { + onKeyDownProp?.(event); + + if (!inputRef.current || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')) { + return; + } + + event.preventDefault(); + const value = parseFloat(inputRef.current.value); + const step = parseFloat(props.step?.toString() ?? '1'); + const min = props.min ?? Number.NEGATIVE_INFINITY; + const max = props.max ?? Number.POSITIVE_INFINITY; + const newValue = clamp(value + (event.key === 'ArrowUp' ? step : -step), min, max); + setInputValueAndDispatchChange(inputRef.current, newValue.toString()); + }; return ( diff --git a/static/app/components/core/inspector.tsx b/static/app/components/core/inspector.tsx index ba7557cd8c1cf4..c756aff7c81a5b 100644 --- a/static/app/components/core/inspector.tsx +++ b/static/app/components/core/inspector.tsx @@ -282,25 +282,22 @@ export function SentryComponentInspector() { }, [state.trace]); const {ref: contextMenuRef, ...contextMenuProps} = {...contextMenu.getMenuProps()}; - const positionContextMenuOnMountRef = useCallback( - (ref: HTMLDivElement | null) => { - contextMenuRef(ref); - - if (ref) { - const position = computeTooltipPosition( - { - x: tooltipPositionRef.current?.mouse.x ?? 0, - y: tooltipPositionRef.current?.mouse.y ?? 0, - }, - ref - ); + const positionContextMenuOnMountRef = (ref: HTMLDivElement | null) => { + contextMenuRef(ref); + + if (ref) { + const position = computeTooltipPosition( + { + x: tooltipPositionRef.current?.mouse.x ?? 0, + y: tooltipPositionRef.current?.mouse.y ?? 0, + }, + ref + ); - ref.style.left = `${position.left}px`; - ref.style.top = `${position.top}px`; - } - }, - [contextMenuRef] - ); + ref.style.left = `${position.left}px`; + ref.style.top = `${position.top}px`; + } + }; const storybookFiles = useStoryBookFiles(); const storybookFilesLookup = useMemo( diff --git a/static/app/components/events/autofix/autofixStepFeedback.tsx b/static/app/components/events/autofix/autofixStepFeedback.tsx index 8d2a026baaa4c8..648ab698579b5e 100644 --- a/static/app/components/events/autofix/autofixStepFeedback.tsx +++ b/static/app/components/events/autofix/autofixStepFeedback.tsx @@ -1,7 +1,7 @@ -import {useCallback, useState} from 'react'; +import {useState} from 'react'; -import {Button} from '@sentry/scraps/button'; import type {ButtonProps} from '@sentry/scraps/button'; +import {Button} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; @@ -34,27 +34,24 @@ export function AutofixStepFeedback({ const organization = useOrganization(); const user = useUser(); - const handleFeedback = useCallback( - (positive: boolean, e?: React.MouseEvent) => { - if (onFeedbackClick && e) { - onFeedbackClick(e); - } + const handleFeedback = (positive: boolean, e?: React.MouseEvent) => { + if (onFeedbackClick && e) { + onFeedbackClick(e); + } - const analyticsData = { - step_type: stepType, - positive, - group_id: groupId, - autofix_run_id: runId, - user_id: user.id, - organization, - }; + const analyticsData = { + step_type: stepType, + positive, + group_id: groupId, + autofix_run_id: runId, + user_id: user.id, + organization, + }; - trackAnalytics('seer.autofix.feedback_submitted', analyticsData); + trackAnalytics('seer.autofix.feedback_submitted', analyticsData); - setFeedbackSubmitted(true); - }, - [stepType, groupId, runId, organization, user, onFeedbackClick] - ); + setFeedbackSubmitted(true); + }; if (feedbackSubmitted) { return ( diff --git a/static/app/components/events/autofix/codingAgentIntegrationCta.tsx b/static/app/components/events/autofix/codingAgentIntegrationCta.tsx index c7728e66dc382c..584b095b10dda0 100644 --- a/static/app/components/events/autofix/codingAgentIntegrationCta.tsx +++ b/static/app/components/events/autofix/codingAgentIntegrationCta.tsx @@ -1,5 +1,3 @@ -import {useCallback} from 'react'; - import {Button, LinkButton} from '@sentry/scraps/button'; import {Container, Flex} from '@sentry/scraps/layout'; import {ExternalLink, Link} from '@sentry/scraps/link'; @@ -61,7 +59,7 @@ export function makeCodingAgentIntegrationCta(config: AgentConfig) { const isConfigured = preference?.automation_handoff?.target === config.target && isAutomationEnabled; - const handleInstallClick = useCallback(() => { + const handleInstallClick = () => { trackAnalytics('coding_integration.install_clicked', { organization, project_slug: project.slug, @@ -69,9 +67,9 @@ export function makeCodingAgentIntegrationCta(config: AgentConfig) { source: 'cta', user_id: user.id, }); - }, [organization, project.slug, user.id]); + }; - const handleSetupClick = useCallback(async () => { + const handleSetupClick = async () => { if (!integration?.id) { throw new Error(`${config.displayName} integration not found`); } @@ -104,17 +102,7 @@ export function makeCodingAgentIntegrationCta(config: AgentConfig) { integration_id: parseInt(integration.id, 10), }, }); - }, [ - organization, - project.slug, - project.seerScannerAutomation, - project.autofixAutomationTuning, - updateProjectSeerPreferences, - updateProjectAutomation, - preference?.repositories, - integration, - user.id, - ]); + }; if (!hasFeatureFlag) { return null; diff --git a/static/app/components/events/autofix/githubCopilotIntegrationCta.tsx b/static/app/components/events/autofix/githubCopilotIntegrationCta.tsx index 38f2b214021fe7..2135c51d52c6d0 100644 --- a/static/app/components/events/autofix/githubCopilotIntegrationCta.tsx +++ b/static/app/components/events/autofix/githubCopilotIntegrationCta.tsx @@ -1,5 +1,3 @@ -import {useCallback} from 'react'; - import {LinkButton} from '@sentry/scraps/button'; import {Container, Flex} from '@sentry/scraps/layout'; import {Heading, Text} from '@sentry/scraps/text'; @@ -21,7 +19,7 @@ export function GithubCopilotIntegrationCta() { organizationIntegrationsCodingAgents(organization) ); - const handleInstallClick = useCallback(() => { + const handleInstallClick = () => { trackAnalytics('coding_integration.install_clicked', { organization, project_slug: '', // GitHub Copilot CTA is not project-specific @@ -29,7 +27,7 @@ export function GithubCopilotIntegrationCta() { source: 'cta', user_id: user.id, }); - }, [organization, user.id]); + }; const githubCopilotIntegration = codingAgentIntegrations?.integrations.find( integration => integration.provider === 'github_copilot' diff --git a/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx b/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx index 54b4a3f39eb8e1..a725ae20a85378 100644 --- a/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx +++ b/static/app/components/events/breadcrumbs/breadcrumbsDataSection.tsx @@ -1,4 +1,4 @@ -import {useCallback, useMemo, useRef, useState} from 'react'; +import {useMemo, useRef, useState} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; @@ -79,44 +79,41 @@ export function BreadcrumbsDataSection({ [summaryCrumbs, timeDisplay] ); - const onViewAllBreadcrumbs = useCallback( - (focusControl?: BreadcrumbControlOptions) => { - trackAnalytics('breadcrumbs.issue_details.drawer_opened', { - control: focusControl ?? 'view all', - organization, - }); - openDrawer( - () => ( - - ), - { - ariaLabel: 'breadcrumb drawer', - drawerKey: 'breadcrumbs-drawer', - // We prevent a click on the 'View All' button from closing the drawer so that - // we don't reopen it immediately, and instead let the button handle this itself. - shouldCloseOnInteractOutside: element => { - const viewAllButton = viewAllButtonRef.current; - if (viewAllButton?.contains(element)) { - return false; - } - // Third-party packages (e.g. Pendo) use a container with id "pendo-guide-container". - // If the click is inside that container, treat it as an internal click. - if (element.closest('#pendo-guide-container')) { - return false; - } - return true; - }, - } - ); - }, - [group, event, project, openDrawer, enhancedCrumbs, organization] - ); + const onViewAllBreadcrumbs = (focusControl?: BreadcrumbControlOptions) => { + trackAnalytics('breadcrumbs.issue_details.drawer_opened', { + control: focusControl ?? 'view all', + organization, + }); + openDrawer( + () => ( + + ), + { + ariaLabel: 'breadcrumb drawer', + drawerKey: 'breadcrumbs-drawer', + // We prevent a click on the 'View All' button from closing the drawer so that + // we don't reopen it immediately, and instead let the button handle this itself. + shouldCloseOnInteractOutside: element => { + const viewAllButton = viewAllButtonRef.current; + if (viewAllButton?.contains(element)) { + return false; + } + // Third-party packages (e.g. Pendo) use a container with id "pendo-guide-container". + // If the click is inside that container, treat it as an internal click. + if (element.closest('#pendo-guide-container')) { + return false; + } + return true; + }, + } + ); + }; if (enhancedCrumbs.length === 0) { return null; diff --git a/static/app/components/events/eventTagsAndScreenshot/tags.tsx b/static/app/components/events/eventTagsAndScreenshot/tags.tsx index 0c590297042e5c..a48866ad68acad 100644 --- a/static/app/components/events/eventTagsAndScreenshot/tags.tsx +++ b/static/app/components/events/eventTagsAndScreenshot/tags.tsx @@ -1,4 +1,4 @@ -import {useCallback, useMemo, useState} from 'react'; +import {useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {Grid} from '@sentry/scraps/layout'; @@ -41,9 +41,9 @@ export function EventTagsDataSection({ const sentryTags = getSentryDefaultTags(); const [tagFilter, setTagFilter] = useState(TagFilter.ALL); - const handleTagFilterChange = useCallback((value: TagFilter) => { + const handleTagFilterChange = (value: TagFilter) => { setTagFilter(value); - }, []); + }; const tagsWithMeta = useMemo(() => { return associateTagsWithMeta({tags: event.tags, meta: event._meta?.tags}); diff --git a/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx b/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx index 22ccb1d57d2902..5e4c5c12dda783 100644 --- a/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx +++ b/static/app/components/events/featureFlags/eventFeatureFlagSection.tsx @@ -170,39 +170,36 @@ function BaseEventFeatureFlagList({event, group, project}: EventFeatureFlagSecti }); }, [suspectFlagNames, eventFlags, generateAction]); - const onViewAllFlags = useCallback( - (focusControl?: FlagControlOptions) => { - trackAnalytics('flags.view-all-clicked', { - organization, - }); - openDrawer( - () => ( - - ), - { - ariaLabel: t('Feature flags drawer'), - drawerKey: 'feature-flags-drawer', - // We prevent a click on the 'View All' button from closing the drawer so that - // we don't reopen it immediately, and instead let the button handle this itself. - shouldCloseOnInteractOutside: element => { - const viewAllButton = viewAllButtonRef.current; - if (viewAllButton?.contains(element)) { - return false; - } - return true; - }, - } - ); - }, - [openDrawer, event, group, project, hydratedFlags, organization, orderBy] - ); + const onViewAllFlags = (focusControl?: FlagControlOptions) => { + trackAnalytics('flags.view-all-clicked', { + organization, + }); + openDrawer( + () => ( + + ), + { + ariaLabel: t('Feature flags drawer'), + drawerKey: 'feature-flags-drawer', + // We prevent a click on the 'View All' button from closing the drawer so that + // we don't reopen it immediately, and instead let the button handle this itself. + shouldCloseOnInteractOutside: element => { + const viewAllButton = viewAllButtonRef.current; + if (viewAllButton?.contains(element)) { + return false; + } + return true; + }, + } + ); + }; useEffect(() => { if (hasFlags) { diff --git a/static/app/components/events/highlights/highlightsDataSection.tsx b/static/app/components/events/highlights/highlightsDataSection.tsx index 04ff34a4898181..954a8dc9d16aa0 100644 --- a/static/app/components/events/highlights/highlightsDataSection.tsx +++ b/static/app/components/events/highlights/highlightsDataSection.tsx @@ -1,5 +1,5 @@ -import {useCallback, useMemo, useRef} from 'react'; -import {css, useTheme, type Theme} from '@emotion/react'; +import {useMemo, useRef} from 'react'; +import {css, type Theme, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; @@ -63,7 +63,7 @@ function useOpenEditHighlightsModal({ [isProjectAdmin] ); - const openEditHighlightsModal = useCallback(() => { + const openEditHighlightsModal = () => { trackAnalytics('highlights.issue_details.edit_clicked', {organization}); openModal( deps => ( @@ -78,7 +78,7 @@ function useOpenEditHighlightsModal({ ), {modalCss: highlightModalCss(theme)} ); - }, [organization, detailedProject, event, theme]); + }; return {openEditHighlightsModal, editProps}; } diff --git a/static/app/components/events/metrics/metricsSection.tsx b/static/app/components/events/metrics/metricsSection.tsx index c5a661f39c714a..f6f6e150910ea1 100644 --- a/static/app/components/events/metrics/metricsSection.tsx +++ b/static/app/components/events/metrics/metricsSection.tsx @@ -1,4 +1,4 @@ -import {useCallback, useEffect, useRef} from 'react'; +import {useEffect, useRef} from 'react'; import {Button} from '@sentry/scraps/button'; import {Flex} from '@sentry/scraps/layout'; @@ -78,26 +78,23 @@ function MetricsSectionContent({ ? result.data.slice(0, NUMBER_ABBREVIATED_METRICS) : undefined; - const onOpenMetricsDrawer = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - trackAnalytics('metrics.issue_details.drawer_opened', { - organization, - }); + const onOpenMetricsDrawer = (e: React.MouseEvent) => { + e.stopPropagation(); + trackAnalytics('metrics.issue_details.drawer_opened', { + organization, + }); - navigate( - { - ...location, - query: { - ...location.query, - [METRICS_DRAWER_QUERY_PARAM]: 'true', - }, + navigate( + { + ...location, + query: { + ...location.query, + [METRICS_DRAWER_QUERY_PARAM]: 'true', }, - {replace: true} - ); - }, - [navigate, location, organization] - ); + }, + {replace: true} + ); + }; useEffect(() => { const shouldOpenDrawer = location.query[METRICS_DRAWER_QUERY_PARAM] === 'true'; diff --git a/static/app/components/events/suspectCommitFeedback.tsx b/static/app/components/events/suspectCommitFeedback.tsx index d11bdc36e58210..093dc0db486c41 100644 --- a/static/app/components/events/suspectCommitFeedback.tsx +++ b/static/app/components/events/suspectCommitFeedback.tsx @@ -1,4 +1,4 @@ -import {useCallback, useState} from 'react'; +import {useState} from 'react'; import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; @@ -22,21 +22,18 @@ export function SuspectCommitFeedback({ const [feedbackSubmitted, setFeedbackSubmitted] = useState(false); const user = useUser(); - const handleFeedback = useCallback( - (isCorrect: boolean) => { - const analyticsData = { - choice_selected: isCorrect, - group_owner_id: groupOwnerId, - user_id: user.id, - organization, - }; + const handleFeedback = (isCorrect: boolean) => { + const analyticsData = { + choice_selected: isCorrect, + group_owner_id: groupOwnerId, + user_id: user.id, + organization, + }; - trackAnalytics('suspect_commit.feedback_submitted', analyticsData); + trackAnalytics('suspect_commit.feedback_submitted', analyticsData); - setFeedbackSubmitted(true); - }, - [groupOwnerId, organization, user] - ); + setFeedbackSubmitted(true); + }; if (feedbackSubmitted) { return ( diff --git a/static/app/components/events/traceEventDataSection.tsx b/static/app/components/events/traceEventDataSection.tsx index c1e18ac0c6d24b..8f58ce2247f8a3 100644 --- a/static/app/components/events/traceEventDataSection.tsx +++ b/static/app/components/events/traceEventDataSection.tsx @@ -84,154 +84,145 @@ export function TraceEventDataSection({ const isMobile = isMobilePlatform(platform); - const handleFilterFramesChange = useCallback( - (val: 'full' | 'relevant') => { - const isFullOptionClicked = val === 'full'; + const handleFilterFramesChange = (val: 'full' | 'relevant') => { + const isFullOptionClicked = val === 'full'; - trackAnalytics( - isFullOptionClicked - ? 'stack-trace.full_stack_trace_clicked' - : 'stack-trace.most_relevant_clicked', - { - organization, - project_slug: projectSlug, - platform, - is_mobile: isMobile, - } - ); - - setIsFullStackTrace(isFullOptionClicked); - }, - [organization, platform, projectSlug, isMobile, setIsFullStackTrace] - ); + trackAnalytics( + isFullOptionClicked + ? 'stack-trace.full_stack_trace_clicked' + : 'stack-trace.most_relevant_clicked', + { + organization, + project_slug: projectSlug, + platform, + is_mobile: isMobile, + } + ); - const handleSortByChange = useCallback( - (val: keyof typeof sortByOptions) => { - const isRecentFirst = val === 'recent-first'; + setIsFullStackTrace(isFullOptionClicked); + }; - trackAnalytics( - isRecentFirst - ? 'stack-trace.sort_option_recent_first_clicked' - : 'stack-trace.sort_option_recent_last_clicked', - { - organization, - project_slug: projectSlug, - platform, - is_mobile: isMobile, - } - ); + const handleSortByChange = (val: keyof typeof sortByOptions) => { + const isRecentFirst = val === 'recent-first'; - setIsNewestFramesFirst(isRecentFirst); - }, - [organization, platform, projectSlug, isMobile, setIsNewestFramesFirst] - ); - - const handleDisplayChange = useCallback( - (vals: typeof displayOptions) => { - if (vals.includes('raw-stack-trace')) { - trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', { - organization, - project_slug: projectSlug, - platform, - is_mobile: isMobile, - checked: true, - }); - } else if (displayOptions.includes('raw-stack-trace')) { - trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', { - organization, - project_slug: projectSlug, - platform, - is_mobile: isMobile, - checked: false, - }); + trackAnalytics( + isRecentFirst + ? 'stack-trace.sort_option_recent_first_clicked' + : 'stack-trace.sort_option_recent_last_clicked', + { + organization, + project_slug: projectSlug, + platform, + is_mobile: isMobile, } + ); - if (vals.includes('absolute-addresses')) { - trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', { - organization, - project_slug: projectSlug, - platform, - is_mobile: isMobile, - checked: true, - }); - } else if (displayOptions.includes('absolute-addresses')) { - trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', { - organization, - project_slug: projectSlug, - platform, - is_mobile: isMobile, - checked: false, - }); - } + setIsNewestFramesFirst(isRecentFirst); + }; + + const handleDisplayChange = (vals: typeof displayOptions) => { + if (vals.includes('raw-stack-trace')) { + trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', { + organization, + project_slug: projectSlug, + platform, + is_mobile: isMobile, + checked: true, + }); + } else if (displayOptions.includes('raw-stack-trace')) { + trackAnalytics('stack-trace.display_option_raw_stack_trace_clicked', { + organization, + project_slug: projectSlug, + platform, + is_mobile: isMobile, + checked: false, + }); + } - if (vals.includes('absolute-file-paths')) { - trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', { - organization, - project_slug: projectSlug, - platform, - is_mobile: isMobile, - checked: true, - }); - } else if (displayOptions.includes('absolute-file-paths')) { - trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', { - organization, - project_slug: projectSlug, - platform, - is_mobile: isMobile, - checked: false, - }); - } + if (vals.includes('absolute-addresses')) { + trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', { + organization, + project_slug: projectSlug, + platform, + is_mobile: isMobile, + checked: true, + }); + } else if (displayOptions.includes('absolute-addresses')) { + trackAnalytics('stack-trace.display_option_absolute_addresses_clicked', { + organization, + project_slug: projectSlug, + platform, + is_mobile: isMobile, + checked: false, + }); + } - if (vals.includes('minified')) { - trackAnalytics( - platform.startsWith('javascript') - ? 'stack-trace.display_option_minified_clicked' - : 'stack-trace.display_option_unsymbolicated_clicked', - { - organization, - project_slug: projectSlug, - platform, - is_mobile: isMobile, - checked: true, - } - ); - } else if (displayOptions.includes('minified')) { - trackAnalytics( - platform.startsWith('javascript') - ? 'stack-trace.display_option_minified_clicked' - : 'stack-trace.display_option_unsymbolicated_clicked', - { - organization, - project_slug: projectSlug, - platform, - is_mobile: isMobile, - checked: false, - } - ); - } + if (vals.includes('absolute-file-paths')) { + trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', { + organization, + project_slug: projectSlug, + platform, + is_mobile: isMobile, + checked: true, + }); + } else if (displayOptions.includes('absolute-file-paths')) { + trackAnalytics('stack-trace.display_option_absolute_file_paths_clicked', { + organization, + project_slug: projectSlug, + platform, + is_mobile: isMobile, + checked: false, + }); + } - if (vals.includes('verbose-function-names')) { - trackAnalytics('stack-trace.display_option_verbose_function_names_clicked', { + if (vals.includes('minified')) { + trackAnalytics( + platform.startsWith('javascript') + ? 'stack-trace.display_option_minified_clicked' + : 'stack-trace.display_option_unsymbolicated_clicked', + { organization, project_slug: projectSlug, platform, is_mobile: isMobile, checked: true, - }); - } else if (displayOptions.includes('verbose-function-names')) { - trackAnalytics('stack-trace.display_option_verbose_function_names_clicked', { + } + ); + } else if (displayOptions.includes('minified')) { + trackAnalytics( + platform.startsWith('javascript') + ? 'stack-trace.display_option_minified_clicked' + : 'stack-trace.display_option_unsymbolicated_clicked', + { organization, project_slug: projectSlug, platform, is_mobile: isMobile, checked: false, - }); - } + } + ); + } - setDisplayOptions(vals); - }, - [organization, platform, projectSlug, isMobile, displayOptions, setDisplayOptions] - ); + if (vals.includes('verbose-function-names')) { + trackAnalytics('stack-trace.display_option_verbose_function_names_clicked', { + organization, + project_slug: projectSlug, + platform, + is_mobile: isMobile, + checked: true, + }); + } else if (displayOptions.includes('verbose-function-names')) { + trackAnalytics('stack-trace.display_option_verbose_function_names_clicked', { + organization, + project_slug: projectSlug, + platform, + is_mobile: isMobile, + checked: false, + }); + } + + setDisplayOptions(vals); + }; const handleCopyRawStacktrace = useCallback(() => { trackAnalytics('stack-trace.copy_raw_clicked', { diff --git a/static/app/components/externalIssues/externalIssueForm.tsx b/static/app/components/externalIssues/externalIssueForm.tsx index 1909ad8c008504..7df49555d65ef5 100644 --- a/static/app/components/externalIssues/externalIssueForm.tsx +++ b/static/app/components/externalIssues/externalIssueForm.tsx @@ -253,13 +253,10 @@ export function ExternalIssueForm({ } }, [isPending, isError, loadSpan, organization, group, integration, hasTrackedLoad]); - const handleClick = useCallback( - (newAction: ExternalIssueAction) => { - setAction(newAction); - refetch(); - }, - [refetch] - ); + const handleClick = (newAction: ExternalIssueAction) => { + setAction(newAction); + refetch(); + }; const handleSubmit = useCallback( async (values: Record) => { diff --git a/static/app/components/feedback/feedbackItem/feedbackItemUsername.tsx b/static/app/components/feedback/feedbackItem/feedbackItemUsername.tsx index dbe388548939ac..17b98d813a6175 100644 --- a/static/app/components/feedback/feedbackItem/feedbackItemUsername.tsx +++ b/static/app/components/feedback/feedbackItem/feedbackItemUsername.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useId, type CSSProperties} from 'react'; +import {type CSSProperties, Fragment, useId} from 'react'; import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; @@ -35,14 +35,14 @@ export function FeedbackItemUsername({className, feedbackIssue, style}: Props) { const userNodeId = useId(); - const handleSelectText = useCallback(() => { + const handleSelectText = () => { const node = document.getElementById(userNodeId); if (!node) { return; } selectText(node); - }, [userNodeId]); + }; const {copy} = useCopyToClipboard(); diff --git a/static/app/components/forms/form.tsx b/static/app/components/forms/form.tsx index 1502ee0f2127a3..9547ab71875de8 100644 --- a/static/app/components/forms/form.tsx +++ b/static/app/components/forms/form.tsx @@ -197,38 +197,22 @@ export function Form({ [formModel, onSubmitError] ); - const handleSubmit = useCallback( - (e: any) => { - if (!skipPreventDefault) { - e.preventDefault(); - } - if (formModel.isSaving) { - return; - } + const handleSubmit = (e: any) => { + if (!skipPreventDefault) { + e.preventDefault(); + } + if (formModel.isSaving) { + return; + } - onPreSubmit?.(); + onPreSubmit?.(); - onSubmit?.( - formModel.getData(), - handleSubmitSuccess, - handleSubmitError, - e, - formModel - ); + onSubmit?.(formModel.getData(), handleSubmitSuccess, handleSubmitError, e, formModel); - if (!onSubmit) { - formModel.saveForm(); - } - }, - [ - formModel, - handleSubmitError, - handleSubmitSuccess, - onPreSubmit, - onSubmit, - skipPreventDefault, - ] - ); + if (!onSubmit) { + formModel.saveForm(); + } + }; const shouldShowFooter = typeof hideFooter === 'undefined' ? !saveOnBlur : !hideFooter; diff --git a/static/app/components/groupPreviewTooltip/groupPreviewHovercard.tsx b/static/app/components/groupPreviewTooltip/groupPreviewHovercard.tsx index 2bd4e684f6b421..564dd842373218 100644 --- a/static/app/components/groupPreviewTooltip/groupPreviewHovercard.tsx +++ b/static/app/components/groupPreviewTooltip/groupPreviewHovercard.tsx @@ -1,5 +1,4 @@ import type {ComponentProps} from 'react'; -import {useCallback} from 'react'; import {css, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; @@ -18,10 +17,7 @@ export function GroupPreviewHovercard({ ...props }: GroupPreviewHovercardProps) { const theme = useTheme(); - const handleStackTracePreviewClick = useCallback( - (e: React.MouseEvent) => e.stopPropagation(), - [] - ); + const handleStackTracePreviewClick = (e: React.MouseEvent) => e.stopPropagation(); // No need to preview on hover for small devices const shouldNotPreview = useMedia(`(max-width: ${theme.breakpoints.lg})`); diff --git a/static/app/components/modals/explore/saveQueryModal.tsx b/static/app/components/modals/explore/saveQueryModal.tsx index ebd50fd47f14eb..c3907fcade9399 100644 --- a/static/app/components/modals/explore/saveQueryModal.tsx +++ b/static/app/components/modals/explore/saveQueryModal.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useState} from 'react'; +import {Fragment, useState} from 'react'; import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; @@ -49,7 +49,7 @@ function SaveQueryModal({ const setQueryParamsSavedQuery = useSetQueryParamsSavedQuery(); - const onSave = useCallback(async () => { + const onSave = async () => { try { setIsSaving(true); addLoadingMessage(t('Saving query...')); @@ -82,17 +82,7 @@ function SaveQueryModal({ } finally { setIsSaving(false); } - }, [ - saveQuery, - name, - starred, - setQueryParamsSavedQuery, - closeModal, - organization, - initialName, - source, - traceItemDataset, - ]); + }; return ( diff --git a/static/app/components/modals/inviteMissingMembersModal/index.tsx b/static/app/components/modals/inviteMissingMembersModal/index.tsx index d7bd9d558c5a64..8582924086b3d3 100644 --- a/static/app/components/modals/inviteMissingMembersModal/index.tsx +++ b/static/app/components/modals/inviteMissingMembersModal/index.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useMemo, useState} from 'react'; +import {Fragment, useMemo, useState} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; @@ -62,44 +62,35 @@ export function InviteMissingMembersModal({ [allowedRoles] ); - const setRole = useCallback( - (role: string, index: number) => { - setMemberInvites(prevInvites => { - const invites = prevInvites.map(i => ({...i})); - invites[index]!.role = role; - if (!allowedRolesMap[role]!.isTeamRolesAllowed) { - invites[index]!.teamSlugs = new Set([]); - } - return invites; - }); - }, - [allowedRolesMap] - ); + const setRole = (role: string, index: number) => { + setMemberInvites(prevInvites => { + const invites = prevInvites.map(i => ({...i})); + invites[index]!.role = role; + if (!allowedRolesMap[role]!.isTeamRolesAllowed) { + invites[index]!.teamSlugs = new Set([]); + } + return invites; + }); + }; - const setTeams = useCallback((teamSlugs: string[], index: number) => { + const setTeams = (teamSlugs: string[], index: number) => { setMemberInvites(prevInvites => { const invites = prevInvites.map(i => ({...i})); invites[index]!.teamSlugs = new Set(teamSlugs); return invites; }); - }, []); - - const selectAll = useCallback( - (checked: boolean) => { - const selectedMembers = memberInvites.map(m => ({...m, selected: checked})); - setMemberInvites(selectedMembers); - }, - [memberInvites] - ); + }; - const toggleCheckbox = useCallback( - (checked: boolean, index: number) => { - const selectedMembers = [...memberInvites]; - selectedMembers[index]!.selected = checked; - setMemberInvites(selectedMembers); - }, - [memberInvites] - ); + const selectAll = (checked: boolean) => { + const selectedMembers = memberInvites.map(m => ({...m, selected: checked})); + setMemberInvites(selectedMembers); + }; + + const toggleCheckbox = (checked: boolean, index: number) => { + const selectedMembers = [...memberInvites]; + selectedMembers[index]!.selected = checked; + setMemberInvites(selectedMembers); + }; if (memberInvites.length === 0 || !organization.access.includes('org:write')) { return null; diff --git a/static/app/components/modals/projectCreationModal.tsx b/static/app/components/modals/projectCreationModal.tsx index e0f7faf25a1177..8111a29280fc63 100644 --- a/static/app/components/modals/projectCreationModal.tsx +++ b/static/app/components/modals/projectCreationModal.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useState} from 'react'; +import {Fragment, useState} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import omit from 'lodash/omit'; @@ -15,14 +15,14 @@ import { clearIndicators, } from 'sentry/actionCreators/indicator'; import { + type ModalRenderProps, openConsoleModal, openProjectCreationModal, - type ModalRenderProps, } from 'sentry/actionCreators/modal'; import { - PlatformPicker, type Category, type Platform, + PlatformPicker, } from 'sentry/components/platformPicker'; import type {TeamOption} from 'sentry/components/teamSelector'; import {TeamSelector} from 'sentry/components/teamSelector'; @@ -94,7 +94,7 @@ export default function ProjectCreationModal({ }); } - const createProject = useCallback(async () => { + const createProject = async () => { const {slug} = organization; const alertRuleConfig = getRequestDataFragment(alertForm); @@ -156,7 +156,7 @@ export default function ProjectCreationModal({ setCreating(false); addErrorMessage(`Failed to create project ${projectName}`); } - }, [api, organization, platform, projectName, team, closeModal, alertForm]); + }; return ( diff --git a/static/app/components/onboarding/productSelection.tsx b/static/app/components/onboarding/productSelection.tsx index c678cce9a43667..79d5e4fc5e59d4 100644 --- a/static/app/components/onboarding/productSelection.tsx +++ b/static/app/components/onboarding/productSelection.tsx @@ -1,5 +1,5 @@ import type {ReactNode} from 'react'; -import {useCallback, useEffect, useEffectEvent, useMemo} from 'react'; +import {useEffect, useEffectEvent, useMemo} from 'react'; import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; @@ -631,39 +631,36 @@ export function ProductSelection({ initializeProducts(); }, []); - const handleClickProduct = useCallback( - (product: ProductSolution) => { - const newProduct = new Set( - urlProducts.includes(product) - ? urlProducts.filter(p => p !== product) - : [...urlProducts, product] - ); + const handleClickProduct = (product: ProductSolution) => { + const newProduct = new Set( + urlProducts.includes(product) + ? urlProducts.filter(p => p !== product) + : [...urlProducts, product] + ); - if (products?.includes(ProductSolution.PROFILING)) { - // Ensure that if profiling is enabled, tracing is also enabled - if ( - product === ProductSolution.PROFILING && - newProduct.has(ProductSolution.PROFILING) - ) { - newProduct.add(ProductSolution.PERFORMANCE_MONITORING); - } else if ( - product === ProductSolution.PERFORMANCE_MONITORING && - !newProduct.has(ProductSolution.PERFORMANCE_MONITORING) - ) { - newProduct.delete(ProductSolution.PROFILING); - } + if (products?.includes(ProductSolution.PROFILING)) { + // Ensure that if profiling is enabled, tracing is also enabled + if ( + product === ProductSolution.PROFILING && + newProduct.has(ProductSolution.PROFILING) + ) { + newProduct.add(ProductSolution.PERFORMANCE_MONITORING); + } else if ( + product === ProductSolution.PERFORMANCE_MONITORING && + !newProduct.has(ProductSolution.PERFORMANCE_MONITORING) + ) { + newProduct.delete(ProductSolution.PROFILING); } + } - const selectedProducts = Array.from(newProduct); + const selectedProducts = Array.from(newProduct); - onChange?.({ - previousProducts: urlProducts, - products: selectedProducts, - }); - setParams({product: selectedProducts}); - }, - [products, setParams, urlProducts, onChange] - ); + onChange?.({ + previousProducts: urlProducts, + products: selectedProducts, + }); + setParams({product: selectedProducts}); + }; if (!products) { // if the platform does not support any product, we don't render anything diff --git a/static/app/components/onboardingWizard/content.tsx b/static/app/components/onboardingWizard/content.tsx index 56aee9b89fb137..619236c8fb87e0 100644 --- a/static/app/components/onboardingWizard/content.tsx +++ b/static/app/components/onboardingWizard/content.tsx @@ -233,7 +233,7 @@ function Task({task, hidePanel}: TaskProps) { [task, organization, navigate, location, hidePanel, tours, sidebarTour] ); - const handleMarkSkipped = useCallback(() => { + const handleMarkSkipped = () => { // all demos tasks are not skippable, // so this apply for the quick start only. // Adding this check here just in case it changes in the future @@ -255,7 +255,7 @@ function Task({task, hidePanel}: TaskProps) { completionSeen: true, }, ]); - }, [task, organization, mutateOnboardingTasks]); + }; const iconTooltipText = useMemo(() => { switch (task.status) { diff --git a/static/app/components/pageFilters/environment/environmentPageFilter.tsx b/static/app/components/pageFilters/environment/environmentPageFilter.tsx index 8efd90901fc26b..7cbf1e07c2dc5e 100644 --- a/static/app/components/pageFilters/environment/environmentPageFilter.tsx +++ b/static/app/components/pageFilters/environment/environmentPageFilter.tsx @@ -225,11 +225,11 @@ export function EnvironmentPageFilter({ const hasStagedChanges = xor(stagedSelect.value, value).length > 0; const shouldShowReset = stagedSelect.value.length > 0; - const handleReset = useCallback(() => { + const handleReset = () => { dispatch({type: 'remove staged'}); handleChange([]); onReset?.(); - }, [dispatch, handleChange, onReset]); + }; return ( ({value: r, label: r})); - const trackCloudFormationClick = useCallback(() => { + const trackCloudFormationClick = () => { trackIntegrationAnalytics('integrations.cloudformation_link_clicked', { integration: 'aws_lambda', integration_type: 'first_party', organization, }); - }, [organization]); + }; const form = useScrapsForm({ ...defaultFormOptions, diff --git a/static/app/components/profiling/continuousProfileHeader.tsx b/static/app/components/profiling/continuousProfileHeader.tsx index 7b0ef4ac20ca16..22305a87fa26a3 100644 --- a/static/app/components/profiling/continuousProfileHeader.tsx +++ b/static/app/components/profiling/continuousProfileHeader.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useMemo} from 'react'; +import {Fragment, useMemo} from 'react'; import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; @@ -40,11 +40,11 @@ export function ContinuousProfileHeader({transaction}: ContinuousProfileHeader) }) : null; - const handleGoToTransaction = useCallback(() => { + const handleGoToTransaction = () => { trackAnalytics('profiling_views.go_to_transaction', { organization, }); - }, [organization]); + }; return ( diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx index 65f9cd9b8280da..97fcdb8caf6f32 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphDrawer.tsx @@ -78,12 +78,9 @@ const FlamegraphDrawer = memo(function FlamegraphDrawer(props: FlamegraphDrawerP return invertCallTree(maybeFilteredRoots); }, [tab, treeType, props.rootNodes]); - const handleRecursionChange = useCallback( - (evt: React.ChangeEvent) => { - setRecursion(evt.currentTarget.checked ? 'collapsed' : null); - }, - [] - ); + const handleRecursionChange = (evt: React.ChangeEvent) => { + setRecursion(evt.currentTarget.checked ? 'collapsed' : null); + }; const onBottomUpClick = useCallback(() => { setTab('bottom up'); @@ -93,17 +90,17 @@ const FlamegraphDrawer = memo(function FlamegraphDrawer(props: FlamegraphDrawerP setTab('top down'); }, [setTab]); - const onAllApplicationsClick = useCallback(() => { + const onAllApplicationsClick = () => { setTreeType('all'); - }, []); + }; - const onApplicationsClick = useCallback(() => { + const onApplicationsClick = () => { setTreeType('application'); - }, []); + }; - const onSystemsClick = useCallback(() => { + const onSystemsClick = () => { setTreeType('system'); - }, []); + }; const onTableLeftClick = useCallback(() => { dispatch({type: 'set layout', payload: 'table left'}); diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphTreeTable.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphTreeTable.tsx index a02ef7c301afa7..62ee0a882f5887 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphTreeTable.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/flamegraphTreeTable.tsx @@ -256,19 +256,16 @@ export function FlamegraphTreeTable({ tree, }); - const onSortChange = useCallback( - (newSort: 'total weight' | 'self weight' | 'name') => { - const newDirection = - newSort === sort ? (direction === 'asc' ? 'desc' : 'asc') : 'desc'; + const onSortChange = (newSort: 'total weight' | 'self weight' | 'name') => { + const newDirection = + newSort === sort ? (direction === 'asc' ? 'desc' : 'asc') : 'desc'; - setDirection(newDirection); - setSort(newSort); + setDirection(newDirection); + setSort(newSort); - const sortFn = makeCallTreeTableSortFunction(newSort, newDirection); - handleSortingChange(sortFn); - }, - [sort, direction, handleSortingChange] - ); + const sortFn = makeCallTreeTableSortFunction(newSort, newDirection); + handleSortingChange(sortFn); + }; useEffect(() => { function onShowInTableView(frame: FlamegraphFrame) { diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx index 70edbe4941b219..56f30f2d64aec9 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useMemo, useRef, useState} from 'react'; +import {Fragment, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; import {PlatformIcon} from 'platformicons'; @@ -67,13 +67,13 @@ export function ProfileDetails(props: ProfileDetailsProps) { p => p.id === String(props.profileGroup.metadata.projectID) ); - const onEnvironmentTabClick = useCallback(() => { + const onEnvironmentTabClick = () => { setDetailsTab('environment'); - }, []); + }; - const onTransactionTabClick = useCallback(() => { + const onTransactionTabClick = () => { setDetailsTab('transaction'); - }, []); + }; const flamegraphPreferences = useFlamegraphPreferences(); const isResizableDetailsBar = diff --git a/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphNegationSwitch.tsx b/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphNegationSwitch.tsx index b62ad7c19ff618..d35fde838c9f35 100644 --- a/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphNegationSwitch.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphNegationSwitch.tsx @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import styled from '@emotion/styled'; import {SegmentedControl} from '@sentry/scraps/segmentedControl'; @@ -13,12 +12,9 @@ export function DifferentialFlamegraphNegationSwitch( props: DifferentialFlamegraphNegationSwitchProps ) { const onNegatedChange = props.onNegatedChange; - const onWrapChange = useCallback( - (value: 'negated' | 'regular') => { - onNegatedChange(value === 'negated'); - }, - [onNegatedChange] - ); + const onWrapChange = (value: 'negated' | 'regular') => { + onNegatedChange(value === 'negated'); + }; return ( diff --git a/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphSettingsButton.tsx b/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphSettingsButton.tsx index 422d1ab259fae6..47f045735a5b29 100644 --- a/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphSettingsButton.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphSettingsButton.tsx @@ -29,9 +29,9 @@ export function DifferentialFlamegraphSettingsButton( const contextMenu = useContextMenu({container: null}); - const onToggleMenu = useCallback(() => { + const onToggleMenu = () => { contextMenu.setOpen(!contextMenu.open); - }, [contextMenu]); + }; const onClose = useCallback(() => { contextMenu.setOpen(false); diff --git a/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphToolbar.tsx b/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphToolbar.tsx index 2baf4d9ea07821..c0d788d2092f75 100644 --- a/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphToolbar.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphToolbar/differentialFlamegraphToolbar.tsx @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; @@ -22,9 +21,9 @@ interface DifferentialFlamegraphProps { onNegatedChange: (source: boolean) => void; } export function DifferentialFlamegraphToolbar(props: DifferentialFlamegraphProps) { - const onResetZoom = useCallback(() => { + const onResetZoom = () => { props.canvasPoolManager.dispatch('reset zoom', []); - }, [props.canvasPoolManager]); + }; return ( diff --git a/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphOptionsMenu.tsx b/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphOptionsMenu.tsx index 918e54c035140b..9a5b59be299ecf 100644 --- a/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphOptionsMenu.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphOptionsMenu.tsx @@ -39,13 +39,13 @@ function FlamegraphOptionsMenu({ [dispatch] ); - const onResetZoom = useCallback(() => { + const onResetZoom = () => { canvasPoolManager.dispatch('reset zoom', []); trackAnalytics('profiling_views.flamegraph.zoom.reset', { organization, profile_type: profileType, }); - }, [canvasPoolManager, organization, profileType]); + }; const continuousLocationDescriptor = useMemo( () => { diff --git a/static/app/components/profiling/profileHeader.tsx b/static/app/components/profiling/profileHeader.tsx index bd412bf6c5e3f1..8de4f4cc618e63 100644 --- a/static/app/components/profiling/profileHeader.tsx +++ b/static/app/components/profiling/profileHeader.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useMemo} from 'react'; +import {Fragment, useMemo} from 'react'; import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; @@ -55,11 +55,11 @@ function ProfileHeader({transaction, projectId, eventId}: ProfileHeaderProps) { }) : null; - const handleGoToTransaction = useCallback(() => { + const handleGoToTransaction = () => { trackAnalytics('profiling_views.go_to_transaction', { organization, }); - }, [organization]); + }; const breadcrumbTrails = useMemo(() => { return [ diff --git a/static/app/components/projects/missingProjectMembership.tsx b/static/app/components/projects/missingProjectMembership.tsx index 239815dbfb1c15..7c3957af5fd0a1 100644 --- a/static/app/components/projects/missingProjectMembership.tsx +++ b/static/app/components/projects/missingProjectMembership.tsx @@ -1,4 +1,4 @@ -import {useCallback, useMemo, useState} from 'react'; +import {useMemo, useState} from 'react'; import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; @@ -29,7 +29,7 @@ function JoinTeamAction({teamSlug, organization}: JoinTeamActionProps) { const teamStoreData = useLegacyStore(TeamStore); const selectedTeam = teamStoreData.teams.find(team => team.slug === teamSlug); - const handleJoinTeam = useCallback(() => { + const handleJoinTeam = () => { setIsLoading(true); joinTeam( @@ -49,7 +49,7 @@ function JoinTeamAction({teamSlug, organization}: JoinTeamActionProps) { }, } ); - }, [api, organization.slug, teamSlug]); + }; const openMembership = organization.features.includes('open-membership'); diff --git a/static/app/components/replays/breadcrumbs/breadcrumbIssueLink.tsx b/static/app/components/replays/breadcrumbs/breadcrumbIssueLink.tsx index 225f1af5496bd2..3d5acc5dd4cad5 100644 --- a/static/app/components/replays/breadcrumbs/breadcrumbIssueLink.tsx +++ b/static/app/components/replays/breadcrumbs/breadcrumbIssueLink.tsx @@ -1,4 +1,4 @@ -import {useCallback, type MouseEvent} from 'react'; +import {type MouseEvent} from 'react'; import styled from '@emotion/styled'; import {ProjectAvatar} from '@sentry/scraps/avatar'; @@ -27,9 +27,9 @@ function CrumbErrorIssue({frame}: {frame: FeedbackFrame | ErrorFrame}) { const organization = useOrganization(); const project = useProjectFromSlug({organization, projectSlug: frame.data.projectSlug}); const {groupId} = useReplayGroupContext(); - const handleClick = useCallback((e: MouseEvent) => { + const handleClick = (e: MouseEvent) => { e.stopPropagation(); - }, []); + }; const projectAvatar = project ? : null; diff --git a/static/app/components/replays/breadcrumbs/errorTitle.tsx b/static/app/components/replays/breadcrumbs/errorTitle.tsx index 0ca0a17c824403..ae8327ce9f49b1 100644 --- a/static/app/components/replays/breadcrumbs/errorTitle.tsx +++ b/static/app/components/replays/breadcrumbs/errorTitle.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, type MouseEvent} from 'react'; +import {Fragment, type MouseEvent} from 'react'; import capitalize from 'lodash/capitalize'; import {Link} from '@sentry/scraps/link'; @@ -11,9 +11,9 @@ import {useOrganization} from 'sentry/utils/useOrganization'; export function CrumbErrorTitle({frame}: {frame: ErrorFrame}) { const organization = useOrganization(); const {eventId} = useReplayGroupContext(); - const handleClick = useCallback((e: MouseEvent) => { + const handleClick = (e: MouseEvent) => { e.stopPropagation(); - }, []); + }; if (eventId === frame.data.eventId) { return Error: This Event; diff --git a/static/app/components/replays/replayFullscreenButton.tsx b/static/app/components/replays/replayFullscreenButton.tsx index aef818da0f71f8..bf1d029f3d272a 100644 --- a/static/app/components/replays/replayFullscreenButton.tsx +++ b/static/app/components/replays/replayFullscreenButton.tsx @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import screenfull from 'screenfull'; import {Button} from '@sentry/scraps/button'; @@ -21,7 +20,7 @@ export function ReplayFullscreenButton({toggleFullscreen}: Props) { const isFullscreen = useIsFullscreen(); const {analyticsContext} = useReplayContext(); - const handleFullscreenToggle = useCallback(() => { + const handleFullscreenToggle = () => { trackAnalytics('replay.toggle-fullscreen', { organization, context: analyticsContext, @@ -29,7 +28,7 @@ export function ReplayFullscreenButton({toggleFullscreen}: Props) { fullscreen: !isFullscreen, }); toggleFullscreen(); - }, [analyticsContext, isFullscreen, organization, toggleFullscreen, user.email]); + }; // If the browser supports going fullscreen or not. iPhone Safari won't do // it. https://caniuse.com/fullscreen diff --git a/static/app/components/replays/table/deleteReplays.tsx b/static/app/components/replays/table/deleteReplays.tsx index 7619fa3cbcfe3f..9416bd9ded2f69 100644 --- a/static/app/components/replays/table/deleteReplays.tsx +++ b/static/app/components/replays/table/deleteReplays.tsx @@ -22,11 +22,11 @@ import {IconCalendar, IconDelete} from 'sentry/icons'; import {t, tct, tn} from 'sentry/locale'; import type {Project} from 'sentry/types/project'; import {getShortEventId} from 'sentry/utils/events'; -import {useQueryClient, type QueryKeyEndpointOptions} from 'sentry/utils/queryClient'; +import {type QueryKeyEndpointOptions, useQueryClient} from 'sentry/utils/queryClient'; import {decodeList} from 'sentry/utils/queryString'; import { - useDeleteReplays, type ReplayBulkDeletePayload, + useDeleteReplays, } from 'sentry/utils/replays/hooks/useDeleteReplays'; import {useLocationQuery} from 'sentry/utils/url/useLocationQuery'; import {useOrganization} from 'sentry/utils/useOrganization'; diff --git a/static/app/components/replays/timeAndScrubberGrid.tsx b/static/app/components/replays/timeAndScrubberGrid.tsx index 0b5b267d488e5b..3c0099774bc286 100644 --- a/static/app/components/replays/timeAndScrubberGrid.tsx +++ b/static/app/components/replays/timeAndScrubberGrid.tsx @@ -1,4 +1,4 @@ -import {useCallback, useRef} from 'react'; +import {useRef} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; @@ -33,21 +33,21 @@ function TimelineSizeBar({isLoading}: {isLoading?: boolean}) { const durationMs = replay?.getDurationMs(); const maxScale = durationMs ? Math.ceil(durationMs / 60000) : 10; - const handleZoomOut = useCallback(() => { + const handleZoomOut = () => { const newScale = Math.max(timelineScale - 1, 1); setTimelineScale(newScale); trackAnalytics('replay.timeline.zoom-out', { organization, }); - }, [timelineScale, setTimelineScale, organization]); + }; - const handleZoomIn = useCallback(() => { + const handleZoomIn = () => { const newScale = Math.min(timelineScale + 1, maxScale); setTimelineScale(newScale); trackAnalytics('replay.timeline.zoom-in', { organization, }); - }, [timelineScale, maxScale, setTimelineScale, organization]); + }; return ( diff --git a/static/app/components/searchQueryBuilder/selectionKeyHandler.tsx b/static/app/components/searchQueryBuilder/selectionKeyHandler.tsx index 568fe91e036124..89d24b09f2ab38 100644 --- a/static/app/components/searchQueryBuilder/selectionKeyHandler.tsx +++ b/static/app/components/searchQueryBuilder/selectionKeyHandler.tsx @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import {VisuallyHidden} from '@react-aria/visually-hidden'; import type {ListState} from '@react-stately/list'; @@ -38,152 +37,143 @@ export function SelectionKeyHandler({ .map(key => state.collection.getItem(key)?.value) .filter(defined); - const onPaste = useCallback( - (e: React.ClipboardEvent) => { - e.preventDefault(); - e.stopPropagation(); - - const text = e.clipboardData.getData('text/plain').replace('\n', '').trim(); - - dispatch({ - type: 'REPLACE_TOKENS_WITH_TEXT_ON_PASTE', - tokens: selectedTokens, - text, - }); - }, - [dispatch, selectedTokens] - ); - - const onKeyDown = useCallback( - (e: React.KeyboardEvent) => { - switch (e.key) { - case 'Backspace': - case 'Delete': { - e.preventDefault(); - e.stopPropagation(); - dispatch({ - type: 'REPLACE_TOKENS_WITH_TEXT_ON_DELETE', - tokens: selectedTokens, - text: '', - }); - state.selectionManager.setFocusedKey( - findNearestFreeTextKey(state, state.selectionManager.firstSelectedKey, 'left') - ); - state.selectionManager.clearSelection(); - - // Ask Seer - Clear the input value when the user deletes all tokens - if (state.collection.size === selectedTokens.length) { - currentInputValueRef.current = ''; - } - return; + const onPaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + + const text = e.clipboardData.getData('text/plain').replace('\n', '').trim(); + + dispatch({ + type: 'REPLACE_TOKENS_WITH_TEXT_ON_PASTE', + tokens: selectedTokens, + text, + }); + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + switch (e.key) { + case 'Backspace': + case 'Delete': { + e.preventDefault(); + e.stopPropagation(); + dispatch({ + type: 'REPLACE_TOKENS_WITH_TEXT_ON_DELETE', + tokens: selectedTokens, + text: '', + }); + state.selectionManager.setFocusedKey( + findNearestFreeTextKey(state, state.selectionManager.firstSelectedKey, 'left') + ); + state.selectionManager.clearSelection(); + + // Ask Seer - Clear the input value when the user deletes all tokens + if (state.collection.size === selectedTokens.length) { + currentInputValueRef.current = ''; } - case 'ArrowRight': - e.preventDefault(); - e.stopPropagation(); - - if (e.shiftKey) { - selectInDirection({state, direction: 'right'}); - return; - } + return; + } + case 'ArrowRight': + e.preventDefault(); + e.stopPropagation(); - state.selectionManager.clearSelection(); - state.selectionManager.setFocusedKey( - findNearestFreeTextKey(state, state.selectionManager.lastSelectedKey, 'right') - ); + if (e.shiftKey) { + selectInDirection({state, direction: 'right'}); return; - case 'ArrowLeft': - e.preventDefault(); - e.stopPropagation(); + } - if (e.shiftKey) { - selectInDirection({state, direction: 'left'}); - return; - } + state.selectionManager.clearSelection(); + state.selectionManager.setFocusedKey( + findNearestFreeTextKey(state, state.selectionManager.lastSelectedKey, 'right') + ); + return; + case 'ArrowLeft': + e.preventDefault(); + e.stopPropagation(); - state.selectionManager.clearSelection(); - state.selectionManager.setFocusedKey( - findNearestFreeTextKey(state, state.selectionManager.firstSelectedKey, 'left') - ); + if (e.shiftKey) { + selectInDirection({state, direction: 'left'}); return; - default: - if (isCtrlKeyPressed(e)) { - const copySelectedTokens = () => { - const queryToCopy = selectedTokens - .map(token => token.text) - .join('') - .trim(); - navigator.clipboard.writeText(queryToCopy); - }; - - if (e.key === 'a') { - state.selectionManager.selectAll(); - e.preventDefault(); - e.stopPropagation(); - } else if (e.key === 'z') { - state.selectionManager.clearSelection(); - undo(); - e.stopPropagation(); - e.preventDefault(); - } else if (e.key === 'x') { - state.selectionManager.clearSelection(); - copySelectedTokens(); - dispatch({ - type: 'REPLACE_TOKENS_WITH_TEXT_ON_CUT', - tokens: selectedTokens, - text: '', - }); - e.stopPropagation(); - e.preventDefault(); - } else if (e.key === 'c') { - copySelectedTokens(); - e.preventDefault(); - e.stopPropagation(); - } - return; - } + } - // Wrap selected tokens in parentheses when ( or ) is pressed - if ((e.key === '(' || e.key === ')') && selectedTokens.length > 0) { + state.selectionManager.clearSelection(); + state.selectionManager.setFocusedKey( + findNearestFreeTextKey(state, state.selectionManager.firstSelectedKey, 'left') + ); + return; + default: + if (isCtrlKeyPressed(e)) { + const copySelectedTokens = () => { + const queryToCopy = selectedTokens + .map(token => token.text) + .join('') + .trim(); + navigator.clipboard.writeText(queryToCopy); + }; + + if (e.key === 'a') { + state.selectionManager.selectAll(); e.preventDefault(); e.stopPropagation(); - dispatch({ - type: 'WRAP_TOKENS_WITH_PARENTHESES', - tokens: selectedTokens, - }); + } else if (e.key === 'z') { state.selectionManager.clearSelection(); - return; - } - - // If the key pressed will generate a symbol, replace the selection with it - if (/^.$/u.test(e.key)) { + undo(); + e.stopPropagation(); + e.preventDefault(); + } else if (e.key === 'x') { + state.selectionManager.clearSelection(); + copySelectedTokens(); dispatch({ - type: 'REPLACE_TOKENS_WITH_TEXT_ON_KEY_DOWN', - text: e.key, + type: 'REPLACE_TOKENS_WITH_TEXT_ON_CUT', tokens: selectedTokens, + text: '', }); - state.selectionManager.clearSelection(); + e.stopPropagation(); + e.preventDefault(); + } else if (e.key === 'c') { + copySelectedTokens(); e.preventDefault(); e.stopPropagation(); } + return; + } + // Wrap selected tokens in parentheses when ( or ) is pressed + if ((e.key === '(' || e.key === ')') && selectedTokens.length > 0) { + e.preventDefault(); + e.stopPropagation(); + dispatch({ + type: 'WRAP_TOKENS_WITH_PARENTHESES', + tokens: selectedTokens, + }); + state.selectionManager.clearSelection(); return; - } - }, - [currentInputValueRef, dispatch, selectInDirection, selectedTokens, state, undo] - ); + } + + // If the key pressed will generate a symbol, replace the selection with it + if (/^.$/u.test(e.key)) { + dispatch({ + type: 'REPLACE_TOKENS_WITH_TEXT_ON_KEY_DOWN', + text: e.key, + tokens: selectedTokens, + }); + state.selectionManager.clearSelection(); + e.preventDefault(); + e.stopPropagation(); + } - // Ensure that the selection is cleared when this input loses focus - const onBlur = useCallback( - (e: React.FocusEvent) => { - // React Aria will sometimes focus the grid element, which we handle in useQueryBuilderGrid(). - // This should be ignored since focus will return here. - if (e.relatedTarget === gridRef.current) { return; - } - state.selectionManager.clearSelection(); - }, - [state.selectionManager, gridRef] - ); + } + }; + + // Ensure that the selection is cleared when this input loses focus + const onBlur = (e: React.FocusEvent) => { + // React Aria will sometimes focus the grid element, which we handle in useQueryBuilderGrid(). + // This should be ignored since focus will return here. + if (e.relatedTarget === gridRef.current) { + return; + } + state.selectionManager.clearSelection(); + }; // Using VisuallyHidden because display: none will not allow the input to be focused return ( diff --git a/static/app/components/stackTrace/exceptionGroup.tsx b/static/app/components/stackTrace/exceptionGroup.tsx index 49c0af15046efc..518a08963d8ab5 100644 --- a/static/app/components/stackTrace/exceptionGroup.tsx +++ b/static/app/components/stackTrace/exceptionGroup.tsx @@ -29,14 +29,14 @@ export function useHiddenExceptions(values: ExceptionValue[]) { ) ); - const toggleRelatedExceptions = useCallback((exceptionId: number) => { + const toggleRelatedExceptions = (exceptionId: number) => { setHiddenExceptions(old => { if (!defined(old[exceptionId])) { return old; } return {...old, [exceptionId]: !old[exceptionId]}; }); - }, []); + }; const expandException = useCallback( (exceptionId: number) => { diff --git a/static/app/components/timeRangeSelector/index.tsx b/static/app/components/timeRangeSelector/index.tsx index 88eceb32d1a9e8..097f23c699ad51 100644 --- a/static/app/components/timeRangeSelector/index.tsx +++ b/static/app/components/timeRangeSelector/index.tsx @@ -270,34 +270,31 @@ export function TimeRangeSelector({ ); }, [showRelative, onChange, internalValue, hasChanges]); - const handleChange = useCallback( - (option: SelectOption) => { - // The absolute option was selected -> open absolute selector - if (option.value === ABSOLUTE_OPTION_VALUE) { - setInternalValue(current => { - const defaultStart = defaultAbsolute?.start - ? defaultAbsolute.start - : getPeriodAgo( - 'hours', - parsePeriodToHours(relative || defaultPeriod || DEFAULT_STATS_PERIOD) - ).toDate(); - const defaultEnd = defaultAbsolute?.end ? defaultAbsolute.end : new Date(); - return { - ...current, - // Update default values for absolute selector - start: start ? getInternalDate(start, utc) : defaultStart, - end: end ? getInternalDate(end, utc) : defaultEnd, - }; - }); - setShowAbsoluteSelector(true); - return; - } + const handleChange = (option: SelectOption) => { + // The absolute option was selected -> open absolute selector + if (option.value === ABSOLUTE_OPTION_VALUE) { + setInternalValue(current => { + const defaultStart = defaultAbsolute?.start + ? defaultAbsolute.start + : getPeriodAgo( + 'hours', + parsePeriodToHours(relative || defaultPeriod || DEFAULT_STATS_PERIOD) + ).toDate(); + const defaultEnd = defaultAbsolute?.end ? defaultAbsolute.end : new Date(); + return { + ...current, + // Update default values for absolute selector + start: start ? getInternalDate(start, utc) : defaultStart, + end: end ? getInternalDate(end, utc) : defaultEnd, + }; + }); + setShowAbsoluteSelector(true); + return; + } - setInternalValue(current => ({...current, relative: option.value})); - onChange?.({relative: option.value, start: undefined, end: undefined}); - }, - [start, end, utc, defaultAbsolute, defaultPeriod, relative, onChange] - ); + setInternalValue(current => ({...current, relative: option.value})); + onChange?.({relative: option.value, start: undefined, end: undefined}); + }; const arbitraryRelativePeriods = getArbitraryRelativePeriod(relative); diff --git a/static/app/components/webAuthn/webAuthnEnroll.tsx b/static/app/components/webAuthn/webAuthnEnroll.tsx index 182082f7e3d6fc..d856b40b204472 100644 --- a/static/app/components/webAuthn/webAuthnEnroll.tsx +++ b/static/app/components/webAuthn/webAuthnEnroll.tsx @@ -1,4 +1,4 @@ -import {useCallback, useState} from 'react'; +import {useState} from 'react'; import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; @@ -26,29 +26,26 @@ export function WebAuthnEnroll({challengeData}: WebAuthnEnrollProps) { const [activated, setActivated] = useState(false); - const triggerEnroll = useCallback( - async (model: FormModel) => { - setActivated(false); - model.setError('challenge', false); - - try { - const webAuthnResponse = await handleEnroll(challengeData); + const triggerEnroll = async (model: FormModel) => { + setActivated(false); + model.setError('challenge', false); - if (!webAuthnResponse) { - model.setError('challenge', FAILURE_MESSAGE); - return; - } + try { + const webAuthnResponse = await handleEnroll(challengeData); - setActivated(true); - model.setValue('response', webAuthnResponse); - model.setValue('challenge', challenge); - } catch (err) { + if (!webAuthnResponse) { model.setError('challenge', FAILURE_MESSAGE); - setActivated(false); + return; } - }, - [challenge, challengeData] - ); + + setActivated(true); + model.setValue('response', webAuthnResponse); + model.setValue('challenge', challenge); + } catch (err) { + model.setError('challenge', FAILURE_MESSAGE); + setActivated(false); + } + }; return ( { + const handleClick = () => { ConfigStore.set('theme', isDark ? 'light' : 'dark'); - }, [isDark]); + }; return ( ; } diff --git a/static/app/views/seerExplorer/blockComponents.tsx b/static/app/views/seerExplorer/blockComponents.tsx index df4a7d9cb7f29b..ce3bb39d4b2e45 100644 --- a/static/app/views/seerExplorer/blockComponents.tsx +++ b/static/app/views/seerExplorer/blockComponents.tsx @@ -287,33 +287,23 @@ export function BlockComponent({ false ); - const trackThumbsFeedback = useCallback( - (type: 'positive' | 'negative') => { - // Guard against missing runId (shouldn't happen with showActions check, but be defensive) - // Do this instead of hiding buttons to prevent flickering while data's loading for this edge case. - if (!feedbackSubmitted && runId !== undefined) { - trackAnalytics('seer.explorer.feedback_submitted', { - organization, - type, - run_id: runId, - block_index: blockIndex, - block_message: block.message.content.slice(0, 100), - langfuse_url: getLangfuseUrl(runId), - explorer_url: getExplorerUrl(runId), - conversations_url: getConversationsUrl('sentry', runId), - }); - setFeedbackSubmitted(true); // disable button for rest of the session - } - }, - [ - organization, - blockIndex, - runId, - block.message.content, - feedbackSubmitted, - setFeedbackSubmitted, - ] - ); + const trackThumbsFeedback = (type: 'positive' | 'negative') => { + // Guard against missing runId (shouldn't happen with showActions check, but be defensive) + // Do this instead of hiding buttons to prevent flickering while data's loading for this edge case. + if (!feedbackSubmitted && runId !== undefined) { + trackAnalytics('seer.explorer.feedback_submitted', { + organization, + type, + run_id: runId, + block_index: blockIndex, + block_message: block.message.content.slice(0, 100), + langfuse_url: getLangfuseUrl(runId), + explorer_url: getExplorerUrl(runId), + conversations_url: getConversationsUrl('sentry', runId), + }); + setFeedbackSubmitted(true); // disable button for rest of the session + } + }; const thumbsFeedbackButton = (type: 'positive' | 'negative') => { const ariaLabel = diff --git a/static/app/views/settings/components/dataScrubbing/modals/dataScrubFormModal.tsx b/static/app/views/settings/components/dataScrubbing/modals/dataScrubFormModal.tsx index 25d9ee24af70e4..fdf7f8259a08b3 100644 --- a/static/app/views/settings/components/dataScrubbing/modals/dataScrubFormModal.tsx +++ b/static/app/views/settings/components/dataScrubbing/modals/dataScrubFormModal.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useState} from 'react'; +import {Fragment, useState} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import sortBy from 'lodash/sortBy'; @@ -163,23 +163,20 @@ export function DataScrubFormModal({ const type = useStore(form.store, state => state.values.type); - const handleValidateAttributeField = useCallback( - (value: string) => { - const traceItemField = TraceItemFieldSelector.fromField(dataset, value); + const handleValidateAttributeField = (value: string) => { + const traceItemField = TraceItemFieldSelector.fromField(dataset, value); - if (!traceItemField) { - return; - } + if (!traceItemField) { + return; + } - const validation = validateTraceItemFieldSelector(traceItemField); - if (!validation.isValid && validation.error) { - setFieldErrors(form, { - source: {message: validation.error}, - }); - } - }, - [dataset, form] - ); + const validation = validateTraceItemFieldSelector(traceItemField); + if (!validation.isValid && validation.error) { + setFieldErrors(form, { + source: {message: validation.error}, + }); + } + }; const containsRootDeepWildcard = (source: string) => { return /(^|[^.])\*\*$/.test(source); diff --git a/static/app/views/settings/components/settingsBreadcrumb/breadcrumbDropdown.tsx b/static/app/views/settings/components/settingsBreadcrumb/breadcrumbDropdown.tsx index 24ac941feb4277..f02ea1b382379b 100644 --- a/static/app/views/settings/components/settingsBreadcrumb/breadcrumbDropdown.tsx +++ b/static/app/views/settings/components/settingsBreadcrumb/breadcrumbDropdown.tsx @@ -112,11 +112,11 @@ function MenuCrumb({crumbLabel, menuHasHover, isLast, ...props}: MenuCrumbProps) closeTimeoutRef.current = window.setTimeout(() => close?.(), CLOSE_MENU_TIMEOUT); }, [close]); - const handleOpen = useCallback(() => { + const handleOpen = () => { activeCrumbStates.forEach(state => state?.close()); window.clearTimeout(closeTimeoutRef.current); open?.(); - }, [open]); + }; useEffect(() => { if (menuHasHover) { diff --git a/static/app/views/settings/featureFlags/changeTracking/newProviderForm.tsx b/static/app/views/settings/featureFlags/changeTracking/newProviderForm.tsx index 429af805dbe652..93883d2592f99a 100644 --- a/static/app/views/settings/featureFlags/changeTracking/newProviderForm.tsx +++ b/static/app/views/settings/featureFlags/changeTracking/newProviderForm.tsx @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import {z} from 'zod'; import {Button} from '@sentry/scraps/button'; @@ -68,11 +67,11 @@ export function NewProviderForm({ const queryClient = useQueryClient(); const navigate = useNavigate(); - const handleGoBack = useCallback(() => { + const handleGoBack = () => { navigate( normalizeUrl(`/settings/${organization.slug}/feature-flags/change-tracking/`) ); - }, [organization.slug, navigate]); + }; const mutation = useMutation({ mutationFn: ({provider, secret}: CreateSecretData) => { diff --git a/static/app/views/settings/organizationAuthTokens/newAuthToken.tsx b/static/app/views/settings/organizationAuthTokens/newAuthToken.tsx index 64af028cdc9004..31bc016641e4eb 100644 --- a/static/app/views/settings/organizationAuthTokens/newAuthToken.tsx +++ b/static/app/views/settings/organizationAuthTokens/newAuthToken.tsx @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import styled from '@emotion/styled'; import {z} from 'zod'; @@ -43,10 +42,7 @@ function AuthTokenCreateForm({ const navigate = useNavigate(); const queryClient = useQueryClient(); - const handleGoBack = useCallback( - () => navigate(`/settings/${organization.slug}/auth-tokens/`), - [navigate, organization.slug] - ); + const handleGoBack = () => navigate(`/settings/${organization.slug}/auth-tokens/`); const mutation = useMutation< OrgAuthTokenWithToken, @@ -132,10 +128,7 @@ export default function OrganizationAuthTokensNewAuthToken() { const organization = useOrganization(); const navigate = useNavigate(); - const handleGoBack = useCallback( - () => navigate(`/settings/${organization.slug}/auth-tokens/`), - [navigate, organization.slug] - ); + const handleGoBack = () => navigate(`/settings/${organization.slug}/auth-tokens/`); return (
diff --git a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/requestLog.tsx b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/requestLog.tsx index 4af52bd6160e7a..23f1f4b59a4356 100644 --- a/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/requestLog.tsx +++ b/static/app/views/settings/organizationDeveloperSettings/sentryApplicationDashboard/requestLog.tsx @@ -170,14 +170,11 @@ export function RequestLog({app}: RequestLogProps) { const hasPrevPage = useMemo(() => currentPage > 0, [currentPage]); - const handleChangeEventType = useCallback( - (newEventType: string) => { - setEventType(newEventType); - setCurrentPage(0); - refetch(); - }, - [refetch] - ); + const handleChangeEventType = (newEventType: string) => { + setEventType(newEventType); + setCurrentPage(0); + refetch(); + }; const handleChangeErrorsOnly = useCallback(() => { setErrorsOnly(!errorsOnly); @@ -185,13 +182,13 @@ export function RequestLog({app}: RequestLogProps) { refetch(); }, [errorsOnly, refetch]); - const handleNextPage = useCallback(() => { + const handleNextPage = () => { setCurrentPage(currentPage + 1); - }, [currentPage]); + }; - const handlePrevPage = useCallback(() => { + const handlePrevPage = () => { setCurrentPage(currentPage - 1); - }, [currentPage]); + }; return ( diff --git a/static/app/views/settings/organizationIntegrations/SplitInstallationIdModal.tsx b/static/app/views/settings/organizationIntegrations/SplitInstallationIdModal.tsx index 4c33c1f700ec29..6f9d1ffe39707c 100644 --- a/static/app/views/settings/organizationIntegrations/SplitInstallationIdModal.tsx +++ b/static/app/views/settings/organizationIntegrations/SplitInstallationIdModal.tsx @@ -30,7 +30,7 @@ export function SplitInstallationIdModal(props: Props) { await navigator.clipboard.writeText(props.installationId); }, [props.installationId]); - const handleContinue = useCallback(() => { + const handleContinue = () => { onCopy(); addSuccessMessage('Copied to clipboard'); @@ -39,7 +39,7 @@ export function SplitInstallationIdModal(props: Props) { openAdminIntegrationTimeoutRef.current = window.setTimeout(() => { window.open('https://app.split.io/org/admin/integrations'); }, 2000); - }, [onCopy]); + }; // no need to translate this temporary component return ( diff --git a/static/app/views/settings/organizationRelay/relayWrapper.tsx b/static/app/views/settings/organizationRelay/relayWrapper.tsx index efdbc53e09a774..e9393c5a677dda 100644 --- a/static/app/views/settings/organizationRelay/relayWrapper.tsx +++ b/static/app/views/settings/organizationRelay/relayWrapper.tsx @@ -1,4 +1,4 @@ -import {useCallback, useState} from 'react'; +import {useState} from 'react'; import omit from 'lodash/omit'; import {z} from 'zod'; @@ -42,7 +42,7 @@ export function RelayWrapper() { const disabled = !organization.access.includes('org:write'); - const handleOpenAddDialog = useCallback(() => { + const handleOpenAddDialog = () => { openModal(modalProps => ( )); - }, [relays, api, organization.slug]); + }; return ( @@ -172,50 +172,44 @@ function RelayUsageList({ } ); - const handleOpenEditDialog = useCallback( - (publicKey: string) => { - const editRelay = relays.find(relay => relay.publicKey === publicKey); + const handleOpenEditDialog = (publicKey: string) => { + const editRelay = relays.find(relay => relay.publicKey === publicKey); - if (!editRelay) { - return; - } - - openModal(modalProps => ( - { - addSuccessMessage(t('Successfully updated Relay public key')); - onRelaysChange(response.trustedRelays); - }} - /> - )); - }, - [orgSlug, relays, api, onRelaysChange] - ); + if (!editRelay) { + return; + } - const handleDeleteRelay = useCallback( - async (publicKey: string) => { - const trustedRelays = relays - .filter(relay => relay.publicKey !== publicKey) - .map(relay => omit(relay, ['created', 'lastModified'])); - - try { - const response = await api.requestPromise(`/organizations/${orgSlug}/`, { - method: 'PUT', - data: {trustedRelays}, - }); - addSuccessMessage(t('Successfully deleted Relay public key')); - onRelaysChange(response.trustedRelays); - } catch { - addErrorMessage(t('An unknown error occurred while deleting Relay public key')); - } - }, - [relays, api, orgSlug, onRelaysChange] - ); + openModal(modalProps => ( + { + addSuccessMessage(t('Successfully updated Relay public key')); + onRelaysChange(response.trustedRelays); + }} + /> + )); + }; + + const handleDeleteRelay = async (publicKey: string) => { + const trustedRelays = relays + .filter(relay => relay.publicKey !== publicKey) + .map(relay => omit(relay, ['created', 'lastModified'])); + + try { + const response = await api.requestPromise(`/organizations/${orgSlug}/`, { + method: 'PUT', + data: {trustedRelays}, + }); + addSuccessMessage(t('Successfully deleted Relay public key')); + onRelaysChange(response.trustedRelays); + } catch { + addErrorMessage(t('An unknown error occurred while deleting Relay public key')); + } + }; if (isPending) { return ; diff --git a/static/app/views/settings/project/preprod/statusCheckRuleForm.tsx b/static/app/views/settings/project/preprod/statusCheckRuleForm.tsx index 6f774b6b63230e..44f41bdf81483a 100644 --- a/static/app/views/settings/project/preprod/statusCheckRuleForm.tsx +++ b/static/app/views/settings/project/preprod/statusCheckRuleForm.tsx @@ -1,4 +1,4 @@ -import {useCallback, useState} from 'react'; +import {useState} from 'react'; import styled from '@emotion/styled'; import {Button} from '@sentry/scraps/button'; @@ -65,9 +65,9 @@ export function StatusCheckRuleForm({rule, onSave, onDelete}: Props) { }); }; - const handleQueryChange = useCallback((query: string) => { + const handleQueryChange = (query: string) => { setFilterQuery(query); - }, []); + }; const handleDelete = () => { const ruleDisplayValue = diff --git a/static/app/views/settings/project/preprod/statusCheckRules.tsx b/static/app/views/settings/project/preprod/statusCheckRules.tsx index 6e9eeaf8b55474..ebcd69a984ca23 100644 --- a/static/app/views/settings/project/preprod/statusCheckRules.tsx +++ b/static/app/views/settings/project/preprod/statusCheckRules.tsx @@ -71,21 +71,18 @@ export function StatusCheckRules() { [location.query, navigate] ); - const handleToggleExpanded = useCallback( - (ruleId: string, isExpanded: boolean) => { - const newExpanded = new Set(expandedRuleIds); - if (isExpanded) { - newExpanded.add(ruleId); - } else { - newExpanded.delete(ruleId); - if (ruleId === newRuleId) { - setNewRuleId(null); - } + const handleToggleExpanded = (ruleId: string, isExpanded: boolean) => { + const newExpanded = new Set(expandedRuleIds); + if (isExpanded) { + newExpanded.add(ruleId); + } else { + newExpanded.delete(ruleId); + if (ruleId === newRuleId) { + setNewRuleId(null); } - updateExpandedInUrl([...newExpanded]); - }, - [expandedRuleIds, newRuleId, updateExpandedInUrl] - ); + } + updateExpandedInUrl([...newExpanded]); + }; const hasRepositories = !isLoadingRepos && repositories && repositories.length > 0; diff --git a/static/app/views/settings/project/projectFilters/projectFiltersSettings.tsx b/static/app/views/settings/project/projectFilters/projectFiltersSettings.tsx index 7e527f79d2ea12..48a2bb35100242 100644 --- a/static/app/views/settings/project/projectFilters/projectFiltersSettings.tsx +++ b/static/app/views/settings/project/projectFilters/projectFiltersSettings.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useState} from 'react'; +import {Fragment, useState} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import iconAndroid from 'sentry-logos/logo-android.svg'; @@ -415,23 +415,20 @@ export function ProjectFiltersSettings({project, params, features}: Props) { const filterList = filterListData ?? []; - const handleLegacyChange = useCallback( - ({ - onChange, - onBlur, - event, - subfilters, - }: { - event: React.ChangeEvent | React.MouseEvent; - onBlur: FormFieldProps['onBlur']; - onChange: FormFieldProps['onChange']; - subfilters: Set; - }) => { - onChange?.(subfilters, event); - onBlur?.(subfilters, event); - }, - [] - ); + const handleLegacyChange = ({ + onChange, + onBlur, + event, + subfilters, + }: { + event: React.ChangeEvent | React.MouseEvent; + onBlur: FormFieldProps['onBlur']; + onChange: FormFieldProps['onChange']; + subfilters: Set; + }) => { + onChange?.(subfilters, event); + onBlur?.(subfilters, event); + }; if (isPending) { return ; diff --git a/static/app/views/settings/project/projectKeys/details/keySettings.tsx b/static/app/views/settings/project/projectKeys/details/keySettings.tsx index 58200b1ab90f14..23a4056de174c8 100644 --- a/static/app/views/settings/project/projectKeys/details/keySettings.tsx +++ b/static/app/views/settings/project/projectKeys/details/keySettings.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback} from 'react'; +import {Fragment} from 'react'; import {Button} from '@sentry/scraps/button'; @@ -51,7 +51,7 @@ export function KeySettings({ const {keyId, projectId} = params; const apiEndpoint = `/projects/${organization.slug}/${projectId}/keys/${keyId}/`; - const handleRemove = useCallback(async () => { + const handleRemove = async () => { addLoadingMessage(t('Revoking key\u2026')); try { @@ -67,7 +67,7 @@ export function KeySettings({ } catch (_err) { addErrorMessage(t('Unable to revoke key')); } - }, [organization, api, onRemove, keyId, projectId]); + }; const showOtlpTraces = organization.features.includes('relay-otlp-traces-endpoint'); const showOtlpLogs = organization.features.includes('relay-otel-logs-endpoint'); diff --git a/static/app/views/settings/project/projectKeys/details/loaderSettings.tsx b/static/app/views/settings/project/projectKeys/details/loaderSettings.tsx index 8cedf6fe164059..7cfd910e18e6ec 100644 --- a/static/app/views/settings/project/projectKeys/details/loaderSettings.tsx +++ b/static/app/views/settings/project/projectKeys/details/loaderSettings.tsx @@ -1,4 +1,4 @@ -import {Fragment, useCallback, useState} from 'react'; +import {Fragment, useState} from 'react'; import {ExternalLink} from '@sentry/scraps/link'; @@ -63,88 +63,74 @@ export function LoaderSettings({keyId, orgSlug, project, data, updateData}: Prop const apiEndpoint = `/projects/${orgSlug}/${project.slug}/keys/${keyId}/`; const loaderLink = data.dsn.cdn; - const updateLoaderOption = useCallback( - async (changes: { - browserSdkVersion?: string; - hasDebug?: boolean; - hasFeedback?: boolean; - hasLogsAndMetrics?: boolean; - hasPerformance?: boolean; - hasReplay?: boolean; - }) => { - setRequestPending(true); - setOptimisticState({ - browserSdkVersion: data.browserSdkVersion, - hasDebug: data.dynamicSdkLoaderOptions.hasDebug, - hasLogsAndMetrics: data.dynamicSdkLoaderOptions.hasLogsAndMetrics, - hasFeedback: data.dynamicSdkLoaderOptions.hasFeedback, - hasPerformance: data.dynamicSdkLoaderOptions.hasPerformance, - hasReplay: data.dynamicSdkLoaderOptions.hasReplay, - ...changes, - }); - addLoadingMessage(); + const updateLoaderOption = async (changes: { + browserSdkVersion?: string; + hasDebug?: boolean; + hasFeedback?: boolean; + hasLogsAndMetrics?: boolean; + hasPerformance?: boolean; + hasReplay?: boolean; + }) => { + setRequestPending(true); + setOptimisticState({ + browserSdkVersion: data.browserSdkVersion, + hasDebug: data.dynamicSdkLoaderOptions.hasDebug, + hasLogsAndMetrics: data.dynamicSdkLoaderOptions.hasLogsAndMetrics, + hasFeedback: data.dynamicSdkLoaderOptions.hasFeedback, + hasPerformance: data.dynamicSdkLoaderOptions.hasPerformance, + hasReplay: data.dynamicSdkLoaderOptions.hasReplay, + ...changes, + }); + addLoadingMessage(); - const browserSdkVersion = changes.browserSdkVersion ?? data.browserSdkVersion; + const browserSdkVersion = changes.browserSdkVersion ?? data.browserSdkVersion; - let payload: any; - if (sdkVersionSupportsPerformanceAndReplay(browserSdkVersion)) { - payload = { - browserSdkVersion, - dynamicSdkLoaderOptions: { - hasDebug: changes.hasDebug ?? data.dynamicSdkLoaderOptions.hasDebug, - hasLogsAndMetrics: sdkVersionSupportsLogsAndMetrics(browserSdkVersion) - ? (changes.hasLogsAndMetrics ?? - data.dynamicSdkLoaderOptions.hasLogsAndMetrics) - : false, - hasFeedback: changes.hasFeedback ?? data.dynamicSdkLoaderOptions.hasFeedback, - hasPerformance: - changes.hasPerformance ?? data.dynamicSdkLoaderOptions.hasPerformance, - hasReplay: changes.hasReplay ?? data.dynamicSdkLoaderOptions.hasReplay, - }, - }; - } else { - payload = { - browserSdkVersion, - dynamicSdkLoaderOptions: { - hasDebug: changes.hasDebug ?? data.dynamicSdkLoaderOptions.hasDebug, - hasLogsAndMetrics: false, - hasFeedback: false, - hasPerformance: false, - hasReplay: false, - }, - }; - } + let payload: any; + if (sdkVersionSupportsPerformanceAndReplay(browserSdkVersion)) { + payload = { + browserSdkVersion, + dynamicSdkLoaderOptions: { + hasDebug: changes.hasDebug ?? data.dynamicSdkLoaderOptions.hasDebug, + hasLogsAndMetrics: sdkVersionSupportsLogsAndMetrics(browserSdkVersion) + ? (changes.hasLogsAndMetrics ?? + data.dynamicSdkLoaderOptions.hasLogsAndMetrics) + : false, + hasFeedback: changes.hasFeedback ?? data.dynamicSdkLoaderOptions.hasFeedback, + hasPerformance: + changes.hasPerformance ?? data.dynamicSdkLoaderOptions.hasPerformance, + hasReplay: changes.hasReplay ?? data.dynamicSdkLoaderOptions.hasReplay, + }, + }; + } else { + payload = { + browserSdkVersion, + dynamicSdkLoaderOptions: { + hasDebug: changes.hasDebug ?? data.dynamicSdkLoaderOptions.hasDebug, + hasLogsAndMetrics: false, + hasFeedback: false, + hasPerformance: false, + hasReplay: false, + }, + }; + } - try { - const response = await api.requestPromise(apiEndpoint, { - method: 'PUT', - data: payload, - }); + try { + const response = await api.requestPromise(apiEndpoint, { + method: 'PUT', + data: payload, + }); - updateData(response); + updateData(response); - addSuccessMessage(t('Successfully updated dynamic SDK loader configuration')); - } catch (error) { - const message = t('Unable to updated dynamic SDK loader configuration'); - handleXhrErrorResponse(message, error as RequestError); - addErrorMessage(message); - } finally { - setRequestPending(false); - } - }, - [ - api, - apiEndpoint, - data.browserSdkVersion, - data.dynamicSdkLoaderOptions.hasDebug, - data.dynamicSdkLoaderOptions.hasLogsAndMetrics, - data.dynamicSdkLoaderOptions.hasFeedback, - data.dynamicSdkLoaderOptions.hasPerformance, - data.dynamicSdkLoaderOptions.hasReplay, - setRequestPending, - updateData, - ] - ); + addSuccessMessage(t('Successfully updated dynamic SDK loader configuration')); + } catch (error) { + const message = t('Unable to updated dynamic SDK loader configuration'); + handleXhrErrorResponse(message, error as RequestError); + addErrorMessage(message); + } finally { + setRequestPending(false); + } + }; return ( diff --git a/static/app/views/settings/projectSeer/autofixRepositories.tsx b/static/app/views/settings/projectSeer/autofixRepositories.tsx index 13598f786ad67a..fa578571cc4340 100644 --- a/static/app/views/settings/projectSeer/autofixRepositories.tsx +++ b/static/app/views/settings/projectSeer/autofixRepositories.tsx @@ -177,32 +177,26 @@ export function AutofixRepositories({project}: ProjectSeerProps) { [updatePreferences] ); - const removeRepository = useCallback( - (repoId: string) => { - setSelectedRepoIds(prevSelectedIds => { - const newIds = prevSelectedIds.filter(id => id !== repoId); - updatePreferences(newIds); - return newIds; - }); - }, - [updatePreferences] - ); - - const updateRepoSettings = useCallback( - (repoId: string, settings: RepoSettings) => { - setRepoSettings(prev => { - const newSettings = { - ...prev, - [repoId]: settings, - }; + const removeRepository = (repoId: string) => { + setSelectedRepoIds(prevSelectedIds => { + const newIds = prevSelectedIds.filter(id => id !== repoId); + updatePreferences(newIds); + return newIds; + }); + }; + + const updateRepoSettings = (repoId: string, settings: RepoSettings) => { + setRepoSettings(prev => { + const newSettings = { + ...prev, + [repoId]: settings, + }; - updatePreferences(undefined, newSettings); + updatePreferences(undefined, newSettings); - return newSettings; - }); - }, - [updatePreferences] - ); + return newSettings; + }); + }; const {unselectedRepositories, filteredSelectedRepositories} = useMemo(() => { if (!repositories || repositories.length === 0) { @@ -231,7 +225,7 @@ export function AutofixRepositories({project}: ProjectSeerProps) { const isRepoLimitReached = selectedRepoIds.length >= MAX_REPOS_LIMIT; - const openAddRepoModal = useCallback(() => { + const openAddRepoModal = () => { openModal(deps => ( )); - }, [selectedRepoIds, handleSaveModalSelections]); + }; return ( diff --git a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx index 082a75e3a8d68c..21c3ac773cf3d4 100644 --- a/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx +++ b/static/app/views/settings/seer/overview/codeReviewOverviewSection.tsx @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import {mutationOptions} from '@tanstack/react-query'; import uniqBy from 'lodash/uniqBy'; import {z} from 'zod'; @@ -20,8 +19,7 @@ import {DEFAULT_CODE_REVIEW_TRIGGERS} from 'sentry/types/integrations'; import type {Organization} from 'sentry/types/organization'; import {useFetchAllPages} from 'sentry/utils/api/apiFetch'; import {getSeerOnboardingCheckQueryOptions} from 'sentry/utils/getSeerOnboardingCheckQueryOptions'; -import {useInfiniteQuery, useQueryClient} from 'sentry/utils/queryClient'; -import {fetchMutation} from 'sentry/utils/queryClient'; +import {fetchMutation, useInfiniteQuery, useQueryClient} from 'sentry/utils/queryClient'; import {organizationRepositoriesWithSettingsInfiniteOptions} from 'sentry/utils/repositories/repoQueryOptions'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -108,71 +106,65 @@ export function CodeReviewOverviewSection({ }, }); - const handleToggleCodeReview = useCallback( - (enabledCodeReview: boolean) => { - const repositoryIds = ( - enabledCodeReview - ? seerRepos.filter(repo => !repo.settings?.enabledCodeReview) - : reposWithCodeReview - ).map(repo => repo.id); - mutateRepositorySettings( - {enabledCodeReview, repositoryIds}, - { - onError: (_, variables) => { - addErrorMessage( - tn( - 'Failed to update code review for %s repository', - 'Failed to update code review for %s repositories', - variables.repositoryIds.length - ) - ); - }, - onSuccess: (_, variables) => { - addSuccessMessage( - tn( - 'Code review updated for %s repository', - 'Code review updated for %s repositories', - variables.repositoryIds.length - ) - ); - }, - } - ); - }, - [mutateRepositorySettings, reposWithCodeReview, seerRepos] - ); + const handleToggleCodeReview = (enabledCodeReview: boolean) => { + const repositoryIds = ( + enabledCodeReview + ? seerRepos.filter(repo => !repo.settings?.enabledCodeReview) + : reposWithCodeReview + ).map(repo => repo.id); + mutateRepositorySettings( + {enabledCodeReview, repositoryIds}, + { + onError: (_, variables) => { + addErrorMessage( + tn( + 'Failed to update code review for %s repository', + 'Failed to update code review for %s repositories', + variables.repositoryIds.length + ) + ); + }, + onSuccess: (_, variables) => { + addSuccessMessage( + tn( + 'Code review updated for %s repository', + 'Code review updated for %s repositories', + variables.repositoryIds.length + ) + ); + }, + } + ); + }; - const handleChangeTriggers = useCallback( - (newTriggers: string[]) => { - mutateRepositorySettings( - { - codeReviewTriggers: newTriggers, - repositoryIds: seerRepos.map(repo => repo.id), + const handleChangeTriggers = (newTriggers: string[]) => { + mutateRepositorySettings( + { + codeReviewTriggers: newTriggers, + repositoryIds: seerRepos.map(repo => repo.id), + }, + { + onError: (_, variables) => { + addErrorMessage( + tn( + 'Failed to update triggers for %s repository', + 'Failed to update triggers for %s repositories', + variables.repositoryIds.length + ) + ); }, - { - onError: (_, variables) => { - addErrorMessage( - tn( - 'Failed to update triggers for %s repository', - 'Failed to update triggers for %s repositories', - variables.repositoryIds.length - ) - ); - }, - onSuccess: (_, variables) => { - addSuccessMessage( - tn( - 'Triggers updated for %s repository', - 'Triggers updated for %s repositories', - variables.repositoryIds.length - ) - ); - }, - } - ); - }, - [mutateRepositorySettings, seerRepos] - ); + onSuccess: (_, variables) => { + addSuccessMessage( + tn( + 'Triggers updated for %s repository', + 'Triggers updated for %s repositories', + variables.repositoryIds.length + ) + ); + }, + } + ); + }; return ( { - event.preventDefault(); - if (!isFormValid || !selectedOrg || !selectedProjectId) { - return; - } - - let projectId = selectedProjectId; - try { - if (isCreateProjectSelected) { - const project = await createProjectMutation.mutateAsync({ - organization: selectedOrg, - team: newProjectTeam, - name: newProjectName, - platform: newProjectPlatform || platformParam || 'other', - }); - - projectId = project.id; - } - } catch { - addErrorMessage('Failed to create project! Please try again'); - return; - } + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (!isFormValid || !selectedOrg || !selectedProjectId) { + return; + } - try { - await updateWizardCacheMutation.mutateAsync({ - organizationId: selectedOrg.id, - projectId, + let projectId = selectedProjectId; + try { + if (isCreateProjectSelected) { + const project = await createProjectMutation.mutateAsync({ + organization: selectedOrg, + team: newProjectTeam, + name: newProjectName, + platform: newProjectPlatform || platformParam || 'other', }); - } catch (e) { - const errorMessage = errorIsHasNoDsnError(e) - ? t( - 'The selected project has no active DSN. Please add an active DSN to the project.' - ) - : t('Something went wrong! Please try again.'); - - addErrorMessage(errorMessage); + + projectId = project.id; } - }, - [ - isFormValid, - selectedOrg, - selectedProjectId, - isCreateProjectSelected, - createProjectMutation, - newProjectTeam, - newProjectName, - newProjectPlatform, - updateWizardCacheMutation, - ] - ); + } catch { + addErrorMessage('Failed to create project! Please try again'); + return; + } + + try { + await updateWizardCacheMutation.mutateAsync({ + organizationId: selectedOrg.id, + projectId, + }); + } catch (e) { + const errorMessage = errorIsHasNoDsnError(e) + ? t( + 'The selected project has no active DSN. Please add an active DSN to the project.' + ) + : t('Something went wrong! Please try again.'); + + addErrorMessage(errorMessage); + } + }; if (isSuccess) { return ; diff --git a/static/eslint/eslintPluginSentry/index.ts b/static/eslint/eslintPluginSentry/index.ts index b4f6a5d5d0fd20..937ad18fff6192 100644 --- a/static/eslint/eslintPluginSentry/index.ts +++ b/static/eslint/eslintPluginSentry/index.ts @@ -5,6 +5,7 @@ import {noFlagComments} from './no-flag-comments'; import {noStaticTranslations} from './no-static-translations'; import {noStyledShortcut} from './no-styled-shortcut'; import {noUnnecessaryTypeAnnotation} from './no-unnecessary-type-annotation'; +import {noUnnecessaryUseCallback} from './no-unnecessary-use-callback'; export const rules = { 'no-default-exports': noDefaultExports, @@ -14,4 +15,5 @@ export const rules = { 'no-static-translations': noStaticTranslations, 'no-styled-shortcut': noStyledShortcut, 'no-unnecessary-type-annotation': noUnnecessaryTypeAnnotation, + 'no-unnecessary-use-callback': noUnnecessaryUseCallback, }; diff --git a/static/eslint/eslintPluginSentry/no-unnecessary-use-callback.spec.ts b/static/eslint/eslintPluginSentry/no-unnecessary-use-callback.spec.ts new file mode 100644 index 00000000000000..ea9a3edf2a2472 --- /dev/null +++ b/static/eslint/eslintPluginSentry/no-unnecessary-use-callback.spec.ts @@ -0,0 +1,521 @@ +import {RuleTester} from '@typescript-eslint/rule-tester'; + +import {noUnnecessaryUseCallback} from './no-unnecessary-use-callback'; + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + ecmaFeatures: {jsx: true}, + }, + }, +}); + +ruleTester.run('no-unnecessary-use-callback', noUnnecessaryUseCallback, { + valid: [ + { + name: 'useCallback passed directly to custom component', + code: ` + const fn = useCallback(() => { + console.log('click'); + }, []); + + `, + }, + { + name: 'regular function passed to intrinsic element', + code: ` + const fn = () => console.log('click'); +