import { isPortfolioLike } from '~sonar-aligned/helpers/component';
import GitHubSynchronisationWarning from '../../../../app/components/GitHubSynchronisationWarning';
import { Image } from '../../../../components/common/Image';
-import { translate } from '../../../../helpers/l10n';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { isDefined } from '../../../../helpers/types';
+import {
+ useIsGitHubProjectQuery,
+ useIsGitLabProjectQuery,
+} from '../../../../queries/devops-integration';
import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github';
+import { useGilabProvisioningEnabledQuery } from '../../../../queries/identity-provider/gitlab';
import { isApplication, isProject } from '../../../../types/component';
import { Component } from '../../../../types/types';
import ApplyTemplate from './ApplyTemplate';
interface Props {
component: Component;
- isGitHubProject?: boolean;
loadHolders: () => void;
}
-export default function PageHeader(props: Props) {
+export default function PageHeader(props: Readonly<Props>) {
+ const { component, loadHolders } = props;
+ const { configuration, key, qualifier, visibility } = component;
const [applyTemplateModal, setApplyTemplateModal] = React.useState(false);
+ const { data: isGitHubProject } = useIsGitHubProjectQuery(key);
+ const { data: isGitLabProject } = useIsGitLabProjectQuery(key);
const { data: githubProvisioningStatus } = useGithubProvisioningEnabledQuery();
+ const { data: gitlabProvisioningStatus } = useGilabProvisioningEnabledQuery();
- const { component, isGitHubProject } = props;
- const { configuration } = component;
const provisionedByGitHub = isGitHubProject && !!githubProvisioningStatus;
- const canApplyPermissionTemplate =
- configuration?.canApplyPermissionTemplate && !provisionedByGitHub;
+ const provisionedByGitLab = isGitLabProject && !!gitlabProvisioningStatus;
+ const provisioned = provisionedByGitHub || provisionedByGitLab;
+ const canApplyPermissionTemplate = configuration?.canApplyPermissionTemplate && !provisioned;
const handleApplyTemplate = () => {
setApplyTemplateModal(true);
};
let description = translate('roles.page.description2');
- if (isPortfolioLike(component.qualifier)) {
+ if (isPortfolioLike(qualifier)) {
description = translate('roles.page.description_portfolio');
- } else if (isApplication(component.qualifier)) {
+ } else if (isApplication(qualifier)) {
description = translate('roles.page.description_application');
}
const visibilityDescription =
- isProject(component.qualifier) && component.visibility
- ? translate('visibility', component.visibility, 'description', component.qualifier)
+ isProject(qualifier) && visibility
+ ? translate('visibility', visibility, 'description', qualifier)
: undefined;
return (
<div>
<Title>
{translate('permissions.page')}
- {provisionedByGitHub && (
+ {provisioned && (
<Image
- alt="github"
+ alt={provisionedByGitHub ? 'github' : 'gitlab'}
className="sw-mx-2 sw-align-baseline"
- aria-label={translate('project_permission.github_managed')}
+ aria-label={translateWithParameters(
+ 'project_permission.managed',
+ provisionedByGitHub ? translate('alm.github') : translate('alm.gitlab'),
+ )}
height={16}
- src="/images/alm/github.svg"
+ src={`/images/alm/${provisionedByGitHub ? 'github' : 'gitlab'}.svg`}
/>
)}
</Title>
<div>
<p>{description}</p>
{isDefined(visibilityDescription) && <p>{visibilityDescription}</p>}
- {provisionedByGitHub && (
+ {provisioned && (
<>
- <p>{translate('roles.page.description.github')}</p>
+ <p>
+ {provisionedByGitHub
+ ? translate('roles.page.description.github')
+ : translate('roles.page.description.gitlab')}
+ </p>
<div className="sw-mt-2">
- <GitHubSynchronisationWarning short />
+ {provisionedByGitHub && <GitHubSynchronisationWarning short />}
</div>
</>
)}
{translate('project_permission.local_project_with_github_provisioning')}
</FlagMessage>
)}
+ {gitlabProvisioningStatus && !isGitLabProject && (
+ <FlagMessage variant="warning" className="sw-mt-2">
+ {translate('project_permission.local_project_with_gitlab_provisioning')}
+ </FlagMessage>
+ )}
</div>
</div>
{canApplyPermissionTemplate && (
{applyTemplateModal && (
<ApplyTemplate
- onApply={props.loadHolders}
+ onApply={loadHolders}
onClose={handleApplyTemplateClose}
project={component}
/>
import { Visibility } from '~sonar-aligned/types/component';
import * as api from '../../../../api/permissions';
import withComponentContext from '../../../../app/components/componentContext/withComponentContext';
-import VisibilitySelector from '../../../../components/common/VisibilitySelector';
import AllHoldersList from '../../../../components/permissions/AllHoldersList';
import { FilterOption } from '../../../../components/permissions/SearchForm';
-import UseQuery from '../../../../helpers/UseQuery';
import { translate } from '../../../../helpers/l10n';
import {
PERMISSIONS_ORDER_BY_QUALIFIER,
convertToPermissionDefinitions,
} from '../../../../helpers/permissions';
-import { useIsGitHubProjectQuery } from '../../../../queries/devops-integration';
-import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github';
import { ComponentContextShape } from '../../../../types/component';
import { Permissions } from '../../../../types/permissions';
import { Component, Paging, PermissionGroup, PermissionUser } from '../../../../types/types';
import '../../styles.css';
import PageHeader from './PageHeader';
+import PermissionsProjectVisibility from './PermissionsProjectVisibility';
import PublicProjectDisclaimer from './PublicProjectDisclaimer';
interface Props extends ComponentContextShape {
usersPaging,
groupsPaging,
} = this.state;
- const canTurnToPrivate =
- component.configuration && component.configuration.canUpdateProjectVisibilityToPrivate;
let order = PERMISSIONS_ORDER_BY_QUALIFIER[component.qualifier];
if (component.visibility === Visibility.Public) {
<PageContentFontWrapper className="sw-my-8 sw-body-sm">
<Helmet defer={false} title={translate('permissions.page')} />
- <UseQuery query={useIsGitHubProjectQuery} args={[component.key]}>
- {({ data: isGitHubProject }) => (
- <>
- <PageHeader
- component={component}
- isGitHubProject={isGitHubProject}
- loadHolders={this.loadHolders}
- />
- <div>
- <UseQuery query={useGithubProvisioningEnabledQuery}>
- {({ data: githubProvisioningStatus, isFetching }) => (
- <VisibilitySelector
- canTurnToPrivate={canTurnToPrivate}
- className="sw-flex sw-my-4"
- onChange={this.handleVisibilityChange}
- loading={loading || isFetching}
- disabled={isGitHubProject && !!githubProvisioningStatus}
- visibility={component.visibility}
- />
- )}
- </UseQuery>
-
- {disclaimer && (
- <PublicProjectDisclaimer
- component={component}
- onClose={this.handleCloseDisclaimer}
- onConfirm={this.handleTurnProjectToPublic}
- />
- )}
- </div>
- </>
+ <PageHeader component={component} loadHolders={this.loadHolders} />
+ <div>
+ <PermissionsProjectVisibility
+ component={component}
+ handleVisibilityChange={this.handleVisibilityChange}
+ isLoading={loading}
+ />
+
+ {disclaimer && (
+ <PublicProjectDisclaimer
+ component={component}
+ onClose={this.handleCloseDisclaimer}
+ onConfirm={this.handleTurnProjectToPublic}
+ />
)}
- </UseQuery>
+ </div>
<AllHoldersList
loading={loading}
--- /dev/null
+/*
+ * 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 * as React from 'react';
+import VisibilitySelector from '../../../../components/common/VisibilitySelector';
+import {
+ useIsGitHubProjectQuery,
+ useIsGitLabProjectQuery,
+} from '../../../../queries/devops-integration';
+import { useGithubProvisioningEnabledQuery } from '../../../../queries/identity-provider/github';
+import { useGilabProvisioningEnabledQuery } from '../../../../queries/identity-provider/gitlab';
+import { Component } from '../../../../types/types';
+
+interface Props {
+ component: Component;
+ handleVisibilityChange: (visibility: string) => void;
+ isLoading: boolean;
+}
+
+export default function PermissionsProjectVisibility(props: Readonly<Props>) {
+ const { component, handleVisibilityChange, isLoading } = props;
+ const canTurnToPrivate = component.configuration?.canUpdateProjectVisibilityToPrivate;
+
+ const { data: isGitHubProject } = useIsGitHubProjectQuery(component.key);
+ const { data: isGitLabProject } = useIsGitLabProjectQuery(component.key);
+ const { data: gitHubProvisioningStatus, isFetching: isFetchingGitHubProvisioningStatus } =
+ useGithubProvisioningEnabledQuery();
+ const { data: gitLabProvisioningStatus, isFetching: isFetchingGitLabProvisioningStatus } =
+ useGilabProvisioningEnabledQuery();
+ const isFetching = isFetchingGitHubProvisioningStatus || isFetchingGitLabProvisioningStatus;
+ const isDisabled =
+ (isGitHubProject && !!gitHubProvisioningStatus) ||
+ (isGitLabProject && !!gitLabProvisioningStatus);
+
+ return (
+ <VisibilitySelector
+ canTurnToPrivate={canTurnToPrivate}
+ className="sw-flex sw-my-4"
+ onChange={handleVisibilityChange}
+ loading={isLoading || isFetching}
+ disabled={isDisabled}
+ visibility={component.visibility}
+ />
+ );
+}
import AlmSettingsServiceMock from '../../../../../api/mocks/AlmSettingsServiceMock';
import DopTranslationServiceMock from '../../../../../api/mocks/DopTranslationServiceMock';
import GithubProvisioningServiceMock from '../../../../../api/mocks/GithubProvisioningServiceMock';
+import GitlabProvisioningServiceMock from '../../../../../api/mocks/GitlabProvisioningServiceMock';
import PermissionsServiceMock from '../../../../../api/mocks/PermissionsServiceMock';
import SystemServiceMock from '../../../../../api/mocks/SystemServiceMock';
+import { mockGitlabConfiguration } from '../../../../../helpers/mocks/alm-integrations';
import { mockComponent } from '../../../../../helpers/mocks/component';
import { mockGitHubConfiguration } from '../../../../../helpers/mocks/dop-translation';
import { mockPermissionGroup, mockPermissionUser } from '../../../../../helpers/mocks/permissions';
let serviceMock: PermissionsServiceMock;
let dopTranslationHandler: DopTranslationServiceMock;
let githubHandler: GithubProvisioningServiceMock;
+let gitlabHandler: GitlabProvisioningServiceMock;
let almHandler: AlmSettingsServiceMock;
let systemHandler: SystemServiceMock;
beforeAll(() => {
serviceMock = new PermissionsServiceMock();
dopTranslationHandler = new DopTranslationServiceMock();
githubHandler = new GithubProvisioningServiceMock(dopTranslationHandler);
+ gitlabHandler = new GitlabProvisioningServiceMock();
almHandler = new AlmSettingsServiceMock();
systemHandler = new SystemServiceMock();
});
serviceMock.reset();
dopTranslationHandler.reset();
githubHandler.reset();
+ gitlabHandler.reset();
almHandler.reset();
});
expect(ui.pageTitle.get()).toBeInTheDocument();
await waitFor(() =>
- expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.github_managed/),
+ expect(ui.pageTitle.get()).toHaveAccessibleName(/project_permission.managed/),
);
expect(ui.pageTitle.byRole('img').get()).toBeInTheDocument();
expect(ui.githubExplanations.get()).toBeInTheDocument();
});
});
+describe('GL provisioning', () => {
+ beforeEach(() => {
+ systemHandler.setProvider(Provider.Gitlab);
+ });
+
+ it('should not allow to change visibility for GitLab Project with auto-provisioning', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ dopTranslationHandler.gitHubConfigurations.push(
+ mockGitHubConfiguration({ provisioningType: ProvisioningType.jit }),
+ );
+ gitlabHandler.setGitlabConfigurations([
+ mockGitlabConfiguration({ id: '1', enabled: true, provisioningType: ProvisioningType.auto }),
+ ]);
+ almHandler.handleSetProjectBinding(AlmKeys.GitLab, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+
+ renderPermissionsProjectApp({}, { featureList: [Feature.GitlabProvisioning] });
+ await ui.appLoaded();
+
+ expect(ui.visibilityRadio(Visibility.Public).get()).toBeDisabled();
+ expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
+ expect(ui.visibilityRadio(Visibility.Private).get()).toBeDisabled();
+ await ui.turnProjectPrivate();
+ expect(ui.visibilityRadio(Visibility.Private).get()).not.toBeChecked();
+ });
+
+ it('should allow to change visibility for non-GitLab Project', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ gitlabHandler.setGitlabConfigurations([
+ mockGitlabConfiguration({ id: '1', enabled: true, provisioningType: ProvisioningType.auto }),
+ ]);
+ almHandler.handleSetProjectBinding(AlmKeys.GitHub, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+ renderPermissionsProjectApp({}, { featureList: [Feature.GitlabProvisioning] });
+ await ui.appLoaded();
+
+ expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
+ expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
+ expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled');
+ await ui.turnProjectPrivate();
+ expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
+ });
+
+ it('should allow to change visibility for GitLab Project with disabled auto-provisioning', async () => {
+ const user = userEvent.setup();
+ const ui = getPageObject(user);
+ gitlabHandler.setGitlabConfigurations([
+ mockGitlabConfiguration({ id: '1', enabled: true, provisioningType: ProvisioningType.jit }),
+ ]);
+ almHandler.handleSetProjectBinding(AlmKeys.GitLab, {
+ almSetting: 'test',
+ repository: 'test',
+ monorepo: false,
+ project: 'my-project',
+ });
+ renderPermissionsProjectApp({}, { featureList: [Feature.GitlabProvisioning] });
+ await ui.appLoaded();
+
+ expect(ui.visibilityRadio(Visibility.Public).get()).not.toHaveClass('disabled');
+ expect(ui.visibilityRadio(Visibility.Public).get()).toBeChecked();
+ expect(ui.visibilityRadio(Visibility.Private).get()).not.toHaveClass('disabled');
+ await ui.turnProjectPrivate();
+ expect(ui.visibilityRadio(Visibility.Private).get()).toBeChecked();
+ });
+});
+
function renderPermissionsProjectApp(
override: Partial<Component> = {},
contextOverride: Partial<RenderContext> = {},
}
export default function VisibilitySelector(props: Readonly<VisibilitySelectorProps>) {
- const { className, canTurnToPrivate, visibility, disabled, loading = false } = props;
+ const { className, canTurnToPrivate, visibility, disabled, loading = false, onChange } = props;
return (
<div className={classNames(className)}>
<RadioButtonGroup
isDisabled: (v === Visibility.Private && !canTurnToPrivate) || loading,
}))}
value={visibility}
- onChange={props.onChange}
+ onChange={onChange}
/>
</div>
);
});
}
+export function useIsGitLabProjectQuery(project?: string) {
+ return useProjectBindingQuery<boolean>(project, {
+ select: (data) => data?.alm === AlmKeys.GitLab,
+ });
+}
+
export function useDeleteProjectAlmBindingMutation(project?: string) {
const keyFromUrl = useProjectKeyFromLocation();
const client = useQueryClient();
export function useGithubProvisioningEnabledQuery() {
const res = useGitHubSyncStatusQuery({ noRefetch: true });
- return mapReactQueryResult(res, (data) => data.enabled);
+ return mapReactQueryResult(res, (data) => {
+ return data.enabled;
+ });
}
export function useSyncWithGitHubNow() {
updateGitLabConfiguration,
} from '../../api/gitlab-provisioning';
import { translate } from '../../helpers/l10n';
+import { mapReactQueryResult } from '../../helpers/react-query';
import { AlmSyncStatus, ProvisioningType } from '../../types/provisioning';
import { TaskStatuses, TaskTypes } from '../../types/tasks';
});
}
+export function useGilabProvisioningEnabledQuery() {
+ const res = useGitLabConfigurationsQuery();
+
+ return mapReactQueryResult(res, (data) =>
+ data.gitlabConfigurations?.some(
+ (configuration) =>
+ configuration.enabled && configuration.provisioningType === ProvisioningType.auto,
+ ),
+ );
+}
+
export function useGitLabSyncStatusQuery() {
const getLastSync = async () => {
const lastSyncTasks = await getActivity({
roles.page.description_portfolio=Grant and revoke portfolio-level permissions. Permissions can be granted to groups or individual users.
roles.page.description_application=Grant and revoke application-level permissions. Permissions can be granted to groups or individual users.
roles.page.description.github=Project permissions are read-only for users provisioned from GitHub. For non-GitHub users, permissions can only be removed.
+roles.page.description.gitlab=Project permissions are read-only for users provisioned from GitLab. For non-GitLab users, permissions can only be removed.
roles.page.change_visibility=Change project visibility
-project_permission.github_managed=Provisioned from GitHub
+project_permission.managed=Provisioned from {0}
project_permission.local_project_with_github_provisioning=Please note that this project is not linked to GitHub. Bind it to GitHub to benefit from permission provisioning.
+project_permission.local_project_with_gitlab_provisioning=Please note that this project is not linked to GitLab. Bind it to GitLab to benefit from permission provisioning.
project_permission.remove_only_confirmation=Are you sure you want to remove the permission {permission} from {holder}? The permission can not be added back.
project_permission.remove_only_confirmation_title=Remove permission
project_settings.page=General Settings