aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/api/fix-suggestions.ts10
-rw-r--r--server/sonar-web/src/main/js/api/mocks/FixSuggestionsServiceMock.ts28
-rw-r--r--server/sonar-web/src/main/js/app/components/GlobalFooter.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/PromotedSection.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/ai-codefix/AiCodeFixAdminCategory.tsx297
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/ai-codefix/AiCodeFixEnablementForm.tsx (renamed from server/sonar-web/src/main/js/apps/settings/components/AiCodeFixAdmin.tsx)231
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/ai-codefix/__tests__/AiCodeFixAdmin-it.tsx (renamed from server/sonar-web/src/main/js/apps/settings/components/__tests__/AiCodeFixAdmin-it.tsx)251
-rw-r--r--server/sonar-web/src/main/js/components/rules/AiCodeFixTab.tsx (renamed from server/sonar-web/src/main/js/components/rules/IssueSuggestionCodeTab.tsx)2
-rw-r--r--server/sonar-web/src/main/js/design-system/components/icons/LockIllustration.tsx55
-rw-r--r--server/sonar-web/src/main/js/design-system/components/icons/MailIcon.tsx51
-rw-r--r--server/sonar-web/src/main/js/design-system/components/icons/OverviewQGPassedIcon.tsx25
-rw-r--r--server/sonar-web/src/main/js/helpers/doc-links.ts2
-rw-r--r--server/sonar-web/src/main/js/queries/fix-suggestions.tsx17
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties43
16 files changed, 686 insertions, 358 deletions
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 a337e8be7aa..d3c374e3fe5 100644
--- a/server/sonar-web/src/main/js/api/fix-suggestions.ts
+++ b/server/sonar-web/src/main/js/api/fix-suggestions.ts
@@ -37,8 +37,12 @@ export type SuggestionServiceStatus =
| 'CONNECTION_ERROR'
| 'SERVICE_ERROR';
-export interface SuggestionServiceStatusCheckResponse {
+export type SubscriptionType = 'EARLY_ACCESS' | 'PAID' | 'NOT_PAID';
+
+export interface ServiceInfo {
+ isEnabled?: boolean;
status: SuggestionServiceStatus;
+ subscriptionType?: SubscriptionType;
}
export interface UpdateFeatureEnablementParams {
@@ -57,8 +61,8 @@ export function getFixSuggestionsIssues(data: FixParam): Promise<AiIssue> {
return axiosToCatch.get(`/api/v2/fix-suggestions/issues/${data.issueId}`);
}
-export function checkSuggestionServiceStatus(): Promise<SuggestionServiceStatusCheckResponse> {
- return axiosToCatch.post(`/api/v2/fix-suggestions/service-status-checks`);
+export function getFixSuggestionServiceInfo(): Promise<ServiceInfo> {
+ return axiosToCatch.get(`/api/v2/fix-suggestions/service-info`);
}
export function updateFeatureEnablement(
diff --git a/server/sonar-web/src/main/js/api/mocks/FixSuggestionsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/FixSuggestionsServiceMock.ts
index f46b2c36ac3..38d0f8ea62c 100644
--- a/server/sonar-web/src/main/js/api/mocks/FixSuggestionsServiceMock.ts
+++ b/server/sonar-web/src/main/js/api/mocks/FixSuggestionsServiceMock.ts
@@ -20,12 +20,13 @@
import { cloneDeep } from 'lodash';
import {
- checkSuggestionServiceStatus,
FixParam,
+ getFixSuggestionServiceInfo,
getFixSuggestionsIssues,
getSuggestions,
+ ServiceInfo,
+ SubscriptionType,
SuggestionServiceStatus,
- SuggestionServiceStatusCheckResponse,
updateFeatureEnablement,
UpdateFeatureEnablementParams,
} from '../fix-suggestions';
@@ -33,7 +34,11 @@ import { ISSUE_101, ISSUE_1101 } from './data/ids';
jest.mock('../fix-suggestions');
-export type MockSuggestionServiceStatus = SuggestionServiceStatus | 'WTF' | undefined;
+export type MockFixSuggestionServiceInfo = {
+ isEnabled?: boolean;
+ status: SuggestionServiceStatus | 'WTF';
+ subscriptionType?: SubscriptionType | 'WTF';
+};
export default class FixSuggestionsServiceMock {
fixSuggestion = {
@@ -50,12 +55,15 @@ export default class FixSuggestionsServiceMock {
],
};
- serviceStatus: MockSuggestionServiceStatus = 'SUCCESS';
+ serviceInfo: MockFixSuggestionServiceInfo | undefined = {
+ status: 'SUCCESS',
+ subscriptionType: 'PAID',
+ };
constructor() {
jest.mocked(getSuggestions).mockImplementation(this.handleGetFixSuggestion);
jest.mocked(getFixSuggestionsIssues).mockImplementation(this.handleGetFixSuggestionsIssues);
- jest.mocked(checkSuggestionServiceStatus).mockImplementation(this.handleCheckService);
+ jest.mocked(getFixSuggestionServiceInfo).mockImplementation(this.handleGetServiceInfo);
jest.mocked(updateFeatureEnablement).mockImplementation(this.handleUpdateFeatureEnablement);
}
@@ -73,9 +81,9 @@ export default class FixSuggestionsServiceMock {
return this.reply(this.fixSuggestion);
};
- handleCheckService = () => {
- if (this.serviceStatus) {
- return this.reply({ status: this.serviceStatus } as SuggestionServiceStatusCheckResponse);
+ handleGetServiceInfo = () => {
+ if (this.serviceInfo) {
+ return this.reply(this.serviceInfo as ServiceInfo);
}
return Promise.reject({ error: { msg: 'Error' } });
};
@@ -92,7 +100,7 @@ export default class FixSuggestionsServiceMock {
});
}
- setServiceStatus(status: MockSuggestionServiceStatus) {
- this.serviceStatus = status;
+ setServiceInfo(info: MockFixSuggestionServiceInfo | undefined) {
+ this.serviceInfo = info;
}
}
diff --git a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx
index 786421988b2..cd6b1af6d72 100644
--- a/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx
+++ b/server/sonar-web/src/main/js/app/components/GlobalFooter.tsx
@@ -24,7 +24,7 @@ import { useIntl } from 'react-intl';
import { FlagMessage, LAYOUT_VIEWPORT_MIN_WIDTH, themeBorder, themeColor } from '~design-system';
import InstanceMessage from '../../components/common/InstanceMessage';
import AppVersionStatus from '../../components/shared/AppVersionStatus';
-import { DocLink } from '../../helpers/doc-links';
+import { COMMUNITY_FORUM_URL, DocLink } from '../../helpers/doc-links';
import { useDocUrl } from '../../helpers/docs';
import { getEdition } from '../../helpers/editions';
import GlobalFooterBranding from './GlobalFooterBranding';
@@ -82,10 +82,7 @@ export default function GlobalFooter({ hideLoggedInInfo }: Readonly<GlobalFooter
</li>
<li>
- <LinkStandalone
- highlight={LinkHighlight.CurrentColor}
- to="https://community.sonarsource.com/c/help/sq"
- >
+ <LinkStandalone highlight={LinkHighlight.CurrentColor} to={COMMUNITY_FORUM_URL}>
{intl.formatMessage({ id: 'footer.community' })}
</LinkStandalone>
</li>
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx
index 0179cd64c9a..a2c4d81e68b 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx
@@ -31,7 +31,7 @@ import {
themeColor,
} from '~design-system';
import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper';
-import { IssueSuggestionCodeTab } from '../../../components/rules/IssueSuggestionCodeTab';
+import { AiCodeFixTab } from '../../../components/rules/AiCodeFixTab';
import IssueTabViewer from '../../../components/rules/IssueTabViewer';
import { fillBranchLike } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
@@ -179,7 +179,7 @@ export default function IssueDetails({
/>
}
suggestionTabContent={
- <IssueSuggestionCodeTab
+ <AiCodeFixTab
branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
issue={openIssue}
language={openRuleDetails.lang}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/PromotedSection.tsx b/server/sonar-web/src/main/js/apps/overview/branches/PromotedSection.tsx
index 49cbc4e5357..a0a3137ac60 100644
--- a/server/sonar-web/src/main/js/apps/overview/branches/PromotedSection.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/branches/PromotedSection.tsx
@@ -24,11 +24,20 @@ import { themeBorder, themeColor } from '~design-system';
interface Props {
content: React.ReactNode;
+ image?: React.ReactNode;
title: string;
}
-export default function PromotedSection({ content, title }: Readonly<Props>) {
- return (
+export default function PromotedSection({ content, image, title }: Readonly<Props>) {
+ return image ? (
+ <StyledWrapper className="sw-flex sw-items-center sw-p-4 sw-gap-8 sw-pl-6 sw-my-6 sw-rounded-2">
+ {image}
+ <div className="sw-flex-col sw-mb-2">
+ <StyledTitle className="sw-typo-lg-semibold sw-mb-4">{title}</StyledTitle>
+ <div className="sw-typo-default">{content}</div>
+ </div>
+ </StyledWrapper>
+ ) : (
<StyledWrapper className="sw-p-4 sw-pl-6 sw-my-6 sw-rounded-2">
<div className="sw-flex sw-justify-between sw-mb-2">
<StyledTitle className="sw-typo-lg-semibold">{title}</StyledTitle>
diff --git a/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx b/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx
index ffd88b6893b..457141e79de 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/AdditionalCategories.tsx
@@ -33,14 +33,14 @@ import {
NEW_CODE_PERIOD_CATEGORY,
PULL_REQUEST_DECORATION_BINDING_CATEGORY,
} from '../constants';
-import AiCodeFixAdmin from './AiCodeFixAdmin';
+import AiCodeFixAdmin from './ai-codefix/AiCodeFixAdminCategory';
+import AlmIntegration from './almIntegration/AlmIntegration';
import { AnalysisScope } from './AnalysisScope';
+import Authentication from './authentication/Authentication';
+import EmailNotification from './email-notification/EmailNotification';
import Languages from './Languages';
import { Mode } from './Mode';
import NewCodeDefinition from './NewCodeDefinition';
-import AlmIntegration from './almIntegration/AlmIntegration';
-import Authentication from './authentication/Authentication';
-import EmailNotification from './email-notification/EmailNotification';
import PullRequestDecorationBinding from './pullRequestDecorationBinding/PRDecorationBinding';
export interface AdditionalCategoryComponentProps {
diff --git a/server/sonar-web/src/main/js/apps/settings/components/ai-codefix/AiCodeFixAdminCategory.tsx b/server/sonar-web/src/main/js/apps/settings/components/ai-codefix/AiCodeFixAdminCategory.tsx
new file mode 100644
index 00000000000..084d678db9c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/settings/components/ai-codefix/AiCodeFixAdminCategory.tsx
@@ -0,0 +1,297 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import styled from '@emotion/styled';
+import {
+ Button,
+ ButtonGroup,
+ Heading,
+ IconError,
+ LinkStandalone,
+ Spinner,
+ Text,
+} from '@sonarsource/echoes-react';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { OverviewQGPassedIcon, themeColor, UnorderedList } from '~design-system';
+import { throwGlobalError } from '~sonar-aligned/helpers/error';
+import { ServiceInfo } from '../../../../api/fix-suggestions';
+import withAvailableFeatures, {
+ WithAvailableFeaturesProps,
+} from '../../../../app/components/available-features/withAvailableFeatures';
+import DocumentationLink from '../../../../components/common/DocumentationLink';
+import { LockIllustration } from '../../../../design-system/components/icons/LockIllustration';
+import { COMMUNITY_FORUM_URL, DocLink } from '../../../../helpers/doc-links';
+import { translate } from '../../../../helpers/l10n';
+import { useGetServiceInfoQuery } from '../../../../queries/fix-suggestions';
+import { Feature } from '../../../../types/features';
+import PromotedSection from '../../../overview/branches/PromotedSection';
+import AiCodeFixEnablementForm from './AiCodeFixEnablementForm';
+
+interface Props extends WithAvailableFeaturesProps {}
+
+function AiCodeFixAdminCategory({ hasFeature }: Readonly<Props>) {
+ const { data, error, isPending, isError, refetch: refreshServiceInfo } = useGetServiceInfoQuery();
+
+ const retry = () => refreshServiceInfo().catch(throwGlobalError);
+
+ if (!hasFeature(Feature.FixSuggestions)) {
+ return null;
+ }
+
+ if (isPending) {
+ return <SubscriptionCheckPendingMessage />;
+ }
+
+ if (isError) {
+ return (
+ <ErrorView
+ onRetry={retry}
+ message={`${translate('property.aicodefix.admin.serviceInfo.result.requestError')} ${error?.status ?? 'No status'}`}
+ />
+ );
+ }
+
+ if (!data) {
+ return (
+ <ErrorView
+ onRetry={retry}
+ message={translate('property.aicodefix.admin.serviceInfo.empty.response.label')}
+ />
+ );
+ }
+
+ return <ServiceInfoCheckValidResponseView response={data} onRetry={retry} />;
+}
+
+function SubscriptionCheckPendingMessage() {
+ return (
+ <div className="sw-p-8">
+ <Spinner label={translate('property.aicodefix.admin.serviceInfo.spinner.label')} />
+ </div>
+ );
+}
+
+function ServiceInfoCheckValidResponseView({
+ response,
+ onRetry,
+}: Readonly<{ onRetry: Function; response: ServiceInfo }>) {
+ switch (response?.status) {
+ case 'SUCCESS':
+ return <ServiceInfoCheckSuccessResponseView onRetry={onRetry} response={response} />;
+ case 'TIMEOUT':
+ case 'CONNECTION_ERROR':
+ return (
+ <ErrorView
+ message={translate('property.aicodefix.admin.serviceInfo.result.unresponsive.message')}
+ onRetry={onRetry}
+ >
+ <div className="sw-flex-col">
+ <p className="sw-mt-4">
+ <ErrorLabel
+ text={translate(
+ 'property.aicodefix.admin.serviceInfo.result.unresponsive.causes.title',
+ )}
+ />
+ </p>
+ <UnorderedList className="sw-ml-8" ticks>
+ <ErrorListItem className="sw-mb-2">
+ <ErrorLabel
+ text={translate(
+ 'property.aicodefix.admin.serviceInfo.result.unresponsive.causes.1',
+ )}
+ />
+ <p>
+ <DocumentationLink shouldOpenInNewTab to={DocLink.AiCodeFixEnabling}>
+ {translate('property.aicodefix.admin.serviceInfo.learnMore')}
+ </DocumentationLink>
+ </p>
+ </ErrorListItem>
+ <ErrorListItem>
+ <ErrorLabel
+ text={translate(
+ 'property.aicodefix.admin.serviceInfo.result.unresponsive.causes.2',
+ )}
+ />
+ </ErrorListItem>
+ </UnorderedList>
+ </div>
+ </ErrorView>
+ );
+ case 'UNAUTHORIZED':
+ return <ServiceInfoCheckUnauthorizedResponseView onRetry={onRetry} response={response} />;
+ case 'SERVICE_ERROR':
+ return (
+ <ErrorView
+ onRetry={onRetry}
+ message={translate('property.aicodefix.admin.serviceInfo.result.serviceError')}
+ />
+ );
+ default:
+ return (
+ <ErrorView
+ onRetry={onRetry}
+ message={`${translate('property.aicodefix.admin.serviceInfo.result.unknown')} ${response?.status ?? 'no status returned from the service'}`}
+ />
+ );
+ }
+}
+
+function ServiceInfoCheckSuccessResponseView({
+ onRetry,
+ response,
+}: Readonly<{ onRetry: Function; response: ServiceInfo }>) {
+ switch (response.subscriptionType) {
+ case 'EARLY_ACCESS':
+ return <AiCodeFixEnablementForm isEarlyAccess />;
+ case 'PAID':
+ return <AiCodeFixEnablementForm />;
+ default:
+ return (
+ <ErrorView
+ onRetry={onRetry}
+ message={translate('property.aicodefix.admin.serviceInfo.unexpected.response.label')}
+ />
+ );
+ }
+}
+
+function ServiceInfoCheckUnauthorizedResponseView({
+ onRetry,
+ response,
+}: Readonly<{ onRetry: Function; response: ServiceInfo }>) {
+ if (response.subscriptionType === 'NOT_PAID') {
+ return <AiCodeFixPromotionMessage />;
+ }
+
+ if (response.isEnabled != null && !response.isEnabled) {
+ return <FeatureNotAvailableMessage />;
+ }
+
+ return (
+ <ErrorView
+ onRetry={onRetry}
+ message={translate('property.aicodefix.admin.serviceInfo.result.unauthorized')}
+ />
+ );
+}
+
+interface ErrorViewProps {
+ children?: React.ReactNode;
+ message: string;
+ onRetry: Function;
+}
+
+function ErrorView({ children, message, onRetry }: Readonly<ErrorViewProps>) {
+ return (
+ <div className="sw-flex sw-flex-col sw-gap-4 sw-items-start sw-max-w-abs-350 sw-p-6">
+ <Heading as="h2" hasMarginBottom>
+ {translate('property.aicodefix.admin.serviceInfo.result.error.title')}
+ </Heading>
+ <div className="sw-flex">
+ <IconError className="sw-mr-1" color="echoes-color-icon-danger" />
+ <div className="sw-flex-col">
+ <ErrorLabel text={message} />
+ {children}
+ </div>
+ </div>
+ <Button onClick={() => onRetry()}>
+ {translate('property.aicodefix.admin.serviceInfo.result.error.retry.action')}
+ </Button>
+ <p>
+ <FormattedMessage
+ defaultMessage={translate(
+ 'property.aicodefix.admin.serviceInfo.result.error.retry.message',
+ )}
+ id="aicodefix.admin.serviceInfo.result.error.retry.message"
+ values={{
+ link: (
+ <LinkStandalone shouldOpenInNewTab to={COMMUNITY_FORUM_URL}>
+ {translate('property.aicodefix.admin.serviceInfo.result.error.retry.get_help')}
+ </LinkStandalone>
+ ),
+ }}
+ />
+ </p>
+ </div>
+ );
+}
+
+function ErrorLabel({ text }: Readonly<TextProps>) {
+ return <Text colorOverride="echoes-color-text-danger">{text}</Text>;
+}
+
+const ErrorListItem = styled.li`
+ ::marker {
+ color: ${themeColor('errorText')};
+ }
+`;
+
+interface TextProps {
+ /** The text to display inside the component */
+ text: string;
+}
+
+function FeatureNotAvailableMessage() {
+ return (
+ <div className="sw-flex sw-flex-col sw-gap-2 sw-items-center sw-py-64">
+ <LockIllustration />
+ <Text as="b" className="sw-text-center">
+ {translate('property.aicodefix.admin.disabled')}
+ </Text>
+ </div>
+ );
+}
+
+function AiCodeFixPromotionMessage() {
+ return (
+ <div>
+ <Heading as="h2" hasMarginBottom>
+ {translate('property.aicodefix.admin.promotion.title')}
+ </Heading>
+ <PromotedSection
+ content={
+ <MaxWidthDiv>
+ <p className="sw-pb-4">{translate('property.aicodefix.admin.promotion.content')}</p>
+ <ButtonGroup>
+ <LinkStandalone
+ shouldOpenInNewTab
+ to="mailto:contact@sonarsource.com?subject=Sonar%20AI%20CodeFix%20-%20Request%20for%20information"
+ >
+ {translate('property.aicodefix.admin.promotion.contact')}
+ </LinkStandalone>
+ <DocumentationLink shouldOpenInNewTab to={DocLink.AiCodeFixEnabling}>
+ {translate('property.aicodefix.admin.promotion.checkDocumentation')}
+ </DocumentationLink>
+ </ButtonGroup>
+ </MaxWidthDiv>
+ }
+ title={translate('property.aicodefix.admin.promotion.subtitle')}
+ image={<OverviewQGPassedIcon width={84} height={84} />}
+ />
+ </div>
+ );
+}
+
+const MaxWidthDiv = styled.div`
+ max-width: var(--echoes-sizes-typography-max-width-default);
+`;
+
+export default withAvailableFeatures(AiCodeFixAdminCategory);
diff --git a/server/sonar-web/src/main/js/apps/settings/components/AiCodeFixAdmin.tsx b/server/sonar-web/src/main/js/apps/settings/components/ai-codefix/AiCodeFixEnablementForm.tsx
index f5a9526bf24..d66a36e6c8d 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/AiCodeFixAdmin.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/ai-codefix/AiCodeFixEnablementForm.tsx
@@ -18,61 +18,44 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import styled from '@emotion/styled';
import {
Button,
ButtonVariety,
Checkbox,
Heading,
- IconCheckCircle,
- IconError,
IconInfo,
Link,
RadioButtonGroup,
- Spinner,
Text,
} from '@sonarsource/echoes-react';
-import { MutationStatus } from '@tanstack/react-query';
-import { AxiosError } from 'axios';
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
-import {
- BasicSeparator,
- HighlightedSection,
- Note,
- themeColor,
- UnorderedList,
-} from '~design-system';
+import { Note } from '~design-system';
import { throwGlobalError } from '~sonar-aligned/helpers/error';
-import { searchProjects } from '../../../api/components';
-import { SuggestionServiceStatusCheckResponse } from '../../../api/fix-suggestions';
-import withAvailableFeatures, {
- WithAvailableFeaturesProps,
-} from '../../../app/components/available-features/withAvailableFeatures';
-import DocumentationLink from '../../../components/common/DocumentationLink';
+import { searchProjects } from '../../../../api/components';
+import withAvailableFeatures from '../../../../app/components/available-features/withAvailableFeatures';
import SelectList, {
SelectListFilter,
SelectListSearchParams,
-} from '../../../components/controls/SelectList';
-import { DocLink } from '../../../helpers/doc-links';
-import { translate } from '../../../helpers/l10n';
-import { getAiCodeFixTermsOfServiceUrl } from '../../../helpers/urls';
+} from '../../../../components/controls/SelectList';
+import { translate } from '../../../../helpers/l10n';
+import { getAiCodeFixTermsOfServiceUrl } from '../../../../helpers/urls';
import {
- useCheckServiceMutation,
useRemoveCodeSuggestionsCache,
useUpdateFeatureEnablementMutation,
-} from '../../../queries/fix-suggestions';
-import { useGetValueQuery } from '../../../queries/settings';
-import { Feature } from '../../../types/features';
-import { AiCodeFixFeatureEnablement } from '../../../types/fix-suggestions';
-import { SettingsKey } from '../../../types/settings';
-import PromotedSection from '../../overview/branches/PromotedSection';
-
-interface Props extends WithAvailableFeaturesProps {}
+} from '../../../../queries/fix-suggestions';
+import { useGetValueQuery } from '../../../../queries/settings';
+import { AiCodeFixFeatureEnablement } from '../../../../types/fix-suggestions';
+import { SettingsKey } from '../../../../types/settings';
+import PromotedSection from '../../../overview/branches/PromotedSection';
const AI_CODE_FIX_SETTING_KEY = SettingsKey.CodeSuggestion;
-function AiCodeFixAdmin({ hasFeature }: Readonly<Props>) {
+interface AiCodeFixEnablementFormProps {
+ isEarlyAccess?: boolean;
+}
+
+function AiCodeFixEnablementForm({ isEarlyAccess }: Readonly<AiCodeFixEnablementFormProps>) {
const { data: aiCodeFixSetting } = useGetValueQuery({
key: AI_CODE_FIX_SETTING_KEY,
});
@@ -86,14 +69,6 @@ function AiCodeFixAdmin({ hasFeature }: Readonly<Props>) {
);
const [currentAiCodeFixEnablement, setCurrentAiCodeFixEnablement] =
React.useState(savedAiCodeFixEnablement);
- const {
- mutate: checkService,
- isIdle,
- isPending: isServiceCheckPending,
- status,
- error,
- data,
- } = useCheckServiceMutation();
const { mutate: updateFeatureEnablement } = useUpdateFeatureEnablementMutation();
const [changedProjects, setChangedProjects] = React.useState<Map<string, boolean>>(new Map());
@@ -146,10 +121,6 @@ function AiCodeFixAdmin({ hasFeature }: Readonly<Props>) {
}
};
- if (!hasFeature(Feature.FixSuggestions)) {
- return null;
- }
-
const renderProjectElement = (projectKey: string): React.ReactNode => {
const project = currentTabItems.find((project) => project.key === projectKey);
return (
@@ -251,17 +222,19 @@ function AiCodeFixAdmin({ hasFeature }: Readonly<Props>) {
<Heading as="h2" hasMarginBottom>
{translate('property.aicodefix.admin.title')}
</Heading>
- <PromotedSection
- content={
- <>
- <p>{translate('property.aicodefix.admin.promoted_section.content1')}</p>
- <p className="sw-mt-2">
- {translate('property.aicodefix.admin.promoted_section.content2')}
- </p>
- </>
- }
- title={translate('property.aicodefix.admin.promoted_section.title')}
- />
+ {isEarlyAccess && (
+ <PromotedSection
+ content={
+ <>
+ <p>{translate('property.aicodefix.admin.early_access.content1')}</p>
+ <p className="sw-mt-2">
+ {translate('property.aicodefix.admin.early_access.content2')}
+ </p>
+ </>
+ }
+ title={translate('property.aicodefix.admin.early_access.title')}
+ />
+ )}
<p>{translate('property.aicodefix.admin.description')}</p>
<Checkbox
className="sw-my-6"
@@ -357,32 +330,6 @@ function AiCodeFixAdmin({ hasFeature }: Readonly<Props>) {
</div>
</div>
</div>
- <div className="sw-flex-col sw-w-abs-600 sw-p-6">
- <HighlightedSection className="sw-items-start">
- <Heading as="h3" hasMarginBottom>
- {translate('property.aicodefix.admin.serviceCheck.title')}
- </Heading>
- <p>{translate('property.aicodefix.admin.serviceCheck.description1')}</p>
- <DocumentationLink to={DocLink.AiCodeFixEnabling}>
- {translate('property.aicodefix.admin.serviceCheck.learnMore')}
- </DocumentationLink>
- <p>{translate('property.aicodefix.admin.serviceCheck.description2')}</p>
- <Button
- className="sw-mt-4"
- variety={ButtonVariety.Default}
- onClick={() => checkService()}
- isDisabled={isServiceCheckPending}
- >
- {translate('property.aicodefix.admin.serviceCheck.action')}
- </Button>
- {!isIdle && (
- <div>
- <BasicSeparator className="sw-my-4" />
- <ServiceCheckResultView data={data} error={error} status={status} />
- </div>
- )}
- </HighlightedSection>
- </div>
</div>
);
}
@@ -405,122 +352,4 @@ interface ProjectItem {
selected: boolean;
}
-interface ServiceCheckResultViewProps {
- data: SuggestionServiceStatusCheckResponse | undefined;
- error: AxiosError | null;
- status: MutationStatus;
-}
-
-function ServiceCheckResultView({ data, error, status }: Readonly<ServiceCheckResultViewProps>) {
- switch (status) {
- case 'pending':
- return <Spinner label={translate('property.aicodefix.admin.serviceCheck.spinner.label')} />;
- case 'error':
- return (
- <ErrorMessage
- text={`${translate('property.aicodefix.admin.serviceCheck.result.requestError')} ${error?.status ?? 'No status'}`}
- />
- );
- case 'success':
- return ServiceCheckValidResponseView(data);
- }
- // normally unreachable
- throw Error(`Unexpected response from the service status check, received ${status}`);
-}
-
-function ServiceCheckValidResponseView(data: SuggestionServiceStatusCheckResponse | undefined) {
- switch (data?.status) {
- case 'SUCCESS':
- return (
- <SuccessMessage text={translate('property.aicodefix.admin.serviceCheck.result.success')} />
- );
- case 'TIMEOUT':
- case 'CONNECTION_ERROR':
- return (
- <div className="sw-flex">
- <IconError className="sw-mr-1" color="echoes-color-icon-danger" />
- <div className="sw-flex-col">
- <ErrorLabel
- text={translate('property.aicodefix.admin.serviceCheck.result.unresponsive.message')}
- />
- <p className="sw-mt-4">
- <ErrorLabel
- text={translate(
- 'property.aicodefix.admin.serviceCheck.result.unresponsive.causes.title',
- )}
- />
- </p>
- <UnorderedList className="sw-ml-8" ticks>
- <ErrorListItem className="sw-mb-2">
- <ErrorLabel
- text={translate(
- 'property.aicodefix.admin.serviceCheck.result.unresponsive.causes.1',
- )}
- />
- </ErrorListItem>
- <ErrorListItem>
- <ErrorLabel
- text={translate(
- 'property.aicodefix.admin.serviceCheck.result.unresponsive.causes.2',
- )}
- />
- </ErrorListItem>
- </UnorderedList>
- </div>
- </div>
- );
- case 'UNAUTHORIZED':
- return (
- <ErrorMessage
- text={translate('property.aicodefix.admin.serviceCheck.result.unauthorized')}
- />
- );
- case 'SERVICE_ERROR':
- return (
- <ErrorMessage
- text={translate('property.aicodefix.admin.serviceCheck.result.serviceError')}
- />
- );
- default:
- return (
- <ErrorMessage
- text={`${translate('property.aicodefix.admin.serviceCheck.result.unknown')} ${data?.status ?? 'no status returned from the service'}`}
- />
- );
- }
-}
-
-function ErrorMessage({ text }: Readonly<TextProps>) {
- return (
- <div className="sw-flex">
- <IconError className="sw-mr-1" color="echoes-color-icon-danger" />
- <ErrorLabel text={text} />
- </div>
- );
-}
-
-function ErrorLabel({ text }: Readonly<TextProps>) {
- return <Text colorOverride="echoes-color-text-danger">{text}</Text>;
-}
-
-function SuccessMessage({ text }: Readonly<TextProps>) {
- return (
- <div className="sw-flex">
- <IconCheckCircle className="sw-mr-1" color="echoes-color-icon-success" />
- <Text colorOverride="echoes-color-text-success">{text}</Text>
- </div>
- );
-}
-
-const ErrorListItem = styled.li`
- ::marker {
- color: ${themeColor('errorText')};
- }
-`;
-
-interface TextProps {
- /** The text to display inside the component */
- text: string;
-}
-
-export default withAvailableFeatures(AiCodeFixAdmin);
+export default withAvailableFeatures(AiCodeFixEnablementForm);
diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/AiCodeFixAdmin-it.tsx b/server/sonar-web/src/main/js/apps/settings/components/ai-codefix/__tests__/AiCodeFixAdmin-it.tsx
index e5c576631da..d62342c030a 100644
--- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/AiCodeFixAdmin-it.tsx
+++ b/server/sonar-web/src/main/js/apps/settings/components/ai-codefix/__tests__/AiCodeFixAdmin-it.tsx
@@ -22,28 +22,28 @@ import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { uniq } from 'lodash';
import { byRole, byText } from '~sonar-aligned/helpers/testSelector';
-import ComponentsServiceMock from '../../../../api/mocks/ComponentsServiceMock';
-import FixSuggestionsServiceMock from '../../../../api/mocks/FixSuggestionsServiceMock';
+import ComponentsServiceMock from '../../../../../api/mocks/ComponentsServiceMock';
+import FixSuggestionsServiceMock from '../../../../../api/mocks/FixSuggestionsServiceMock';
import SettingsServiceMock, {
DEFAULT_DEFINITIONS_MOCK,
-} from '../../../../api/mocks/SettingsServiceMock';
-import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext';
-import { mockComponent, mockComponentRaw } from '../../../../helpers/mocks/component';
-import { definitions } from '../../../../helpers/mocks/definitions-list';
-import { renderComponent } from '../../../../helpers/testReactTestingUtils';
-import { Feature } from '../../../../types/features';
-import { AdditionalCategoryComponentProps } from '../AdditionalCategories';
-import AiCodeFixAdmin from '../AiCodeFixAdmin';
+} from '../../../../../api/mocks/SettingsServiceMock';
+import { AvailableFeaturesContext } from '../../../../../app/components/available-features/AvailableFeaturesContext';
+import { mockComponent, mockComponentRaw } from '../../../../../helpers/mocks/component';
+import { definitions } from '../../../../../helpers/mocks/definitions-list';
+import { renderComponent } from '../../../../../helpers/testReactTestingUtils';
+import { Feature } from '../../../../../types/features';
+import { AdditionalCategoryComponentProps } from '../../AdditionalCategories';
+import AiCodeFixAdmin from '../AiCodeFixAdminCategory';
let settingServiceMock: SettingsServiceMock;
let componentsServiceMock: ComponentsServiceMock;
-let fixIssueServiceMock: FixSuggestionsServiceMock;
+let fixSuggestionsServiceMock: FixSuggestionsServiceMock;
beforeAll(() => {
settingServiceMock = new SettingsServiceMock();
settingServiceMock.setDefinitions(definitions);
componentsServiceMock = new ComponentsServiceMock();
- fixIssueServiceMock = new FixSuggestionsServiceMock();
+ fixSuggestionsServiceMock = new FixSuggestionsServiceMock();
});
afterEach(() => {
@@ -57,8 +57,8 @@ const ui = {
}),
saveButton: byRole('button', { name: 'save' }),
cancelButton: byRole('button', { name: 'cancel' }),
- checkServiceStatusButton: byRole('button', {
- name: 'property.aicodefix.admin.serviceCheck.action',
+ retryButton: byRole('button', {
+ name: 'property.aicodefix.admin.serviceInfo.result.error.retry.action',
}),
allProjectsEnabledRadio: byRole('radio', {
name: 'property.aicodefix.admin.enable.all.projects.label',
@@ -71,7 +71,137 @@ const ui = {
allTab: byRole('radio', { name: 'all' }),
};
+it('should display the enablement form when having a paid subscription', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'SUCCESS', subscriptionType: 'PAID' });
+ renderCodeFixAdmin();
+
+ expect(await screen.findByText('property.aicodefix.admin.description')).toBeInTheDocument();
+});
+
+it('should display the enablement form and disclaimer when in early access', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({
+ isEnabled: true,
+ status: 'SUCCESS',
+ subscriptionType: 'EARLY_ACCESS',
+ });
+ renderCodeFixAdmin();
+
+ expect(
+ await screen.findByText('property.aicodefix.admin.early_access.content1'),
+ ).toBeInTheDocument();
+});
+
+it('should display a disabled message when in early access and disabled by the customer', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({
+ isEnabled: false,
+ status: 'UNAUTHORIZED',
+ subscriptionType: 'EARLY_ACCESS',
+ });
+ renderCodeFixAdmin();
+
+ expect(await screen.findByText('property.aicodefix.admin.disabled')).toBeInTheDocument();
+});
+
+it('should promote the feature when not paid', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({
+ status: 'UNAUTHORIZED',
+ subscriptionType: 'NOT_PAID',
+ });
+ renderCodeFixAdmin();
+
+ expect(await screen.findByText('property.aicodefix.admin.promotion.content')).toBeInTheDocument();
+});
+
+it('should display an error message when the subscription is unknown', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({
+ status: 'SUCCESS',
+ subscriptionType: 'WTF',
+ });
+ renderCodeFixAdmin();
+
+ expect(
+ await screen.findByText('property.aicodefix.admin.serviceInfo.unexpected.response.label'),
+ ).toBeInTheDocument();
+});
+
+it('should display an error message when the service is not responsive', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'TIMEOUT' });
+ renderCodeFixAdmin();
+
+ expect(
+ await screen.findByText('property.aicodefix.admin.serviceInfo.result.unresponsive.message'),
+ ).toBeInTheDocument();
+});
+
+it('should display an error message when there is a connection error with the service', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'CONNECTION_ERROR' });
+ renderCodeFixAdmin();
+
+ expect(
+ await screen.findByText('property.aicodefix.admin.serviceInfo.result.unresponsive.message'),
+ ).toBeInTheDocument();
+});
+
+it('should propose to retry when an error occurs', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'CONNECTION_ERROR' });
+ const user = userEvent.setup();
+ renderCodeFixAdmin();
+
+ expect(
+ await screen.findByText('property.aicodefix.admin.serviceInfo.result.unresponsive.message'),
+ ).toBeInTheDocument();
+ expect(ui.retryButton.get()).toBeEnabled();
+
+ fixSuggestionsServiceMock.setServiceInfo({
+ isEnabled: true,
+ status: 'SUCCESS',
+ subscriptionType: 'EARLY_ACCESS',
+ });
+ await user.click(ui.retryButton.get());
+
+ expect(
+ await screen.findByText('property.aicodefix.admin.early_access.content1'),
+ ).toBeInTheDocument();
+});
+
+it('should display an error message when the current instance is unauthorized', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'UNAUTHORIZED' });
+ renderCodeFixAdmin();
+
+ expect(
+ await screen.findByText('property.aicodefix.admin.serviceInfo.result.unauthorized'),
+ ).toBeInTheDocument();
+});
+
+it('should display an error message when an error happens at service level', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'SERVICE_ERROR' });
+ renderCodeFixAdmin();
+
+ expect(
+ await screen.findByText('property.aicodefix.admin.serviceInfo.result.serviceError'),
+ ).toBeInTheDocument();
+});
+
+it('should display an error message when the service answers with an unknown status', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'WTF' });
+ renderCodeFixAdmin();
+
+ expect(
+ await screen.findByText('property.aicodefix.admin.serviceInfo.result.unknown WTF'),
+ ).toBeInTheDocument();
+});
+
+it('should display an error message when the backend answers with an error', async () => {
+ fixSuggestionsServiceMock.setServiceInfo(undefined);
+ renderCodeFixAdmin();
+
+ expect(
+ await screen.findByText('property.aicodefix.admin.serviceInfo.result.requestError No status'),
+ ).toBeInTheDocument();
+});
+
it('should by default propose enabling for all projects when enabling the feature', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'SUCCESS', subscriptionType: 'PAID' });
settingServiceMock.set('sonar.ai.suggestions.enabled', 'DISABLED');
const user = userEvent.setup();
renderCodeFixAdmin();
@@ -84,6 +214,7 @@ it('should by default propose enabling for all projects when enabling the featur
});
it('should be able to enable the code fix feature for all projects', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'SUCCESS', subscriptionType: 'PAID' });
settingServiceMock.set('sonar.ai.suggestions.enabled', 'DISABLED');
const user = userEvent.setup();
renderCodeFixAdmin();
@@ -103,13 +234,16 @@ it('should be able to enable the code fix feature for all projects', async () =>
});
it('should be able to enable the code fix feature for some projects', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'SUCCESS', subscriptionType: 'PAID' });
settingServiceMock.set('sonar.ai.suggestions.enabled', 'DISABLED');
const project = mockComponentRaw({ isAiCodeFixEnabled: false });
componentsServiceMock.registerProject(project);
const user = userEvent.setup();
renderCodeFixAdmin();
- expect(ui.enableAiCodeFixCheckbox.get()).not.toBeChecked();
+ await waitFor(() => {
+ expect(ui.enableAiCodeFixCheckbox.get()).not.toBeChecked();
+ });
await user.click(ui.enableAiCodeFixCheckbox.get());
expect(ui.someProjectsEnabledRadio.get()).toBeEnabled();
@@ -133,6 +267,7 @@ it('should be able to enable the code fix feature for some projects', async () =
});
it('should be able to disable the feature for a single project', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'SUCCESS', subscriptionType: 'PAID' });
settingServiceMock.set('sonar.ai.suggestions.enabled', 'ENABLED_FOR_SOME_PROJECTS');
const project = mockComponentRaw({ isAiCodeFixEnabled: true });
componentsServiceMock.registerProject(project);
@@ -158,6 +293,7 @@ it('should be able to disable the feature for a single project', async () => {
});
it('should be able to disable the code fix feature', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'SUCCESS', subscriptionType: 'PAID' });
settingServiceMock.set('sonar.ai.suggestions.enabled', 'ENABLED_FOR_ALL_PROJECTS');
const user = userEvent.setup();
renderCodeFixAdmin();
@@ -173,6 +309,7 @@ it('should be able to disable the code fix feature', async () => {
});
it('should be able to reset the form when canceling', async () => {
+ fixSuggestionsServiceMock.setServiceInfo({ status: 'SUCCESS', subscriptionType: 'PAID' });
settingServiceMock.set('sonar.ai.suggestions.enabled', 'ENABLED_FOR_ALL_PROJECTS');
componentsServiceMock.registerComponent(mockComponent());
const user = userEvent.setup();
@@ -189,90 +326,6 @@ it('should be able to reset the form when canceling', async () => {
expect(ui.enableAiCodeFixCheckbox.get()).toBeChecked();
});
-it('should display a success message when the service status can be successfully checked', async () => {
- fixIssueServiceMock.setServiceStatus('SUCCESS');
- const user = userEvent.setup();
- renderCodeFixAdmin();
-
- await user.click(ui.checkServiceStatusButton.get());
-
- expect(
- await screen.findByText('property.aicodefix.admin.serviceCheck.result.success'),
- ).toBeInTheDocument();
-});
-
-it('should display an error message when the service is not responsive', async () => {
- fixIssueServiceMock.setServiceStatus('TIMEOUT');
- const user = userEvent.setup();
- renderCodeFixAdmin();
-
- await user.click(ui.checkServiceStatusButton.get());
-
- expect(
- await screen.findByText('property.aicodefix.admin.serviceCheck.result.unresponsive.message'),
- ).toBeInTheDocument();
-});
-
-it('should display an error message when there is a connection error with the service', async () => {
- fixIssueServiceMock.setServiceStatus('CONNECTION_ERROR');
- const user = userEvent.setup();
- renderCodeFixAdmin();
-
- await user.click(ui.checkServiceStatusButton.get());
-
- expect(
- await screen.findByText('property.aicodefix.admin.serviceCheck.result.unresponsive.message'),
- ).toBeInTheDocument();
-});
-
-it('should display an error message when the current instance is unauthorized', async () => {
- fixIssueServiceMock.setServiceStatus('UNAUTHORIZED');
- const user = userEvent.setup();
- renderCodeFixAdmin();
-
- await user.click(ui.checkServiceStatusButton.get());
-
- expect(
- await screen.findByText('property.aicodefix.admin.serviceCheck.result.unauthorized'),
- ).toBeInTheDocument();
-});
-
-it('should display an error message when an error happens at service level', async () => {
- fixIssueServiceMock.setServiceStatus('SERVICE_ERROR');
- const user = userEvent.setup();
- renderCodeFixAdmin();
-
- await user.click(ui.checkServiceStatusButton.get());
-
- expect(
- await screen.findByText('property.aicodefix.admin.serviceCheck.result.serviceError'),
- ).toBeInTheDocument();
-});
-
-it('should display an error message when the service answers with an unknown status', async () => {
- fixIssueServiceMock.setServiceStatus('WTF');
- const user = userEvent.setup();
- renderCodeFixAdmin();
-
- await user.click(ui.checkServiceStatusButton.get());
-
- expect(
- await screen.findByText('property.aicodefix.admin.serviceCheck.result.unknown WTF'),
- ).toBeInTheDocument();
-});
-
-it('should display an error message when the backend answers with an error', async () => {
- fixIssueServiceMock.setServiceStatus(undefined);
- const user = userEvent.setup();
- renderCodeFixAdmin();
-
- await user.click(ui.checkServiceStatusButton.get());
-
- expect(
- await screen.findByText('property.aicodefix.admin.serviceCheck.result.requestError No status'),
- ).toBeInTheDocument();
-});
-
function renderCodeFixAdmin(
overrides: Partial<AdditionalCategoryComponentProps> = {},
features?: Feature[],
diff --git a/server/sonar-web/src/main/js/components/rules/IssueSuggestionCodeTab.tsx b/server/sonar-web/src/main/js/components/rules/AiCodeFixTab.tsx
index 55525b6b8e7..40085e539ca 100644
--- a/server/sonar-web/src/main/js/components/rules/IssueSuggestionCodeTab.tsx
+++ b/server/sonar-web/src/main/js/components/rules/AiCodeFixTab.tsx
@@ -34,7 +34,7 @@ interface Props {
language?: string;
}
-export function IssueSuggestionCodeTab({ branchLike, issue, language }: Readonly<Props>) {
+export function AiCodeFixTab({ branchLike, issue, language }: Readonly<Props>) {
const prefetchSuggestion = usePrefetchSuggestion(issue.key);
const { isPending, isLoading, isError, refetch } = useUnifiedSuggestionsQuery(issue, false);
const { isError: isIssueRawError } = useRawSourceQuery({
diff --git a/server/sonar-web/src/main/js/design-system/components/icons/LockIllustration.tsx b/server/sonar-web/src/main/js/design-system/components/icons/LockIllustration.tsx
new file mode 100644
index 00000000000..e1e8fc57153
--- /dev/null
+++ b/server/sonar-web/src/main/js/design-system/components/icons/LockIllustration.tsx
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+export function LockIllustration() {
+ return (
+ <svg
+ width="168"
+ height="168"
+ viewBox="0 0 168 168"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <g clipPath="url(#clip0_21804_11)">
+ <path d="M137 154.5V93C131 141.4 87.8333 154.333 67 154.5H137Z" fill="#F4F6FF" />
+ <path d="M31 63L31 158H39L39 63H31Z" fill="#BDC6FF" />
+ <path
+ d="M38.4996 160.125C34.8538 160.125 31.7548 158.849 29.2028 156.297C26.6507 153.745 25.3746 150.646 25.3746 147V73.4999C25.3746 69.8541 26.6507 66.7551 29.2028 64.203C31.7548 61.6509 34.8538 60.3749 38.4996 60.3749H51.6246V42.8749C51.6246 33.8332 54.7601 26.177 61.0309 19.9061C67.3017 13.6353 74.958 10.4999 83.9996 10.4999C93.0413 10.4999 100.698 13.6353 106.968 19.9061C113.239 26.177 116.375 33.8332 116.375 42.8749V60.3749H129.5C133.145 60.3749 136.244 61.6509 138.797 64.203C141.349 66.7551 142.625 69.8541 142.625 73.4999V147C142.625 150.646 141.349 153.745 138.797 156.297C136.244 158.849 133.145 160.125 129.5 160.125H38.4996ZM38.4996 154H129.5C131.541 154 133.218 153.344 134.531 152.031C135.843 150.719 136.5 149.042 136.5 147V73.4999C136.5 71.4582 135.843 69.7811 134.531 68.4686C133.218 67.1561 131.541 66.4999 129.5 66.4999H38.4996C36.458 66.4999 34.7809 67.1561 33.4684 68.4686C32.1559 69.7811 31.4996 71.4582 31.4996 73.4999V147C31.4996 149.042 32.1559 150.719 33.4684 152.031C34.7809 153.344 36.458 154 38.4996 154ZM57.7496 60.3749H110.25V42.8749C110.25 35.5832 107.698 29.3853 102.593 24.2811C97.4892 19.177 91.2913 16.6249 83.9996 16.6249C76.708 16.6249 70.5101 19.177 65.4059 24.2811C60.3017 29.3853 57.7496 35.5832 57.7496 42.8749V60.3749Z"
+ fill="#6A7590"
+ />
+ <path
+ d="M92.4215 114.234C90.1611 116.495 87.3538 117.625 83.9996 117.625C80.6455 117.625 77.8382 116.495 75.5778 114.234C73.3173 111.974 72.1871 109.167 72.1871 105.813C72.1871 102.458 73.3173 99.651 75.5778 97.3906C77.8382 95.1302 80.6455 94 83.9996 94C87.3538 94 90.1611 95.1302 92.4215 97.3906C94.6819 99.651 95.8121 102.458 95.8121 105.813C95.8121 109.167 94.6819 111.974 92.4215 114.234Z"
+ fill="#7B87D9"
+ />
+ <path d="M78 111.563H90V126.563H78V111.563Z" fill="#7B87D9" />
+ <path
+ d="M67 63.5V42.8C67 38.0963 68.6275 34.1903 71.944 30.907C75.2616 27.6225 79.2214 26 84 26C88.7786 26 92.7384 27.6225 96.056 30.907C99.3725 34.1903 101 38.0963 101 42.8V63.5H67Z"
+ stroke="#6A7590"
+ strokeWidth="6"
+ />
+ </g>
+ <defs>
+ <clipPath id="clip0_21804_11">
+ <rect width="168" height="168" fill="white" />
+ </clipPath>
+ </defs>
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/design-system/components/icons/MailIcon.tsx b/server/sonar-web/src/main/js/design-system/components/icons/MailIcon.tsx
new file mode 100644
index 00000000000..0998a3c1948
--- /dev/null
+++ b/server/sonar-web/src/main/js/design-system/components/icons/MailIcon.tsx
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import { useTheme } from '@emotion/react';
+import { themeColor } from '~design-system';
+
+export function MailIcon() {
+ const theme = useTheme();
+
+ return (
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <g id="mail">
+ <mask
+ id="mask0_6548_2965"
+ style={{ maskType: 'alpha' }}
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="16"
+ height="16"
+ >
+ <rect id="Bounding box" width="16" height="16" fill="#D9D9D9" />
+ </mask>
+ <g mask="url(#mask0_6548_2965)">
+ <path
+ id="mail_2"
+ d="M2.66659 13.3332C2.29992 13.3332 1.98603 13.2026 1.72492 12.9415C1.46381 12.6804 1.33325 12.3665 1.33325 11.9998V3.99984C1.33325 3.63317 1.46381 3.31928 1.72492 3.05817C1.98603 2.79706 2.29992 2.6665 2.66659 2.6665H13.3333C13.6999 2.6665 14.0138 2.79706 14.2749 3.05817C14.536 3.31928 14.6666 3.63317 14.6666 3.99984V11.9998C14.6666 12.3665 14.536 12.6804 14.2749 12.9415C14.0138 13.2026 13.6999 13.3332 13.3333 13.3332H2.66659ZM7.99992 8.6665L2.66659 5.33317V11.9998H13.3333V5.33317L7.99992 8.6665ZM7.99992 7.33317L13.3333 3.99984H2.66659L7.99992 7.33317ZM2.66659 5.33317V3.99984V11.9998V5.33317Z"
+ fill={themeColor('currentColor')({ theme })}
+ />
+ </g>
+ </g>
+ </svg>
+ );
+}
diff --git a/server/sonar-web/src/main/js/design-system/components/icons/OverviewQGPassedIcon.tsx b/server/sonar-web/src/main/js/design-system/components/icons/OverviewQGPassedIcon.tsx
index 86c6324373e..a0d861b45c3 100644
--- a/server/sonar-web/src/main/js/design-system/components/icons/OverviewQGPassedIcon.tsx
+++ b/server/sonar-web/src/main/js/design-system/components/icons/OverviewQGPassedIcon.tsx
@@ -19,19 +19,34 @@
*/
import { useTheme } from '@emotion/react';
-import { themeColor } from '../../helpers/theme';
+import { themeColor } from '../../helpers';
-export function OverviewQGPassedIcon({ className }: { className?: string }) {
+interface OverviewQGPassedIconProps {
+ className?: string;
+ height?: number;
+ width?: number;
+}
+
+const DEFAULT_WIDTH = 154;
+const DEFAULT_HEIGHT = 136;
+
+export function OverviewQGPassedIcon({
+ className,
+ height,
+ width,
+}: Readonly<OverviewQGPassedIconProps>) {
const theme = useTheme();
+ const actualWidth = width ?? DEFAULT_WIDTH;
+ const actualHeight = height ?? DEFAULT_HEIGHT;
return (
<svg
className={className}
fill="none"
- height="136"
+ height={actualHeight}
role="img"
- viewBox="0 0 154 136"
- width="154"
+ viewBox={`0 0 ${DEFAULT_WIDTH} ${DEFAULT_HEIGHT}`}
+ width={actualWidth}
xmlns="http://www.w3.org/2000/svg"
>
<path
diff --git a/server/sonar-web/src/main/js/helpers/doc-links.ts b/server/sonar-web/src/main/js/helpers/doc-links.ts
index d8047d15e73..a846d31cf84 100644
--- a/server/sonar-web/src/main/js/helpers/doc-links.ts
+++ b/server/sonar-web/src/main/js/helpers/doc-links.ts
@@ -20,6 +20,8 @@
import { AlmKeys } from '../types/alm-settings';
+export const COMMUNITY_FORUM_URL = 'https://community.sonarsource.com/c/help/sq';
+
export const DOC_URL = 'https://docs.sonarsource.com/sonarqube/latest';
export enum DocLink {
diff --git a/server/sonar-web/src/main/js/queries/fix-suggestions.tsx b/server/sonar-web/src/main/js/queries/fix-suggestions.tsx
index 8edcb03f044..4a125384e82 100644
--- a/server/sonar-web/src/main/js/queries/fix-suggestions.tsx
+++ b/server/sonar-web/src/main/js/queries/fix-suggestions.tsx
@@ -23,10 +23,10 @@ import { AxiosError } from 'axios';
import { some } from 'lodash';
import React, { useContext } from 'react';
import {
- checkSuggestionServiceStatus,
+ getFixSuggestionServiceInfo,
getFixSuggestionsIssues,
getSuggestions,
- SuggestionServiceStatusCheckResponse,
+ ServiceInfo,
updateFeatureEnablement,
} from '../api/fix-suggestions';
import { useAvailableFeatures } from '../app/components/available-features/withAvailableFeatures';
@@ -184,14 +184,15 @@ export function withUseGetFixSuggestionsIssues<P extends { issue: Issue }>(
};
}
-export function useUpdateFeatureEnablementMutation() {
- return useMutation({
- mutationFn: updateFeatureEnablement,
+export function useGetServiceInfoQuery() {
+ return useQuery<ServiceInfo, AxiosError>({
+ queryKey: ['fix-suggestions', 'service-info'],
+ queryFn: getFixSuggestionServiceInfo,
});
}
-export function useCheckServiceMutation() {
- return useMutation<SuggestionServiceStatusCheckResponse, AxiosError>({
- mutationFn: checkSuggestionServiceStatus,
+export function useUpdateFeatureEnablementMutation() {
+ return useMutation({
+ mutationFn: updateFeatureEnablement,
});
}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index c4f1ee47089..76efd8bddd1 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -1948,6 +1948,7 @@ property.category.housekeeping.auditLogs=Audit Logs
property.aicodefix.admin.title=Enable AI-generated fix suggestions
property.aicodefix.admin.description=Activate this option to enable users of all or part of the projects to generate an AI-suggested code fix for an issue using the Sonar AI CodeFix service.
property.aicodefix.admin.checkbox.label=Enable AI CodeFix
+property.aicodefix.admin.disabled=According to your company policy, access to AI CodeFix is not available for this SonarQube instance
property.aicodefix.admin.acceptTerm.label=By activating this option, you agree to the {terms}
property.aicodefix.admin.acceptTerm.terms=AI CodeFix Terms
property.aicodefix.admin.enable.title=Choose which projects should have AI CodeFix enabled
@@ -1956,24 +1957,30 @@ property.aicodefix.admin.enable.all.projects.help=Enable AI CodeFix on all exist
property.aicodefix.admin.enable.some.projects.label=Only selected projects
property.aicodefix.admin.enable.some.projects.help=Enable AI CodeFix on selected projects only
property.aicodefix.admin.enable.some.projects.note=AI CodeFix will not be automatically enabled on new projects.
-property.aicodefix.admin.promoted_section.title=Free - early access feature
-property.aicodefix.admin.promoted_section.content1=This no cost trial is offered to you at Sonar’s discretion. Sonar can decide to stop this trial anytime.
-property.aicodefix.admin.promoted_section.content2=At the end of the trial, this feature will be deactivated and your choice to “enable AI CodeFix” below will be ignored. Your organisation will not be charged.
-property.aicodefix.admin.serviceCheck.title=Test the AI CodeFix service
-property.aicodefix.admin.serviceCheck.description1=Make sure this SonarQube instance can communicate with the AI CodeFix service, which requires network connectivity to function.
-property.aicodefix.admin.serviceCheck.description2=This test is free and should only take a few seconds.
-property.aicodefix.admin.serviceCheck.learnMore=Read more about enabling AI CodeFix
-property.aicodefix.admin.serviceCheck.action=Test AI CodeFix service
-property.aicodefix.admin.serviceCheck.spinner.label=Waiting for AI CodeFix service to respond...
-property.aicodefix.admin.serviceCheck.result.success=The AI CodeFix service responded successfully.
-property.aicodefix.admin.serviceCheck.result.unresponsive.message=The AI CodeFix service does not respond or is not reachable.
-property.aicodefix.admin.serviceCheck.result.unresponsive.causes.title=Here are some possible causes of this error:
-property.aicodefix.admin.serviceCheck.result.unresponsive.causes.1=The network may not be properly configured on this SonarQube instance. Please check the firewall and connectivity settings.
-property.aicodefix.admin.serviceCheck.result.unresponsive.causes.2=The AI CodeFix service may be down.
-property.aicodefix.admin.serviceCheck.result.requestError=Error checking the AI CodeFix service:
-property.aicodefix.admin.serviceCheck.result.serviceError=The AI CodeFix service is reachable but returned an error. Check logs for more details.
-property.aicodefix.admin.serviceCheck.result.unauthorized=This SonarQube instance is not allowed to use AI CodeFix.
-property.aicodefix.admin.serviceCheck.result.unknown=The AI CodeFix service returned an unexpected message:
+property.aicodefix.admin.early_access.title=Free - early access feature
+property.aicodefix.admin.early_access.content1=This no cost trial is offered to you at Sonar’s discretion. Sonar can decide to stop this trial anytime.
+property.aicodefix.admin.early_access.content2=At the end of the trial, this feature will be deactivated and your choice to “enable AI CodeFix” below will be ignored. Your organisation will not be charged.
+property.aicodefix.admin.serviceInfo.learnMore=Read more about enabling AI CodeFix
+property.aicodefix.admin.serviceInfo.spinner.label=Waiting for AI CodeFix service to respond...
+property.aicodefix.admin.serviceInfo.unexpected.response.label=The AI CodeFix service returned an unexpected response. You might need to upgrade your instance.
+property.aicodefix.admin.serviceInfo.empty.response.label=The AI CodeFix service returned an empty response. Please report the issue to the maintainers.
+property.aicodefix.admin.serviceInfo.result.unresponsive.message=The AI CodeFix service does not respond or is not reachable.
+property.aicodefix.admin.serviceInfo.result.unresponsive.causes.title=Here are some possible causes of this error:
+property.aicodefix.admin.serviceInfo.result.unresponsive.causes.1=The network may not be properly configured on this SonarQube instance. Please check the firewall and connectivity settings.
+property.aicodefix.admin.serviceInfo.result.unresponsive.causes.2=The AI CodeFix service may be down.
+property.aicodefix.admin.serviceInfo.result.requestError=Error checking the AI CodeFix service:
+property.aicodefix.admin.serviceInfo.result.serviceError=The AI CodeFix service is reachable but returned an error. Check logs for more details.
+property.aicodefix.admin.serviceInfo.result.unauthorized=This SonarQube instance is not allowed to use AI CodeFix.
+property.aicodefix.admin.serviceInfo.result.unknown=The AI CodeFix service returned an unexpected message:
+property.aicodefix.admin.serviceInfo.result.error.title=AI CodeFix Error
+property.aicodefix.admin.serviceInfo.result.error.retry.message=If the issue persists, try again later or { link }
+property.aicodefix.admin.serviceInfo.result.error.retry.get_help=get help
+property.aicodefix.admin.serviceInfo.result.error.retry.action=Retry
+property.aicodefix.admin.promotion.title=This is a paid feature
+property.aicodefix.admin.promotion.subtitle=Unlock AI-generated fix suggestions
+property.aicodefix.admin.promotion.content=Enable users of all or part of the projects to generate an AI-suggested code fix for an issue using the Sonar AI CodeFix service. To get access to AI CodeFix:
+property.aicodefix.admin.promotion.contact=Contact us
+property.aicodefix.admin.promotion.checkDocumentation=Check documentation
#------------------------------------------------------------------------------
#