From 502956b75fa6797e1a82d0d8ddb192e50683e92d Mon Sep 17 00:00:00 2001 From: Mathieu Suen Date: Tue, 17 Sep 2024 11:29:45 +0200 Subject: [PATCH] CODEFIX-33 Remove the get fix button when feature is not available --- .../src/main/js/api/fix-suggestions.ts | 9 +++++ .../main/js/api/mocks/FixIssueServiceMock.ts | 12 +++++-- .../js/apps/issues/__tests__/IssueApp-it.tsx | 16 +++++++-- .../ComponentSourceSnippetGroupViewer.tsx | 35 +++++++------------ .../js/components/rules/IssueTabViewer.tsx | 18 +++++----- ...fix-suggestions.ts => fix-suggestions.tsx} | 32 ++++++++++++++++- 6 files changed, 87 insertions(+), 35 deletions(-) rename server/sonar-web/src/main/js/queries/{fix-suggestions.ts => fix-suggestions.tsx} (76%) diff --git a/server/sonar-web/src/main/js/api/fix-suggestions.ts b/server/sonar-web/src/main/js/api/fix-suggestions.ts index 84570ff31e1..f4e582f343f 100644 --- a/server/sonar-web/src/main/js/api/fix-suggestions.ts +++ b/server/sonar-web/src/main/js/api/fix-suggestions.ts @@ -24,6 +24,15 @@ export interface FixParam { issueId: string; } +export interface AiIssue { + aiSuggestion: 'AVAILABLE' | 'NOT_AVAILABLE_FILE_LEVEL_ISSUE' | 'NOT_AVAILABLE_UNSUPPORTED_RULE'; + id: string; +} + export function getSuggestions(data: FixParam): Promise { return axiosToCatch.post('/api/v2/fix-suggestions/ai-suggestions', data); } + +export function getFixSuggestionsIssues(data: FixParam): Promise { + return axiosToCatch.get(`/api/v2/fix-suggestions/issues/${data.issueId}`); +} diff --git a/server/sonar-web/src/main/js/api/mocks/FixIssueServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/FixIssueServiceMock.ts index be6dfcfcf69..ce7da44af52 100644 --- a/server/sonar-web/src/main/js/api/mocks/FixIssueServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/FixIssueServiceMock.ts @@ -18,8 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { cloneDeep } from 'lodash'; -import { FixParam, getSuggestions } from '../fix-suggestions'; -import { ISSUE_101 } from './data/ids'; +import { FixParam, getFixSuggestionsIssues, getSuggestions } from '../fix-suggestions'; +import { ISSUE_101, ISSUE_1101 } from './data/ids'; jest.mock('../fix-suggestions'); @@ -40,8 +40,16 @@ export default class FixIssueServiceMock { constructor() { jest.mocked(getSuggestions).mockImplementation(this.handleGetFixSuggestion); + jest.mocked(getFixSuggestionsIssues).mockImplementation(this.handleGetFixSuggestionsIssues); } + handleGetFixSuggestionsIssues = (data: FixParam) => { + if (data.issueId === ISSUE_1101) { + return this.reply({ aiSuggestion: 'NOT_AVAILABLE_FILE_LEVEL_ISSUE', id: 'id1' } as const); + } + return this.reply({ aiSuggestion: 'AVAILABLE', id: 'id1' } as const); + }; + handleGetFixSuggestion = (data: FixParam) => { if (data.issueId === ISSUE_101) { return Promise.reject({ error: { msg: 'Invalid issue' } }); diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx index d341530762b..4082b39f2a3 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx @@ -22,7 +22,7 @@ import userEvent from '@testing-library/user-event'; import { range } from 'lodash'; import React from 'react'; import { byRole, byText } from '~sonar-aligned/helpers/testSelector'; -import { ISSUE_101 } from '../../../api/mocks/data/ids'; +import { ISSUE_101, ISSUE_1101 } from '../../../api/mocks/data/ids'; import { TabKeys } from '../../../components/rules/RuleTabViewer'; import { mockCurrentUser, mockCve, mockLoggedInUser } from '../../../helpers/testMocks'; import { Feature } from '../../../types/features'; @@ -96,7 +96,7 @@ describe('issue app', () => { [Feature.BranchSupport, Feature.FixSuggestions], ); - expect(await ui.getFixSuggestion.find()).toBeInTheDocument(); + expect(await ui.getFixSuggestion.find(undefined, { timeout: 5000 })).toBeInTheDocument(); await user.click(ui.getFixSuggestion.get()); expect(await ui.suggestedExplanation.find()).toBeInTheDocument(); @@ -118,6 +118,18 @@ describe('issue app', () => { expect(ui.issueCodeFixTab.query()).not.toBeInTheDocument(); }); + it('should not be able to trigger a fix when issue is not eligible', async () => { + renderProjectIssuesApp( + `project/issues?issueStatuses=CONFIRMED&open=${ISSUE_1101}&id=myproject`, + {}, + mockCurrentUser(), + [Feature.BranchSupport, Feature.FixSuggestions], + ); + expect(await ui.issueCodeTab.find()).toBeInTheDocument(); + expect(ui.getFixSuggestion.query()).not.toBeInTheDocument(); + expect(ui.issueCodeFixTab.query()).not.toBeInTheDocument(); + }); + it('should show error when no fix is available', async () => { const user = userEvent.setup(); renderProjectIssuesApp( diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx index ba696076c9d..e6444f83480 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx @@ -25,20 +25,18 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like'; import { getSources } from '../../../api/components'; -import { AvailableFeaturesContext } from '../../../app/components/available-features/AvailableFeaturesContext'; -import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import { TabKeys } from '../../../components/rules/IssueTabViewer'; import { TabSelectorContext } from '../../../components/rules/TabSelectorContext'; import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus'; import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing'; import { translate } from '../../../helpers/l10n'; import { + useGetFixSuggestionsIssuesQuery, usePrefetchSuggestion, useUnifiedSuggestionsQuery, } from '../../../queries/fix-suggestions'; import { BranchLike } from '../../../types/branch-like'; import { isFile } from '../../../types/component'; -import { Feature } from '../../../types/features'; import { IssueDeprecatedStatus } from '../../../types/issues'; import { Dict, @@ -53,7 +51,6 @@ import { SourceViewerFile, Issue as TypeIssue, } from '../../../types/types'; -import { CurrentUser, isLoggedIn } from '../../../types/users'; import { IssueSourceViewerScrollContext } from '../components/IssueSourceViewerScrollContext'; import { IssueSourceViewerHeader } from './IssueSourceViewerHeader'; import SnippetViewer from './SnippetViewer'; @@ -68,7 +65,6 @@ import { interface Props { branchLike: BranchLike | undefined; - currentUser: CurrentUser; duplications?: Duplication[]; duplicationsByLine?: { [line: number]: number[] }; highlightedLocationMessage: { index: number; text: string | undefined } | undefined; @@ -94,7 +90,10 @@ interface State { snippets: Snippet[]; } -class ComponentSourceSnippetGroupViewer extends React.PureComponent, State> { +export default class ComponentSourceSnippetGroupViewer extends React.PureComponent< + Readonly, + State +> { mounted = false; constructor(props: Readonly) { @@ -229,8 +228,7 @@ class ComponentSourceSnippetGroupViewer extends React.PureComponent { - const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup, currentUser } = - this.props; + const { isLastOccurenceOfPrimaryComponent, issue, issuesByLine, snippetGroup } = this.props; const locations = issue.component === snippetGroup.component.key && issue.textRange !== undefined ? locationsByLine([issue]) @@ -267,9 +265,7 @@ class ComponentSourceSnippetGroupViewer extends React.PureComponent - ) : undefined + isSelectedIssue ? : undefined } /> )} @@ -345,7 +341,8 @@ class ComponentSourceSnippetGroupViewer extends React.PureComponent} /> )} @@ -413,21 +410,17 @@ const FileLevelIssueStyle = styled.div` border: 1px solid ${themeColor('codeLineBorder')}; `; -function GetFixButton({ - currentUser, - issue, -}: Readonly<{ currentUser: CurrentUser; issue: Issue }>) { +function GetFixButton({ issue }: Readonly<{ issue: Issue }>) { const handler = React.useContext(TabSelectorContext); const { data: suggestion, isLoading } = useUnifiedSuggestionsQuery(issue, false); const prefetchSuggestion = usePrefetchSuggestion(issue.key); - const isSuggestionFeatureEnabled = React.useContext(AvailableFeaturesContext).includes( - Feature.FixSuggestions, - ); + const { data } = useGetFixSuggestionsIssuesQuery(issue); - if (!isLoggedIn(currentUser) || !isSuggestionFeatureEnabled) { + if (data?.aiSuggestion !== 'AVAILABLE') { return null; } + return ( {suggestion !== undefined && ( @@ -455,5 +448,3 @@ function GetFixButton({ ); } - -export default withCurrentUserContext(ComponentSourceSnippetGroupViewer); diff --git a/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx b/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx index a77a8c25839..15feceeea3b 100644 --- a/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx +++ b/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx @@ -23,7 +23,6 @@ import { cloneDeep, debounce, groupBy } from 'lodash'; import * as React from 'react'; import { Location } from 'react-router-dom'; import { dismissNotice } from '../../api/users'; -import withAvailableFeatures from '../../app/components/available-features/withAvailableFeatures'; import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext'; import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; import { RuleDescriptionSections } from '../../apps/coding-rules/rule'; @@ -32,8 +31,8 @@ import IssueHeader from '../../apps/issues/components/IssueHeader'; import StyledHeader from '../../apps/issues/components/StyledHeader'; import { fillBranchLike } from '../../helpers/branch-like'; import { translate } from '../../helpers/l10n'; +import { withUseGetFixSuggestionsIssues } from '../../queries/fix-suggestions'; import { Cve } from '../../types/cves'; -import { Feature } from '../../types/features'; import { Issue, RuleDetails } from '../../types/types'; import { CurrentUser, NoticeType } from '../../types/users'; import ScreenPositionHelper from '../common/ScreenPositionHelper'; @@ -44,11 +43,11 @@ import { TabSelectorContext } from './TabSelectorContext'; interface IssueTabViewerProps extends CurrentUserContextInterface { activityTabContent?: React.ReactNode; + aiSuggestionAvailable: boolean; codeTabContent?: React.ReactNode; currentUser: CurrentUser; cve?: Cve; extendedDescription?: string; - hasFeature: (feature: string) => boolean; issue: Issue; location: Location; onIssueChange: (issue: Issue) => void; @@ -127,6 +126,7 @@ export class IssueTabViewer extends React.PureComponent this.computeState( @@ -194,7 +195,6 @@ export class IssueTabViewer extends React.PureComponent { const { codeTabContent, - currentUser: { isLoggedIn }, ruleDetails: { descriptionSections, educationPrinciples, lang: ruleLanguage, type: ruleType }, ruleDescriptionContextKey, extendedDescription, @@ -202,7 +202,7 @@ export class IssueTabViewer extends React.PureComponent ), }, - ...(hasFeature(Feature.FixSuggestions) && isLoggedIn + ...(aiSuggestionAvailable ? [ { value: TabKeys.CodeFix, @@ -433,4 +433,6 @@ export class IssueTabViewer extends React.PureComponent(IssueTabViewer)), +); diff --git a/server/sonar-web/src/main/js/queries/fix-suggestions.ts b/server/sonar-web/src/main/js/queries/fix-suggestions.tsx similarity index 76% rename from server/sonar-web/src/main/js/queries/fix-suggestions.ts rename to server/sonar-web/src/main/js/queries/fix-suggestions.tsx index c40d76bf8ec..a235a9101b6 100644 --- a/server/sonar-web/src/main/js/queries/fix-suggestions.ts +++ b/server/sonar-web/src/main/js/queries/fix-suggestions.tsx @@ -19,8 +19,13 @@ */ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { some } from 'lodash'; -import { getSuggestions } from '../api/fix-suggestions'; +import React, { useContext } from 'react'; +import { getFixSuggestionsIssues, getSuggestions } from '../api/fix-suggestions'; +import { useAvailableFeatures } from '../app/components/available-features/withAvailableFeatures'; +import { CurrentUserContext } from '../app/components/current-user/CurrentUserContext'; +import { Feature } from '../types/features'; import { Issue } from '../types/types'; +import { isLoggedIn } from '../types/users'; import { useRawSourceQuery } from './sources'; const UNKNOWN = -1; @@ -132,3 +137,28 @@ export function useUnifiedSuggestionsQuery(issue: Issue, enabled = true) { }, }); } + +export function useGetFixSuggestionsIssuesQuery(issue: Issue) { + const { currentUser } = useContext(CurrentUserContext); + const { hasFeature } = useAvailableFeatures(); + + return useQuery({ + queryKey: ['code-suggestions', 'issues', 'details', issue.key], + queryFn: () => + getFixSuggestionsIssues({ + issueId: issue.key, + }), + enabled: hasFeature(Feature.FixSuggestions) && isLoggedIn(currentUser), + }); +} + +export function withUseGetFixSuggestionsIssues

( + Component: React.ComponentType< + Omit & { aiSuggestionAvailable: boolean } + >, +) { + return function WithGetFixSuggestion(props: Omit) { + const { data } = useGetFixSuggestionsIssuesQuery(props.issue); + return ; + }; +} -- 2.39.5