deleteProjectAlmBinding,
getAlmDefinitions,
getAlmSettings,
+ getAlmSettingsNoCatch,
getProjectAlmBinding,
setProjectAzureBinding,
setProjectBitbucketBinding,
summaryCommentEnabled?: boolean;
}
+jest.mock('../alm-settings');
+
export default class AlmSettingsServiceMock {
#almDefinitions: AlmSettingsBindingDefinitions;
#almSettings: AlmSettingsInstance[];
this.#almSettings = cloneDeep(defaultAlmSettings);
this.#almDefinitions = cloneDeep(defaultAlmDefinitions);
jest.mocked(getAlmSettings).mockImplementation(this.handleGetAlmSettings);
+ jest.mocked(getAlmSettingsNoCatch).mockImplementation(this.handleGetAlmSettings);
jest.mocked(getAlmDefinitions).mockImplementation(this.handleGetAlmDefinitions);
jest.mocked(countBoundProjects).mockImplementation(this.handleCountBoundProjects);
jest.mocked(validateAlmSettings).mockImplementation(this.handleValidateAlmSettings);
import * as React from 'react';
import { Helmet } from 'react-helmet-async';
import { Outlet } from 'react-router-dom';
-import { getProjectAlmBinding, validateProjectAlmBinding } from '../../api/alm-settings';
+import { validateProjectAlmBinding } from '../../api/alm-settings';
import { getTasksForComponent } from '../../api/ce';
import { getComponentData } from '../../api/components';
import { getComponentNavigation } from '../../api/navigation';
import { translateWithParameters } from '../../helpers/l10n';
import { HttpStatus } from '../../helpers/request';
import { getPortfolioUrl, getProjectUrl } from '../../helpers/urls';
-import {
- ProjectAlmBindingConfigurationErrors,
- ProjectAlmBindingResponse,
-} from '../../types/alm-settings';
+import { ProjectAlmBindingConfigurationErrors } from '../../types/alm-settings';
import { ComponentQualifier, isPortfolioLike } from '../../types/component';
import { Feature } from '../../types/features';
import { Task, TaskStatuses, TaskTypes } from '../../types/tasks';
currentTask?: Task;
isPending: boolean;
loading: boolean;
- projectBinding?: ProjectAlmBindingResponse;
projectBindingErrors?: ProjectAlmBindingConfigurationErrors;
tasksInProgress?: Task[];
}
this.props.router.replace(getPortfolioUrl(componentWithQualifier.key));
}
- let projectBinding;
-
- if (componentWithQualifier.qualifier === ComponentQualifier.Project) {
- projectBinding = await getProjectAlmBinding(key).catch(() => undefined);
- }
-
if (this.mounted) {
this.setState(
{
component: componentWithQualifier,
- projectBinding,
loading: false,
},
() => {
return <ComponentContainerNotFound />;
}
- const { currentTask, isPending, projectBinding, projectBindingErrors, tasksInProgress } =
- this.state;
+ const { currentTask, isPending, projectBindingErrors, tasksInProgress } = this.state;
const isInProgress = tasksInProgress && tasksInProgress.length > 0;
currentTask={currentTask}
isInProgress={isInProgress}
isPending={isPending}
- projectBinding={projectBinding}
projectBindingErrors={projectBindingErrors}
/>
)}
isPending,
onComponentChange: this.handleComponentChange,
fetchComponent: this.fetchComponent,
- projectBinding,
}}
>
<Outlet />
import { HttpStatus } from '../../../helpers/request';
import { mockLocation, mockRouter } from '../../../helpers/testMocks';
import { waitAndUpdate } from '../../../helpers/testUtils';
-import { AlmKeys } from '../../../types/alm-settings';
import { ComponentQualifier, Visibility } from '../../../types/component';
import { TaskStatuses, TaskTypes } from '../../../types/tasks';
import { Component } from '../../../types/types';
});
});
-it('loads the project binding, if any', async () => {
- const component = mockComponent({
- breadcrumbs: [{ key: 'foo', name: 'foo', qualifier: ComponentQualifier.Project }],
- });
-
- jest
- .mocked(getComponentNavigation)
- .mockResolvedValueOnce({} as unknown as Awaited<ReturnType<typeof getComponentNavigation>>);
-
- jest
- .mocked(getComponentData)
- .mockResolvedValueOnce({ component } as unknown as Awaited<ReturnType<typeof getComponentData>>)
- .mockResolvedValueOnce({ component } as unknown as Awaited<
- ReturnType<typeof getComponentData>
- >);
-
- jest
- .mocked(getProjectAlmBinding)
- .mockResolvedValueOnce(undefined as unknown as Awaited<ReturnType<typeof getProjectAlmBinding>>)
- .mockResolvedValueOnce({
- alm: AlmKeys.GitHub,
- key: 'foo',
- } as unknown as Awaited<ReturnType<typeof getProjectAlmBinding>>);
-
- const wrapper = shallowRender();
- await waitAndUpdate(wrapper);
- expect(getProjectAlmBinding).toHaveBeenCalled();
- expect(wrapper.state().projectBinding).toBeUndefined();
-
- wrapper.setProps({ location: mockLocation({ query: { id: 'bar' } }) });
- await waitAndUpdate(wrapper);
- expect(wrapper.state().projectBinding).toEqual({ alm: AlmKeys.GitHub, key: 'foo' });
-});
-
it("doesn't load branches portfolio", async () => {
const wrapper = shallowRender({ location: mockLocation({ query: { id: 'portfolioKey' } }) });
await waitAndUpdate(wrapper);
import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper';
import NCDAutoUpdateMessage from '../../../../components/new-code-definition/NCDAutoUpdateMessage';
import { translate } from '../../../../helpers/l10n';
-import {
- ProjectAlmBindingConfigurationErrors,
- ProjectAlmBindingResponse,
-} from '../../../../types/alm-settings';
+import { ProjectAlmBindingConfigurationErrors } from '../../../../types/alm-settings';
import { ComponentQualifier } from '../../../../types/component';
import { Task } from '../../../../types/tasks';
import { Component } from '../../../../types/types';
currentTask?: Task;
isInProgress?: boolean;
isPending?: boolean;
- projectBinding?: ProjectAlmBindingResponse;
projectBindingErrors?: ProjectAlmBindingConfigurationErrors;
}
export default function ComponentNav(props: ComponentNavProps) {
- const { component, currentTask, isInProgress, isPending, projectBinding, projectBindingErrors } =
- props;
+ const { component, currentTask, isInProgress, isPending, projectBindingErrors } = props;
React.useEffect(() => {
const { breadcrumbs, key, name } = component;
style={{ top: `${top}px` }}
>
<div className="sw-min-h-10 sw-flex sw-justify-between">
- <Header component={component} projectBinding={projectBinding} />
+ <Header component={component} />
<HeaderMeta
component={component}
currentTask={currentTask}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { ProjectAlmBindingResponse } from '../../../../types/alm-settings';
import { Component } from '../../../../types/types';
import { CurrentUser } from '../../../../types/users';
import withCurrentUserContext from '../../current-user/withCurrentUserContext';
export interface HeaderProps {
component: Component;
currentUser: CurrentUser;
- projectBinding?: ProjectAlmBindingResponse;
}
export function Header(props: HeaderProps) {
- const { component, currentUser, projectBinding } = props;
+ const { component, currentUser } = props;
return (
<div className="sw-flex sw-flex-shrink sw-items-center">
<Breadcrumb component={component} currentUser={currentUser} />
- <BranchLikeNavigation component={component} projectBinding={projectBinding} />
+ <BranchLikeNavigation component={component} />
</div>
);
}
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
+import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock';
import BranchesServiceMock from '../../../../../api/mocks/BranchesServiceMock';
-import { mockProjectAlmBindingResponse } from '../../../../../helpers/mocks/alm-settings';
import { mockMainBranch, mockPullRequest } from '../../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../../helpers/mocks/component';
import { mockCurrentUser, mockLoggedInUser } from '../../../../../helpers/testMocks';
}));
const handler = new BranchesServiceMock();
+const almHandler = new AlmSettingsServiceMock();
-beforeEach(() => handler.reset());
+beforeEach(() => {
+ handler.reset();
+ almHandler.reset();
+});
it('should render correctly when there is only 1 branch', async () => {
handler.emptyBranchesAndPullRequest();
it('should show the correct help tooltip when branch support is not enabled', async () => {
handler.emptyBranchesAndPullRequest();
handler.addBranch(mockMainBranch());
+ almHandler.handleSetProjectBinding(AlmKeys.GitLab, {
+ almSetting: 'key',
+ project: 'header-project',
+ repository: 'header-project',
+ monorepo: true,
+ });
renderHeader(
{
currentUser: mockLoggedInUser(),
- projectBinding: mockProjectAlmBindingResponse({
- alm: AlmKeys.GitLab,
- key: 'key',
- monorepo: true,
- }),
},
[]
);
import HelpTooltip from '../../../../../components/controls/HelpTooltip';
import { translate, translateWithParameters } from '../../../../../helpers/l10n';
import { getApplicationAdminUrl } from '../../../../../helpers/urls';
-import { ProjectAlmBindingResponse } from '../../../../../types/alm-settings';
+import { useProjectBindingQuery } from '../../../../../queries/devops-integration';
+import { AlmKeys } from '../../../../../types/alm-settings';
import { Component } from '../../../../../types/types';
interface Props {
component: Component;
isApplication: boolean;
- projectBinding?: ProjectAlmBindingResponse;
hasManyBranches: boolean;
canAdminComponent?: boolean;
branchSupportEnabled: boolean;
- isGitLab: boolean;
}
export default function BranchHelpTooltip({
component,
isApplication,
- projectBinding,
hasManyBranches,
canAdminComponent,
branchSupportEnabled,
- isGitLab,
}: Props) {
const helpIcon = <HelperHintIcon aria-label="help-tooltip" />;
+ const { data: projectBinding } = useProjectBindingQuery(component.key);
+ const isGitLab = projectBinding != null && projectBinding.alm === AlmKeys.GitLab;
if (isApplication) {
if (!hasManyBranches && canAdminComponent) {
return (
<DocumentationTooltip
content={
- projectBinding !== undefined
+ projectBinding != null
? translateWithParameters(
`branch_like_navigation.no_branch_support.content_x.${isGitLab ? 'mr' : 'pr'}`,
translate('alm', projectBinding.alm)
},
]}
title={
- projectBinding !== undefined
+ projectBinding != null
? translate('branch_like_navigation.no_branch_support.title', isGitLab ? 'mr' : 'pr')
: translate('branch_like_navigation.no_branch_support.title')
}
import FocusOutHandler from '../../../../../components/controls/FocusOutHandler';
import OutsideClickHandler from '../../../../../components/controls/OutsideClickHandler';
import { useBranchesQuery } from '../../../../../queries/branch';
-import { AlmKeys, ProjectAlmBindingResponse } from '../../../../../types/alm-settings';
import { ComponentQualifier } from '../../../../../types/component';
import { Feature } from '../../../../../types/features';
import { Component } from '../../../../../types/types';
export interface BranchLikeNavigationProps extends WithAvailableFeaturesProps {
component: Component;
- projectBinding?: ProjectAlmBindingResponse;
}
export function BranchLikeNavigation(props: BranchLikeNavigationProps) {
const {
component,
component: { configuration },
- projectBinding,
} = props;
const { data: { branchLikes, branchLike: currentBranchLike } = { branchLikes: [] } } =
}
const isApplication = component.qualifier === ComponentQualifier.Application;
- const isGitLab = projectBinding !== undefined && projectBinding.alm === AlmKeys.GitLab;
const branchSupportEnabled = props.hasFeature(Feature.BranchSupport);
const canAdminComponent = configuration?.showSettings;
<BranchHelpTooltip
component={component}
isApplication={isApplication}
- projectBinding={projectBinding}
hasManyBranches={hasManyBranches}
canAdminComponent={canAdminComponent}
branchSupportEnabled={branchSupportEnabled}
- isGitLab={isGitLab}
/>
</div>
extractStatusConditionsFromProjectStatus,
} from '../../../helpers/qualityGates';
import { isDefined } from '../../../helpers/types';
-import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
import { ApplicationPeriod } from '../../../types/application';
import { Branch, BranchLike } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
branch?: Branch;
branchesEnabled?: boolean;
component: Component;
- projectBinding?: ProjectAlmBindingResponse;
}
interface State {
};
render() {
- const { branch, branchesEnabled, component, projectBinding } = this.props;
+ const { branch, branchesEnabled, component } = this.props;
const {
analyses,
appLeak,
metrics={metrics}
onGraphChange={this.handleGraphChange}
period={period}
- projectBinding={projectBinding}
projectIsEmpty={projectIsEmpty}
qgStatuses={qgStatuses}
/>
import * as React from 'react';
import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget';
import { parseDate } from '../../../helpers/dates';
-import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
import { ApplicationPeriod } from '../../../types/application';
import { Branch } from '../../../types/branch-like';
import { ComponentQualifier } from '../../../types/component';
metrics?: Metric[];
onGraphChange: (graph: GraphType) => void;
period?: Period;
- projectBinding?: ProjectAlmBindingResponse;
projectIsEmpty?: boolean;
qgStatuses?: QualityGateStatus[];
}
metrics = [],
onGraphChange,
period,
- projectBinding,
projectIsEmpty,
qgStatuses,
} = props;
component={component}
branchesEnabled={branchesEnabled}
detectedCIOnLastAnalysis={detectedCIOnLastAnalysis}
- projectBinding={projectBinding}
/>
<LargeCenteredLayout>
<PageContentFontWrapper>
import DismissableAlert from '../../../components/ui/DismissableAlert';
import { translate } from '../../../helpers/l10n';
import { queryToSearch } from '../../../helpers/urls';
-import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
+import { useProjectBindingQuery } from '../../../queries/devops-integration';
import { ComponentQualifier } from '../../../types/component';
import { Component } from '../../../types/types';
import { CurrentUser, isLoggedIn } from '../../../types/users';
component: Component;
currentUser: CurrentUser;
detectedCIOnLastAnalysis?: boolean;
- projectBinding?: ProjectAlmBindingResponse;
}
export function FirstAnalysisNextStepsNotif(props: FirstAnalysisNextStepsNotifProps) {
- const { component, currentUser, branchesEnabled, detectedCIOnLastAnalysis, projectBinding } =
- props;
+ const { component, currentUser, branchesEnabled, detectedCIOnLastAnalysis } = props;
- if (!isLoggedIn(currentUser) || component.qualifier !== ComponentQualifier.Project) {
+ const { data: projectBinding, isLoading } = useProjectBindingQuery(component.key);
+
+ if (!isLoggedIn(currentUser) || component.qualifier !== ComponentQualifier.Project || isLoading) {
return null;
}
- const showConfigurePullRequestDecoNotif = branchesEnabled && projectBinding === undefined;
+ const showConfigurePullRequestDecoNotif = branchesEnabled && projectBinding == null;
const showConfigureCINotif =
detectedCIOnLastAnalysis !== undefined ? !detectedCIOnLastAnalysis : false;
import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { getMeasuresWithPeriodAndMetrics } from '../../../../api/measures';
+import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
import { getProjectActivity } from '../../../../api/projectActivity';
import {
getApplicationQualityGate,
};
});
+const almHandler = new AlmSettingsServiceMock();
+
beforeEach(jest.clearAllMocks);
+afterEach(() => {
+ almHandler.reset();
+});
+
describe('project overview', () => {
it('should show a successful QG', async () => {
const user = userEvent.setup();
import { isPullRequest } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { useBranchesQuery } from '../../../queries/branch';
-import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
import { isPortfolioLike } from '../../../types/component';
import { Feature } from '../../../types/features';
import { Component } from '../../../types/types';
interface AppProps extends WithAvailableFeaturesProps {
component: Component;
- projectBinding?: ProjectAlmBindingResponse;
}
export function App(props: AppProps) {
- const { component, projectBinding } = props;
+ const { component } = props;
const branchSupportEnabled = props.hasFeature(Feature.BranchSupport);
const { data } = useBranchesQuery(component);
branch={branchLike}
branchesEnabled={branchSupportEnabled}
component={component}
- projectBinding={projectBinding}
/>
)}
</main>
interface Props {
component: Component;
- isGitHubProject: boolean;
+ isGitHubProject?: boolean;
loadHolders: () => void;
loading: boolean;
}
PERMISSIONS_ORDER_BY_QUALIFIER,
convertToPermissionDefinitions,
} from '../../../../helpers/permissions';
+import { useIsGitHubProjectQuery } from '../../../../queries/devops-integration';
import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider';
-import { AlmKeys } from '../../../../types/alm-settings';
import { ComponentContextShape, Visibility } from '../../../../types/component';
import { Permissions } from '../../../../types/permissions';
import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
};
render() {
- const { component, projectBinding } = this.props;
+ const { component } = this.props;
const {
filter,
groups,
order = without(order, Permissions.Browse, Permissions.CodeViewer);
}
const permissions = convertToPermissionDefinitions(order, 'projects_role');
- const isGitHubProject = projectBinding?.alm === AlmKeys.GitHub;
return (
<main className="page page-limited" id="project-permissions-page">
<Helmet defer={false} title={translate('permissions.page')} />
- <PageHeader
- component={component}
- isGitHubProject={isGitHubProject}
- loadHolders={this.loadHolders}
- loading={loading}
- />
- <div>
- <UseQuery query={useGithubProvisioningEnabledQuery}>
- {({ data: githubProvisioningStatus, isFetching }) => (
- <VisibilitySelector
- canTurnToPrivate={canTurnToPrivate}
- className="sw-flex big-spacer-top big-spacer-bottom"
- onChange={this.handleVisibilityChange}
- loading={loading || isFetching}
- disabled={isGitHubProject && !!githubProvisioningStatus}
- visibility={component.visibility}
+ <UseQuery query={useIsGitHubProjectQuery} args={[component.key]}>
+ {({ data: isGitHubProject }) => (
+ <>
+ <PageHeader
+ component={component}
+ isGitHubProject={isGitHubProject}
+ loadHolders={this.loadHolders}
+ loading={loading}
/>
- )}
- </UseQuery>
-
- {disclaimer && (
- <PublicProjectDisclaimer
- component={component}
- onClose={this.handleCloseDisclaimer}
- onConfirm={this.handleTurnProjectToPublic}
- />
+ <div>
+ <UseQuery query={useGithubProvisioningEnabledQuery}>
+ {({ data: githubProvisioningStatus, isFetching }) => (
+ <VisibilitySelector
+ canTurnToPrivate={canTurnToPrivate}
+ className="sw-flex big-spacer-top big-spacer-bottom"
+ onChange={this.handleVisibilityChange}
+ loading={loading || isFetching}
+ disabled={isGitHubProject && !!githubProvisioningStatus}
+ visibility={component.visibility}
+ />
+ )}
+ </UseQuery>
+
+ {disclaimer && (
+ <PublicProjectDisclaimer
+ component={component}
+ onClose={this.handleCloseDisclaimer}
+ onConfirm={this.handleTurnProjectToPublic}
+ />
+ )}
+ </div>
+ </>
)}
- </div>
+ </UseQuery>
+
<AllHoldersList
filter={filter}
onGrantPermissionToGroup={this.handleGrantPermissionToGroup}
onGrantPermissionToUser={this.handleGrantPermissionToUser}
groups={groups}
- isGitHubProject={isGitHubProject}
groupsPaging={groupsPaging}
onFilter={this.handleFilterChange}
onLoadMore={this.handleLoadMore}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { act, screen } from '@testing-library/react';
+import { act, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock';
import AuthenticationServiceMock from '../../../../../api/mocks/AuthenticationServiceMock';
import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock';
import { mockComponent } from '../../../../../helpers/mocks/component';
let serviceMock: PermissionsServiceMock;
let authHandler: AuthenticationServiceMock;
+let almHandler: AlmSettingsServiceMock;
beforeAll(() => {
serviceMock = new PermissionsServiceMock();
authHandler = new AuthenticationServiceMock();
+ almHandler = new AlmSettingsServiceMock();
});
afterEach(() => {
serviceMock.reset();
authHandler.reset();
+ almHandler.reset();
});
describe('rendering', () => {
const user = userEvent.setup();
const ui = getPageObject(user);
authHandler.githubProvisioningStatus = true;
- renderPermissionsProjectApp(
- {},
- { featureList: [Feature.GithubProvisioning] },
- { projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false } }
- );
+ almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+ renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
await ui.appLoaded();
expect(ui.visibilityRadio(Visibility.Public).get()).toBeDisabled();
const user = userEvent.setup();
const ui = getPageObject(user);
authHandler.githubProvisioningStatus = true;
- renderPermissionsProjectApp(
- {},
- { featureList: [Feature.GithubProvisioning] },
- { projectBinding: { alm: AlmKeys.Azure, key: 'test', repository: 'test', monorepo: false } }
- );
+ almHandler.handleSetProjectBinding(AlmKeys.Azure, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+ renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
await ui.appLoaded();
expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
const user = userEvent.setup();
const ui = getPageObject(user);
authHandler.githubProvisioningStatus = false;
- renderPermissionsProjectApp(
- {},
- { featureList: [Feature.GithubProvisioning] },
- { projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false } }
- );
+ almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+ renderPermissionsProjectApp({}, { featureList: [Feature.GithubProvisioning] });
await ui.appLoaded();
expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
const user = userEvent.setup();
const ui = getPageObject(user);
authHandler.githubProvisioningStatus = true;
+ almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
renderPermissionsProjectApp(
{},
{ featureList: [Feature.GithubProvisioning] },
{
component: mockComponent({ visibility: Visibility.Private }),
- projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false },
}
);
await ui.appLoaded();
expect(ui.pageTitle.get()).toBeInTheDocument();
- expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.github_managed/);
+ await waitFor(() =>
+ expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.github_managed/)
+ );
expect(ui.pageTitle.byRole('img').get()).toBeInTheDocument();
expect(ui.githubExplanations.get()).toBeInTheDocument();
const user = userEvent.setup();
const ui = getPageObject(user);
authHandler.githubProvisioningStatus = false;
+ almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
renderPermissionsProjectApp(
{ visibility: Visibility.Private },
- { featureList: [Feature.GithubProvisioning] },
- {
- projectBinding: { alm: AlmKeys.GitHub, key: 'test', repository: 'test', monorepo: false },
- }
+ { featureList: [Feature.GithubProvisioning] }
);
await ui.appLoaded();
contextOverride: Partial<RenderContext> = {},
componentContextOverride: Partial<ComponentContextShape> = {}
) {
- return renderAppWithComponentContext('project_roles', projectPermissionsRoutes, contextOverride, {
- component: mockComponent({
- visibility: Visibility.Public,
- configuration: {
- canUpdateProjectVisibilityToPrivate: true,
- canApplyPermissionTemplate: true,
- },
- ...override,
- }),
- ...componentContextOverride,
- });
+ return renderAppWithComponentContext(
+ 'project_roles?id=my-project',
+ projectPermissionsRoutes,
+ contextOverride,
+ {
+ component: mockComponent({
+ visibility: Visibility.Public,
+ configuration: {
+ canUpdateProjectVisibilityToPrivate: true,
+ canApplyPermissionTemplate: true,
+ },
+ ...override,
+ }),
+ ...componentContextOverride,
+ }
+ );
}
*/
import { cloneDeep } from 'lodash';
import * as React from 'react';
-import {
- deleteProjectAlmBinding,
- getAlmSettings,
- getProjectAlmBinding,
- setProjectAzureBinding,
- setProjectBitbucketBinding,
- setProjectBitbucketCloudBinding,
- setProjectGithubBinding,
- setProjectGitlabBinding,
- validateProjectAlmBinding,
-} from '../../../../api/alm-settings';
+import { getAlmSettings, validateProjectAlmBinding } from '../../../../api/alm-settings';
import withCurrentUserContext from '../../../../app/components/current-user/withCurrentUserContext';
-import { throwGlobalError } from '../../../../helpers/error';
-import { HttpStatus } from '../../../../helpers/request';
import { hasGlobalPermission } from '../../../../helpers/users';
+import {
+ useDeleteProjectAlmBindingMutation,
+ useProjectBindingQuery,
+ useSetProjectBindingMutation,
+} from '../../../../queries/devops-integration';
import {
AlmKeys,
AlmSettingsInstance,
const INITIAL_FORM_DATA = { key: '', repository: '', monorepo: false };
-export class PRDecorationBinding extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = {
- formData: cloneDeep(INITIAL_FORM_DATA),
- instances: [],
- isChanged: false,
- isConfigured: false,
- isValid: false,
- loading: true,
- updating: false,
- successfullyUpdated: false,
- checkingConfiguration: false,
- };
+export function PRDecorationBinding(props: Props) {
+ const { component, currentUser } = props;
+ const [formData, setFormData] = React.useState<FormData>(cloneDeep(INITIAL_FORM_DATA));
+ const [instances, setInstances] = React.useState<AlmSettingsInstance[]>([]);
+ const [configurationErrors, setConfigurationErrors] = React.useState(undefined);
+ const [loading, setLoading] = React.useState(true);
+ const [successfullyUpdated, setSuccessfullyUpdated] = React.useState(false);
+ const [checkingConfiguration, setCheckingConfiguration] = React.useState(false);
+ const { data: originalData } = useProjectBindingQuery(component.key);
+ const { mutateAsync: deleteMutation, isLoading: isDeleting } = useDeleteProjectAlmBindingMutation(
+ component.key
+ );
+ const { mutateAsync: updateMutation, isLoading: isUpdating } = useSetProjectBindingMutation();
+
+ const isConfigured = !!originalData;
+ const updating = isDeleting || isUpdating;
+
+ const isValid = React.useMemo(() => {
+ const validateForm = ({ key, ...additionalFields }: State['formData']) => {
+ const selected = instances.find((i) => i.key === key);
+ if (!key || !selected) {
+ return false;
+ }
+ return REQUIRED_FIELDS_BY_ALM[selected.alm].reduce(
+ (result: boolean, field) => result && Boolean(additionalFields[field]),
+ true
+ );
+ };
- componentDidMount() {
- this.mounted = true;
- this.fetchDefinitions();
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- fetchDefinitions = () => {
- const project = this.props.component.key;
- return Promise.all([getAlmSettings(project), this.getProjectBinding(project)])
- .then(([instances, originalData]) => {
- if (this.mounted) {
- this.setState(({ formData }) => {
- const newFormData = originalData || formData;
- return {
- formData: newFormData,
- instances: instances || [],
- isChanged: false,
- isConfigured: !!originalData,
- isValid: this.validateForm(newFormData),
- loading: false,
- originalData: newFormData,
- configurationErrors: undefined,
- };
- });
- }
- })
- .catch(() => {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- })
- .then(() => this.checkConfiguration());
+ return validateForm(formData);
+ }, [formData, instances]);
+
+ const isDataSame = (
+ { key, repository = '', slug = '', summaryCommentEnabled = false, monorepo = false }: FormData,
+ {
+ key: oKey = '',
+ repository: oRepository = '',
+ slug: oSlug = '',
+ summaryCommentEnabled: osummaryCommentEnabled = false,
+ monorepo: omonorepo = false,
+ }: FormData
+ ) => {
+ return (
+ key === oKey &&
+ repository === oRepository &&
+ slug === oSlug &&
+ summaryCommentEnabled === osummaryCommentEnabled &&
+ monorepo === omonorepo
+ );
};
- getProjectBinding(project: string): Promise<ProjectAlmBindingResponse | undefined> {
- return getProjectAlmBinding(project).catch((response: Response) => {
- if (response && response.status === HttpStatus.NotFound) {
- return undefined;
- }
- return throwGlobalError(response);
- });
- }
+ const isChanged = !isDataSame(formData, originalData ?? cloneDeep(INITIAL_FORM_DATA));
- catchError = () => {
- if (this.mounted) {
- this.setState({ updating: false });
- }
+ React.useEffect(() => {
+ fetchDefinitions();
+ }, []);
+
+ React.useEffect(() => {
+ checkConfiguration();
+ }, [originalData]);
+
+ React.useEffect(() => {
+ setFormData((formData) => originalData ?? formData);
+ }, [originalData]);
+
+ const fetchDefinitions = () => {
+ const project = component.key;
+
+ return getAlmSettings(project)
+ .then((instances) => {
+ setInstances(instances || []);
+ setConfigurationErrors(undefined);
+ setLoading(false);
+ })
+ .catch(() => {
+ setLoading(false);
+ });
};
- handleReset = () => {
- const { component } = this.props;
- this.setState({ updating: true });
- deleteProjectAlmBinding(component.key)
+ const handleReset = () => {
+ deleteMutation()
.then(() => {
- if (this.mounted) {
- this.setState({
- formData: {
- key: '',
- repository: '',
- slug: '',
- monorepo: false,
- },
- originalData: undefined,
- isChanged: false,
- isConfigured: false,
- updating: false,
- successfullyUpdated: true,
- configurationErrors: undefined,
- });
- }
+ setFormData({
+ key: '',
+ repository: '',
+ slug: '',
+ monorepo: false,
+ });
+ setSuccessfullyUpdated(true);
+ setConfigurationErrors(undefined);
})
- .catch(this.catchError);
+ .catch(() => {});
};
- submitProjectAlmBinding(
+ const submitProjectAlmBinding = (
alm: AlmKeys,
key: string,
almSpecificFields: Omit<FormData, 'key'>
- ): Promise<void> {
+ ): Promise<void> => {
const almSetting = key;
const { repository, slug = '', monorepo = false } = almSpecificFields;
- const project = this.props.component.key;
-
- switch (alm) {
- case AlmKeys.Azure: {
- return setProjectAzureBinding({
- almSetting,
- project,
- projectName: slug,
- repositoryName: repository,
- monorepo,
- });
- }
- case AlmKeys.BitbucketServer: {
- return setProjectBitbucketBinding({
- almSetting,
- project,
- repository,
- slug,
- monorepo,
- });
- }
- case AlmKeys.BitbucketCloud: {
- return setProjectBitbucketCloudBinding({
- almSetting,
- project,
- repository,
- monorepo,
- });
- }
- case AlmKeys.GitHub: {
- // By default it must remain true.
- const summaryCommentEnabled = almSpecificFields?.summaryCommentEnabled ?? true;
- return setProjectGithubBinding({
- almSetting,
- project,
- repository,
- summaryCommentEnabled,
- monorepo,
- });
- }
-
- case AlmKeys.GitLab: {
- return setProjectGitlabBinding({
- almSetting,
- project,
- repository,
- monorepo,
- });
- }
-
- default:
- return Promise.reject();
+ const project = component.key;
+
+ const baseParams = {
+ almSetting,
+ project,
+ repository,
+ monorepo,
+ };
+ let updateParams;
+
+ if (alm === AlmKeys.Azure || alm === AlmKeys.BitbucketServer) {
+ updateParams = {
+ alm,
+ ...baseParams,
+ slug,
+ };
+ } else if (alm === AlmKeys.GitHub) {
+ updateParams = {
+ alm,
+ ...baseParams,
+ summaryCommentEnabled: almSpecificFields?.summaryCommentEnabled ?? true,
+ };
+ } else {
+ updateParams = {
+ alm,
+ ...baseParams,
+ };
}
- }
- checkConfiguration = async () => {
- const {
- component: { key: projectKey },
- } = this.props;
+ return updateMutation(updateParams);
+ };
- const { isConfigured } = this.state;
+ const checkConfiguration = async () => {
+ const projectKey = component.key;
if (!isConfigured) {
return;
}
- this.setState({ checkingConfiguration: true, configurationErrors: undefined });
+ setCheckingConfiguration(true);
+ setConfigurationErrors(undefined);
const configurationErrors = await validateProjectAlmBinding(projectKey).catch((error) => error);
- if (this.mounted) {
- this.setState({ checkingConfiguration: false, configurationErrors });
- }
+ setCheckingConfiguration(false);
+ setConfigurationErrors(configurationErrors);
};
- handleSubmit = () => {
- this.setState({ updating: true });
- const {
- formData: { key, ...additionalFields },
- instances,
- } = this.state;
+ const handleSubmit = () => {
+ const { key, ...additionalFields } = formData;
const selected = instances.find((i) => i.key === key);
if (!key || !selected) {
return;
}
- this.submitProjectAlmBinding(selected.alm, key, additionalFields)
+ submitProjectAlmBinding(selected.alm, key, additionalFields)
.then(() => {
- if (this.mounted) {
- this.setState({
- updating: false,
- successfullyUpdated: true,
- });
- }
+ setSuccessfullyUpdated(true);
})
- .then(this.fetchDefinitions)
- .catch(this.catchError);
- };
-
- isDataSame(
- { key, repository = '', slug = '', summaryCommentEnabled = false, monorepo = false }: FormData,
- {
- key: oKey = '',
- repository: oRepository = '',
- slug: oSlug = '',
- summaryCommentEnabled: osummaryCommentEnabled = false,
- monorepo: omonorepo = false,
- }: FormData
- ) {
- return (
- key === oKey &&
- repository === oRepository &&
- slug === oSlug &&
- summaryCommentEnabled === osummaryCommentEnabled &&
- monorepo === omonorepo
- );
- }
-
- handleFieldChange = (id: keyof ProjectAlmBindingResponse, value: string | boolean) => {
- this.setState(({ formData, originalData }) => {
- const newFormData = {
- ...formData,
- [id]: value,
- };
-
- return {
- formData: newFormData,
- isValid: this.validateForm(newFormData),
- isChanged: !this.isDataSame(newFormData, originalData || cloneDeep(INITIAL_FORM_DATA)),
- successfullyUpdated: false,
- };
- });
+ .then(fetchDefinitions)
+ .catch(() => {});
};
- validateForm = ({ key, ...additionalFields }: State['formData']) => {
- const { instances } = this.state;
- const selected = instances.find((i) => i.key === key);
- if (!key || !selected) {
- return false;
- }
- return REQUIRED_FIELDS_BY_ALM[selected.alm].reduce(
- (result: boolean, field) => result && Boolean(additionalFields[field]),
- true
- );
+ const handleFieldChange = (id: keyof ProjectAlmBindingResponse, value: string | boolean) => {
+ setFormData((formData) => ({
+ ...formData,
+ [id]: value,
+ }));
+ setSuccessfullyUpdated(false);
};
- handleCheckConfiguration = async () => {
- await this.checkConfiguration();
+ const handleCheckConfiguration = async () => {
+ await checkConfiguration();
};
- render() {
- const { currentUser } = this.props;
-
- return (
- <PRDecorationBindingRenderer
- onFieldChange={this.handleFieldChange}
- onReset={this.handleReset}
- onSubmit={this.handleSubmit}
- onCheckConfiguration={this.handleCheckConfiguration}
- isSysAdmin={hasGlobalPermission(currentUser, Permissions.Admin)}
- {...this.state}
- />
- );
- }
+ return (
+ <PRDecorationBindingRenderer
+ onFieldChange={handleFieldChange}
+ onReset={handleReset}
+ onSubmit={handleSubmit}
+ onCheckConfiguration={handleCheckConfiguration}
+ isSysAdmin={hasGlobalPermission(currentUser, Permissions.Admin)}
+ instances={instances}
+ formData={formData}
+ isChanged={isChanged}
+ isValid={isValid}
+ isConfigured={isConfigured}
+ loading={loading}
+ updating={updating}
+ successfullyUpdated={successfullyUpdated}
+ checkingConfiguration={checkingConfiguration}
+ configurationErrors={configurationErrors}
+ />
+ );
}
export default withCurrentUserContext(PRDecorationBinding);
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import selectEvent from 'react-select-event';
import { CurrentUser } from '../../../../../types/users';
import PRDecorationBinding from '../PRDecorationBinding';
-jest.mock('../../../../../api/alm-settings');
-
let almSettings: AlmSettingsServiceMock;
beforeAll(() => {
await ui.setInput(inputId, value);
}
// Save form and check for errors
- await user.click(ui.saveButton.get());
- expect(ui.validationMsg('cute error').get()).toBeInTheDocument();
+ await act(() => user.click(ui.saveButton.get()));
+ expect(await ui.validationMsg('cute error').find()).toBeInTheDocument();
// Check validation with errors
- await user.click(ui.validateButton.get());
+ await act(() => user.click(ui.validateButton.get()));
expect(ui.validationMsg('cute error').get()).toBeInTheDocument();
// Save form and check for errors
Object.keys(list).find((key) => key.endsWith('.repository')) as string,
'Anything'
);
- await user.click(ui.saveButton.get());
+ await act(() => user.click(ui.saveButton.get()));
expect(
await ui.validationMsg('settings.pr_decoration.binding.check_configuration.success').find()
).toBeInTheDocument();
- await user.click(ui.validateButton.get());
+ await act(() => user.click(ui.validateButton.get()));
expect(
ui.validationMsg('settings.pr_decoration.binding.check_configuration.success').get()
).toBeInTheDocument();
expect(ui.saveButton.query()).not.toBeInTheDocument();
// Reset binding
- await user.click(ui.resetButton.get());
+ await act(() => user.click(ui.resetButton.get()));
expect(ui.input('', 'textbox').query()).not.toBeInTheDocument();
expect(ui.input('', 'switch').query()).not.toBeInTheDocument();
}
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import TutorialSelection from '../../../components/tutorials/TutorialSelection';
import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication';
-import { ProjectAlmBindingResponse } from '../../../types/alm-settings';
import { Component } from '../../../types/types';
import { CurrentUser, isLoggedIn } from '../../../types/users';
export interface TutorialsAppProps {
component: Component;
currentUser: CurrentUser;
- projectBinding?: ProjectAlmBindingResponse;
}
export function TutorialsApp(props: TutorialsAppProps) {
- const { component, currentUser, projectBinding } = props;
+ const { component, currentUser } = props;
if (!isLoggedIn(currentUser)) {
handleRequiredAuthentication();
return (
<LargeCenteredLayout className="sw-pt-8">
<PageContentFontWrapper>
- <TutorialSelection
- component={component}
- currentUser={currentUser}
- projectBinding={projectBinding}
- />
+ <TutorialSelection component={component} currentUser={currentUser} />
</PageContentFontWrapper>
</LargeCenteredLayout>
);
selectedPermission?: string;
onSelectPermission?: (permissions?: string) => void;
loading?: boolean;
- isGitHubProject?: boolean;
}
export default class AllHoldersList extends React.PureComponent<Props> {
};
render() {
- const {
- filter,
- query,
- groups,
- users,
- permissions,
- selectedPermission,
- loading,
- isGitHubProject,
- } = this.props;
+ const { filter, query, groups, users, permissions, selectedPermission, loading } = this.props;
const { count, total } = this.getPaging();
return (
<>
<HoldersList
loading={loading}
- isGitHubProject={isGitHubProject}
filter={filter}
groups={groups}
onSelectPermission={this.props.onSelectPermission}
import UseQuery from '../../helpers/UseQuery';
import { translate } from '../../helpers/l10n';
import { isPermissionDefinitionGroup } from '../../helpers/permissions';
+import { useIsGitHubProjectQuery } from '../../queries/devops-integration';
import { useGithubProvisioningEnabledQuery } from '../../queries/identity-provider';
import { Dict, PermissionDefinitions, PermissionGroup, PermissionUser } from '../../types/types';
import GroupHolder from './GroupHolder';
permissions: PermissionDefinitions;
query?: string;
selectedPermission?: string;
- isGitHubProject?: boolean;
users: PermissionUser[];
}
}
renderItem(item: PermissionUser | PermissionGroup, permissions: PermissionDefinitions) {
- const { isGitHubProject, selectedPermission, isComponentPrivate } = this.props;
+ const { selectedPermission, isComponentPrivate } = this.props;
return (
- <UseQuery key={this.getKey(item)} query={useGithubProvisioningEnabledQuery}>
- {({ data: githubProvisioningStatus }) => (
- <>
- {this.isPermissionUser(item) ? (
- <UserHolder
- key={`user-${item.login}`}
- onToggle={this.handleUserToggle}
- permissions={permissions}
- selectedPermission={selectedPermission}
- user={item}
- disabled={isGitHubProject && !!githubProvisioningStatus && item.managed}
- removeOnly={isGitHubProject && !!githubProvisioningStatus && !item.managed}
- isGitHubProject={isGitHubProject}
- />
- ) : (
- <GroupHolder
- group={item}
- isComponentPrivate={isComponentPrivate}
- key={`group-${item.id || item.name}`}
- onToggle={this.handleGroupToggle}
- permissions={permissions}
- selectedPermission={selectedPermission}
- disabled={isGitHubProject && !!githubProvisioningStatus && item.managed}
- removeOnly={isGitHubProject && !!githubProvisioningStatus && !item.managed}
- isGitHubProject={isGitHubProject}
- />
+ <UseQuery key={this.getKey(item)} query={useIsGitHubProjectQuery}>
+ {({ data: isGitHubProject }) => (
+ <UseQuery query={useGithubProvisioningEnabledQuery}>
+ {({ data: githubProvisioningStatus }) => (
+ <>
+ {this.isPermissionUser(item) ? (
+ <UserHolder
+ key={`user-${item.login}`}
+ onToggle={this.handleUserToggle}
+ permissions={permissions}
+ selectedPermission={selectedPermission}
+ user={item}
+ disabled={isGitHubProject && !!githubProvisioningStatus && item.managed}
+ removeOnly={isGitHubProject && !!githubProvisioningStatus && !item.managed}
+ isGitHubProject={isGitHubProject}
+ />
+ ) : (
+ <GroupHolder
+ group={item}
+ isComponentPrivate={isComponentPrivate}
+ key={`group-${item.id || item.name}`}
+ onToggle={this.handleGroupToggle}
+ permissions={permissions}
+ selectedPermission={selectedPermission}
+ disabled={isGitHubProject && !!githubProvisioningStatus && item.managed}
+ removeOnly={isGitHubProject && !!githubProvisioningStatus && !item.managed}
+ isGitHubProject={isGitHubProject}
+ />
+ )}
+ </>
)}
- </>
+ </UseQuery>
)}
</UseQuery>
);
import { getValue } from '../../api/settings';
import { getHostUrl } from '../../helpers/urls';
import { hasGlobalPermission } from '../../helpers/users';
-import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../types/alm-settings';
+import { useProjectBindingQuery } from '../../queries/devops-integration';
+import { AlmSettingsInstance } from '../../types/alm-settings';
import { Permissions } from '../../types/permissions';
import { SettingsKey } from '../../types/settings';
import { Component } from '../../types/types';
interface Props {
component: Component;
currentUser: LoggedInUser;
- projectBinding?: ProjectAlmBindingResponse;
willRefreshAutomatically?: boolean;
location: Location;
}
-interface State {
- almBinding?: AlmSettingsInstance;
- currentUserCanScanProject: boolean;
- baseUrl: string;
- loading: boolean;
-}
-
-export class TutorialSelection extends React.PureComponent<Props, State> {
- mounted = false;
- state: State = {
- currentUserCanScanProject: false,
- baseUrl: getHostUrl(),
- loading: true,
- };
-
- async componentDidMount() {
- this.mounted = true;
-
- await Promise.all([this.fetchAlmBindings(), this.fetchBaseUrl(), this.checkUserPermissions()]);
-
- if (this.mounted) {
- this.setState({ loading: false });
- }
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
+export function TutorialSelection(props: Props) {
+ const { component, currentUser, location, willRefreshAutomatically } = props;
+ const [currentUserCanScanProject, setCurrentUserCanScanProject] = React.useState(false);
+ const [baseUrl, setBaseUrl] = React.useState(getHostUrl());
+ const [loading, setLoading] = React.useState(true);
+ const [loadingAlm, setLoadingAlm] = React.useState(false);
+ const [almBinding, setAlmBinding] = React.useState<AlmSettingsInstance | undefined>(undefined);
+ const { data: projectBinding } = useProjectBindingQuery(component.key);
+
+ React.useEffect(() => {
+ const checkUserPermissions = async () => {
+ if (hasGlobalPermission(currentUser, Permissions.Scan)) {
+ setCurrentUserCanScanProject(true);
+ return Promise.resolve();
+ }
- checkUserPermissions = async () => {
- const { component, currentUser } = this.props;
+ const { projects } = await getScannableProjects();
+ setCurrentUserCanScanProject(projects.find((p) => p.key === component.key) !== undefined);
- if (hasGlobalPermission(currentUser, Permissions.Scan)) {
- this.setState({ currentUserCanScanProject: true });
return Promise.resolve();
- }
-
- const { projects } = await getScannableProjects();
- this.setState({
- currentUserCanScanProject: projects.find((p) => p.key === component.key) !== undefined,
- });
+ };
- return Promise.resolve();
- };
-
- fetchAlmBindings = async () => {
- const { component, projectBinding } = this.props;
-
- if (projectBinding !== undefined) {
- const almSettings = await getAlmSettingsNoCatch(component.key).catch(() => undefined);
- if (this.mounted) {
+ const fetchBaseUrl = async () => {
+ const setting = await getValue({ key: SettingsKey.ServerBaseUrl }).catch(() => undefined);
+ const baseUrl = setting?.value;
+ if (baseUrl && baseUrl.length > 0) {
+ setBaseUrl(baseUrl);
+ }
+ };
+
+ Promise.all([fetchBaseUrl(), checkUserPermissions()])
+ .then(() => {
+ setLoading(false);
+ })
+ .catch(() => {});
+ }, [component.key, currentUser]);
+
+ React.useEffect(() => {
+ const fetchAlmBindings = async () => {
+ if (projectBinding != null) {
+ setLoadingAlm(true);
+ const almSettings = await getAlmSettingsNoCatch(component.key).catch(() => undefined);
let almBinding;
if (almSettings !== undefined) {
almBinding = almSettings.find((d) => d.key === projectBinding.key);
}
- this.setState({ almBinding });
+ setAlmBinding(almBinding);
+ setLoadingAlm(false);
}
- }
- };
-
- fetchBaseUrl = async () => {
- const setting = await getValue({ key: SettingsKey.ServerBaseUrl }).catch(() => undefined);
- const baseUrl = setting?.value;
- if (baseUrl && baseUrl.length > 0 && this.mounted) {
- this.setState({ baseUrl });
- }
- };
-
- render() {
- const { component, currentUser, location, projectBinding, willRefreshAutomatically } =
- this.props;
- const { almBinding, baseUrl, currentUserCanScanProject, loading } = this.state;
-
- const selectedTutorial: TutorialModes | undefined = location.query?.selectedTutorial;
-
- return (
- <TutorialSelectionRenderer
- almBinding={almBinding}
- baseUrl={baseUrl}
- component={component}
- currentUser={currentUser}
- currentUserCanScanProject={currentUserCanScanProject}
- loading={loading}
- projectBinding={projectBinding}
- selectedTutorial={selectedTutorial}
- willRefreshAutomatically={willRefreshAutomatically}
- />
- );
- }
+ };
+
+ fetchAlmBindings().catch(() => {});
+ }, [component.key, projectBinding]);
+
+ const selectedTutorial: TutorialModes | undefined = location.query?.selectedTutorial;
+
+ return (
+ <TutorialSelectionRenderer
+ almBinding={almBinding}
+ baseUrl={baseUrl}
+ component={component}
+ currentUser={currentUser}
+ currentUserCanScanProject={currentUserCanScanProject}
+ loading={loading || loadingAlm}
+ projectBinding={projectBinding}
+ selectedTutorial={selectedTutorial}
+ willRefreshAutomatically={willRefreshAutomatically}
+ />
+ );
}
export default withRouter(TutorialSelection);
let showAzurePipelines = true;
let showJenkins = true;
- if (projectBinding !== undefined) {
+ if (projectBinding != null) {
showGitHubActions = projectBinding.alm === AlmKeys.GitHub;
showGitLabCICD = projectBinding.alm === AlmKeys.GitLab;
showBitbucketPipelines = projectBinding.alm === AlmKeys.BitbucketCloud;
component={component}
currentUser={currentUser}
mainBranchName={mainBranchName}
- projectBinding={projectBinding}
willRefreshAutomatically={willRefreshAutomatically}
/>
)}
component={component}
currentUser={currentUser}
mainBranchName={mainBranchName}
- projectBinding={projectBinding}
willRefreshAutomatically={willRefreshAutomatically}
/>
)}
almBinding={almBinding}
baseUrl={baseUrl}
component={component}
- projectBinding={projectBinding}
willRefreshAutomatically={willRefreshAutomatically}
/>
)}
import userEvent from '@testing-library/user-event';
import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup';
import * as React from 'react';
-import { getAlmSettingsNoCatch } from '../../../api/alm-settings';
import { getScannableProjects } from '../../../api/components';
+import AlmSettingsServiceMock from '../../../api/mocks/AlmSettingsServiceMock';
import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
import UserTokensMock from '../../../api/mocks/UserTokensMock';
-import { mockProjectAlmBindingResponse } from '../../../helpers/mocks/alm-settings';
import { mockComponent } from '../../../helpers/mocks/component';
import { mockLoggedInUser } from '../../../helpers/testMocks';
import { renderApp } from '../../../helpers/testReactTestingUtils';
getHostUrl: jest.fn().mockReturnValue('http://host.url'),
}));
-jest.mock('../../../api/alm-settings', () => ({
- getAlmSettingsNoCatch: jest.fn().mockRejectedValue(null),
-}));
-
jest.mock('../../../api/components', () => ({
getScannableProjects: jest.fn().mockResolvedValue({ projects: [] }),
}));
let settingsMock: SettingsServiceMock;
let tokenMock: UserTokensMock;
+let almMock: AlmSettingsServiceMock;
beforeAll(() => {
settingsMock = new SettingsServiceMock();
tokenMock = new UserTokensMock();
+ almMock = new AlmSettingsServiceMock();
});
afterEach(() => {
tokenMock.reset();
settingsMock.reset();
+ almMock.reset();
});
beforeEach(jest.clearAllMocks);
[AlmKeys.BitbucketServer, [TutorialModes.Jenkins]],
[AlmKeys.BitbucketCloud, [TutorialModes.BitbucketPipelines, TutorialModes.Jenkins]],
])('should show correct buttons if project is bound to %s', async (alm, modes) => {
- renderTutorialSelection({ projectBinding: mockProjectAlmBindingResponse({ alm }) });
+ almMock.handleSetProjectBinding(alm, {
+ project: 'foo',
+ almSetting: 'foo',
+ repository: 'repo',
+ monorepo: false,
+ });
+ renderTutorialSelection();
await waitOnDataLoaded();
modes.forEach((mode) => expect(ui.chooseTutorialLink(mode).get()).toBeInTheDocument());
});
it('should correctly fetch the corresponding ALM setting', async () => {
- jest
- .mocked(getAlmSettingsNoCatch)
- .mockResolvedValueOnce([
- { key: 'binding', url: 'https://enterprise.github.com', alm: AlmKeys.GitHub },
- ]);
- renderTutorialSelection(
- {
- projectBinding: mockProjectAlmBindingResponse({ alm: AlmKeys.GitHub, key: 'binding' }),
- },
- `tutorials?selectedTutorial=${TutorialModes.Jenkins}&id=bar`
- );
+ almMock.handleSetProjectBinding(AlmKeys.GitHub, {
+ project: 'foo',
+ almSetting: 'conf-github-1',
+ repository: 'repo',
+ monorepo: false,
+ });
+ renderTutorialSelection({}, `tutorials?selectedTutorial=${TutorialModes.Jenkins}&id=foo`);
await waitOnDataLoaded();
- expect(screen.getByText('https://enterprise.github.com', { exact: false })).toBeInTheDocument();
+ expect(await screen.findByText('http://url', { exact: false })).toBeInTheDocument();
});
it('should correctly fetch the instance URL', async () => {
import { BasicSeparator, Title, TutorialStep, TutorialStepList } from 'design-system';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
-import {
- AlmKeys,
- AlmSettingsInstance,
- ProjectAlmBindingResponse,
-} from '../../../types/alm-settings';
+import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import { Component } from '../../../types/types';
import { LoggedInUser } from '../../../types/users';
import AllSet from '../components/AllSet';
component: Component;
currentUser: LoggedInUser;
mainBranchName: string;
- projectBinding?: ProjectAlmBindingResponse;
willRefreshAutomatically?: boolean;
}
export default function BitbucketPipelinesTutorial(props: BitbucketPipelinesTutorialProps) {
- const {
- almBinding,
- baseUrl,
- currentUser,
- component,
- projectBinding,
- willRefreshAutomatically,
- mainBranchName,
- } = props;
+ const { almBinding, baseUrl, currentUser, component, willRefreshAutomatically, mainBranchName } =
+ props;
const [done, setDone] = React.useState<boolean>(false);
return (
baseUrl={baseUrl}
component={component}
currentUser={currentUser}
- projectBinding={projectBinding}
/>
</TutorialStep>
<TutorialStep title={translate('onboarding.tutorial.with.bitbucket_pipelines.yaml.title')}>
{done && (
<>
<BasicSeparator className="sw-my-10" />
- <AllSet alm={AlmKeys.GitLab} willRefreshAutomatically={willRefreshAutomatically} />
+ <AllSet
+ alm={AlmKeys.BitbucketCloud}
+ willRefreshAutomatically={willRefreshAutomatically}
+ />
</>
)}
</TutorialStepList>
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate } from '../../../helpers/l10n';
-import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../../types/alm-settings';
+import { useProjectBindingQuery } from '../../../queries/devops-integration';
+import { AlmSettingsInstance } from '../../../types/alm-settings';
import { Component } from '../../../types/types';
import { LoggedInUser } from '../../../types/users';
import { InlineSnippet } from '../components/InlineSnippet';
baseUrl: string;
component: Component;
currentUser: LoggedInUser;
- projectBinding?: ProjectAlmBindingResponse;
}
export default function RepositoryVariables(props: RepositoryVariablesProps) {
- const { almBinding, baseUrl, component, currentUser, projectBinding } = props;
+ const { almBinding, baseUrl, component, currentUser } = props;
+ const { data: projectBinding } = useProjectBindingQuery(component.key);
return (
<>
<FormattedMessage
import userEvent from '@testing-library/user-event';
import React from 'react';
import selectEvent from 'react-select-event';
+import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
import UserTokensMock from '../../../../api/mocks/UserTokensMock';
-import {
- mockAlmSettingsInstance,
- mockProjectAlmBindingResponse,
-} from '../../../../helpers/mocks/alm-settings';
+import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockLanguage, mockLoggedInUser } from '../../../../helpers/testMocks';
import { RenderContext, renderApp } from '../../../../helpers/testReactTestingUtils';
}));
const tokenMock = new UserTokensMock();
+const almMock = new AlmSettingsServiceMock();
afterEach(() => {
tokenMock.reset();
+ almMock.reset();
});
const ui = {
it('navigates between steps', async () => {
const user = userEvent.setup();
+ almMock.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'my-project',
+ project: 'my-project',
+ repository: 'my-project',
+ monorepo: true,
+ });
renderBitbucketPipelinesTutorial({
almBinding: mockAlmSettingsInstance({
alm: AlmKeys.BitbucketCloud,
url: 'http://localhost/qube',
}),
- projectBinding: mockProjectAlmBindingResponse({
- alm: AlmKeys.BitbucketCloud,
- repository: 'my-project',
- }),
});
// If project is bound, link to repo is visible
import { BasicSeparator, Title, TutorialStep, TutorialStepList } from 'design-system';
import * as React from 'react';
import { translate } from '../../../helpers/l10n';
-import {
- AlmKeys,
- AlmSettingsInstance,
- ProjectAlmBindingResponse,
-} from '../../../types/alm-settings';
+import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import { Component } from '../../../types/types';
import { LoggedInUser } from '../../../types/users';
import AllSet from '../components/AllSet';
component: Component;
currentUser: LoggedInUser;
mainBranchName: string;
- projectBinding?: ProjectAlmBindingResponse;
willRefreshAutomatically?: boolean;
}
export default function GitHubActionTutorial(props: GitHubActionTutorialProps) {
const [done, setDone] = React.useState<boolean>(false);
- const {
- almBinding,
- baseUrl,
- currentUser,
- component,
- projectBinding,
- mainBranchName,
- willRefreshAutomatically,
- } = props;
+ const { almBinding, baseUrl, currentUser, component, mainBranchName, willRefreshAutomatically } =
+ props;
return (
<>
<Title>{translate('onboarding.tutorial.with.github_ci.title')}</Title>
baseUrl={baseUrl}
component={component}
currentUser={currentUser}
- projectBinding={projectBinding}
/>
</TutorialStep>
<TutorialStep title={translate('onboarding.tutorial.with.github_action.yaml.title')}>
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { translate } from '../../../helpers/l10n';
-import { AlmSettingsInstance, ProjectAlmBindingResponse } from '../../../types/alm-settings';
+import { useProjectBindingQuery } from '../../../queries/devops-integration';
+import { AlmSettingsInstance } from '../../../types/alm-settings';
import { Component } from '../../../types/types';
import { LoggedInUser } from '../../../types/users';
import { InlineSnippet } from '../components/InlineSnippet';
baseUrl: string;
component: Component;
currentUser: LoggedInUser;
- projectBinding?: ProjectAlmBindingResponse;
}
export default function SecretStep(props: SecretStepProps) {
- const { almBinding, baseUrl, component, currentUser, projectBinding } = props;
+ const { almBinding, baseUrl, component, currentUser } = props;
+ const { data: projectBinding } = useProjectBindingQuery(component.key);
return (
<>
import userEvent from '@testing-library/user-event';
import React from 'react';
import selectEvent from 'react-select-event';
+import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
import UserTokensMock from '../../../../api/mocks/UserTokensMock';
-import {
- mockAlmSettingsInstance,
- mockProjectAlmBindingResponse,
-} from '../../../../helpers/mocks/alm-settings';
+import { mockAlmSettingsInstance } from '../../../../helpers/mocks/alm-settings';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockLanguage, mockLoggedInUser } from '../../../../helpers/testMocks';
import { RenderContext, renderApp } from '../../../../helpers/testReactTestingUtils';
}));
const tokenMock = new UserTokensMock();
+const almMock = new AlmSettingsServiceMock();
afterEach(() => {
tokenMock.reset();
+ almMock.reset();
});
const ui = {
it('navigates between steps', async () => {
const user = userEvent.setup();
+ almMock.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'my-project',
+ project: 'my-project',
+ repository: 'my-project',
+ monorepo: true,
+ });
renderGithubActionTutorial({
almBinding: mockAlmSettingsInstance({
alm: AlmKeys.GitHub,
url: 'http://localhost/qube',
}),
- projectBinding: mockProjectAlmBindingResponse({ alm: AlmKeys.GitHub }),
});
// If project is bound, link to repo is visible
WithAvailableFeaturesProps,
} from '../../../app/components/available-features/withAvailableFeatures';
import { translate } from '../../../helpers/l10n';
-import {
- AlmKeys,
- AlmSettingsInstance,
- ProjectAlmBindingResponse,
-} from '../../../types/alm-settings';
+import { useProjectBindingQuery } from '../../../queries/devops-integration';
+import { AlmKeys, AlmSettingsInstance } from '../../../types/alm-settings';
import { Feature } from '../../../types/features';
import { Component } from '../../../types/types';
import AllSet from '../components/AllSet';
almBinding?: AlmSettingsInstance;
baseUrl: string;
component: Component;
- projectBinding?: ProjectAlmBindingResponse;
willRefreshAutomatically?: boolean;
}
export function JenkinsTutorial(props: JenkinsTutorialProps) {
- const { almBinding, baseUrl, component, projectBinding, willRefreshAutomatically } = props;
+ const { almBinding, baseUrl, component, willRefreshAutomatically } = props;
+ const { data: projectBinding } = useProjectBindingQuery(component.key);
const hasSelectAlmStep = projectBinding?.alm === undefined;
const branchSupportEnabled = props.hasFeature(Feature.BranchSupport);
const [alm, setAlm] = React.useState<AlmKeys | undefined>(projectBinding?.alm);
const [done, setDone] = React.useState(false);
+ React.useEffect(() => {
+ setAlm(projectBinding?.alm);
+ }, [projectBinding]);
+
return (
<>
<Title>{translate('onboarding.tutorial.with.jenkins.title')}</Title>
</ListItem>
<ListItem>
{almBinding !== undefined &&
- projectBinding !== undefined &&
+ projectBinding != null &&
buildGithubLink(almBinding, projectBinding) !== null ? (
<LabelValuePair
translationKey="onboarding.tutorial.with.jenkins.multi_branch_pipeline.step2.github.repo_url"
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
+import AlmSettingsServiceMock from '../../../../api/mocks/AlmSettingsServiceMock';
import UserTokensMock from '../../../../api/mocks/UserTokensMock';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockLanguage } from '../../../../helpers/testMocks';
}));
const tokenMock = new UserTokensMock();
+const almMock = new AlmSettingsServiceMock();
afterEach(() => {
tokenMock.reset();
+ almMock.reset();
});
const ui = {
'%s: completes tutorial with bound alm and project',
async (alm: AlmKeys) => {
const user = userEvent.setup();
+ await almMock.handleSetProjectBinding(alm, {
+ almSetting: 'my-project',
+ project: 'my-project',
+ repository: 'my-project',
+ monorepo: true,
+ });
renderJenkinsTutorial({
almBinding: {
alm,
url: 'http://localhost/qube',
key: 'my-project',
},
- projectBinding: {
- alm,
- key: 'my-project',
- repository: 'my-project',
- monorepo: true,
- },
});
- expect(ui.devopsPlatformTitle.query()).not.toBeInTheDocument();
+ await waitFor(() => expect(ui.devopsPlatformTitle.query()).not.toBeInTheDocument());
expect(ui.webhookAlmLink(alm).get()).toBeInTheDocument();
await user.click(ui.mavenBuildButton.get());
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { UseQueryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useLocation } from 'react-router-dom';
+import {
+ deleteProjectAlmBinding,
+ getProjectAlmBinding,
+ setProjectAzureBinding,
+ setProjectBitbucketBinding,
+ setProjectBitbucketCloudBinding,
+ setProjectGithubBinding,
+ setProjectGitlabBinding,
+} from '../api/alm-settings';
+import { HttpStatus } from '../helpers/request';
+import { AlmKeys, ProjectAlmBindingParams, ProjectAlmBindingResponse } from '../types/alm-settings';
+
+function useProjectKeyFromLocation() {
+ const location = useLocation();
+ const search = new URLSearchParams(location.search);
+ const id = search.get('id');
+ return id as string;
+}
+
+export function useProjectBindingQuery<T = ProjectAlmBindingResponse>(
+ project?: string,
+ options?: UseQueryOptions<
+ ProjectAlmBindingResponse,
+ unknown,
+ T,
+ ['devops_integration', string, 'binding']
+ >
+) {
+ const keyFromUrl = useProjectKeyFromLocation();
+
+ const projectKey = project ?? keyFromUrl;
+
+ return useQuery(
+ ['devops_integration', projectKey, 'binding'],
+ ({ queryKey: [_, key] }) =>
+ getProjectAlmBinding(key).catch((e: Response) => {
+ if (e.status === HttpStatus.NotFound) {
+ return null;
+ }
+ throw e;
+ }),
+ {
+ staleTime: 60_000,
+ retry: false,
+ ...options,
+ }
+ );
+}
+
+export function useIsGitHubProjectQuery(project?: string) {
+ return useProjectBindingQuery(project, { select: (data) => data?.alm === AlmKeys.GitHub });
+}
+
+export function useDeleteProjectAlmBindingMutation(project?: string) {
+ const keyFromUrl = useProjectKeyFromLocation();
+ const client = useQueryClient();
+ return useMutation({
+ mutationFn: () => deleteProjectAlmBinding(project ?? keyFromUrl),
+ onSuccess: () => {
+ client.invalidateQueries(['devops_integration', project ?? keyFromUrl, 'binding']);
+ },
+ });
+}
+
+const getSetProjectBindingFn = (data: SetBindingParams) => {
+ const { alm, almSetting, project, monorepo, slug, repository, summaryCommentEnabled } = data;
+ switch (alm) {
+ case AlmKeys.Azure: {
+ return setProjectAzureBinding({
+ almSetting,
+ project,
+ projectName: slug,
+ repositoryName: repository,
+ monorepo,
+ });
+ }
+ case AlmKeys.BitbucketServer: {
+ return setProjectBitbucketBinding({
+ almSetting,
+ project,
+ repository,
+ slug,
+ monorepo,
+ });
+ }
+ case AlmKeys.BitbucketCloud: {
+ return setProjectBitbucketCloudBinding({
+ almSetting,
+ project,
+ repository,
+ monorepo,
+ });
+ }
+ case AlmKeys.GitHub: {
+ return setProjectGithubBinding({
+ almSetting,
+ project,
+ repository,
+ summaryCommentEnabled,
+ monorepo,
+ });
+ }
+
+ case AlmKeys.GitLab: {
+ return setProjectGitlabBinding({
+ almSetting,
+ project,
+ repository,
+ monorepo,
+ });
+ }
+
+ default:
+ return Promise.reject();
+ }
+};
+
+type SetBindingParams = ProjectAlmBindingParams & {
+ repository: string;
+} & (
+ | { alm: AlmKeys.Azure | AlmKeys.BitbucketServer; slug: string; summaryCommentEnabled?: never }
+ | { alm: AlmKeys.GitHub; summaryCommentEnabled: boolean; slug?: never }
+ | {
+ alm: Exclude<AlmKeys, AlmKeys.Azure | AlmKeys.GitHub | AlmKeys.BitbucketServer>;
+ slug?: never;
+ summaryCommentEnabled?: never;
+ }
+ );
+
+export function useSetProjectBindingMutation() {
+ const client = useQueryClient();
+ return useMutation({
+ mutationFn: (data: SetBindingParams) => getSetProjectBindingFn(data),
+ onSuccess: (_, variables) => {
+ client.invalidateQueries(['devops_integration', variables.project, 'binding']);
+ },
+ });
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { ProjectAlmBindingResponse } from './alm-settings';
import { Component, LightComponent } from './types';
export enum Visibility {
isPending?: boolean;
onComponentChange: (changes: Partial<Component>) => void;
fetchComponent: () => Promise<void>;
- projectBinding?: ProjectAlmBindingResponse;
}