Skip to content
Snippets Groups Projects
Commit 6c6d93d9 authored by Briley  Sandlin's avatar Briley Sandlin :red_circle:
Browse files

Merge branch '407249-mark-project-as-ci-resource' into 'master'

Mark project as CI resource

See merge request !120145



Merged-by: default avatarBriley Sandlin <bsandlin@gitlab.com>
Approved-by: default avatarMireya Andres <mandres@gitlab.com>
Reviewed-by: default avatarMarcel Amirault <mamirault@gitlab.com>
Reviewed-by: Frédéric Caplette's avatarFrédéric Caplette <fcaplette@gitlab.com>
Reviewed-by: default avatarMireya Andres <mandres@gitlab.com>
Co-authored-by: default avatarAvielle Wolfe <awolfe@gitlab.com>
parents 1696e4e0 3478ade3
No related branches found
No related tags found
No related merge requests found
Pipeline #887417328 failed
Pipeline: GitLab

#887442741

    Pipeline: E2E GDK

    #887439215

      Pipeline: GitLab

      #887431799

        Showing
        with 491 additions and 12 deletions
        <script>
        import { GlBadge, GlLink, GlLoadingIcon, GlModal, GlSprintf, GlToggle } from '@gitlab/ui';
        import { createAlert, VARIANT_INFO } from '~/alert';
        import { __, s__ } from '~/locale';
        import { helpPagePath } from '~/helpers/help_page_helper';
        import getCiCatalogSettingsQuery from '../graphql/queries/get_ci_catalog_settings.query.graphql';
        import catalogResourcesCreate from '../graphql/mutations/catalog_resources_create.mutation.graphql';
        export const i18n = {
        badgeText: __('Experiment'),
        catalogResourceQueryError: s__(
        'CiCatalog|There was a problem fetching the CI/CD Catalog setting.',
        ),
        catalogResourceMutationError: s__(
        'CiCatalog|There was a problem marking the project as a CI/CD Catalog resource.',
        ),
        catalogResourceMutationSuccess: s__('CiCatalog|This project is now a CI/CD Catalog resource.'),
        ciCatalogLabel: s__('CiCatalog|CI/CD Catalog resource'),
        ciCatalogHelpText: s__(
        'CiCatalog|Mark project as a CI/CD Catalog resource. %{linkStart}What is the CI/CD Catalog?%{linkEnd}',
        ),
        modal: {
        actionPrimary: {
        text: s__('CiCatalog|Mark project as a CI/CD Catalog resource'),
        },
        actionCancel: {
        text: __('Cancel'),
        },
        body: s__(
        'CiCatalog|This project will be marked as a CI/CD Catalog resource and will be visible in the CI/CD Catalog. This action is not reversible.',
        ),
        title: s__('CiCatalog|Mark project as a CI/CD Catalog resource'),
        },
        readMeHelpText: s__(
        'CiCatalog|The project must contain a README.md file and a template.yml file. When enabled, the repository is available in the CI/CD Catalog.',
        ),
        };
        export const ciCatalogHelpPath = helpPagePath('ci/components/index', {
        anchor: 'components-catalog',
        });
        export default {
        i18n,
        components: {
        GlBadge,
        GlLink,
        GlLoadingIcon,
        GlModal,
        GlSprintf,
        GlToggle,
        },
        props: {
        fullPath: {
        type: String,
        required: true,
        },
        },
        data() {
        return {
        ciCatalogHelpPath,
        isCatalogResource: false,
        showCatalogResourceModal: false,
        };
        },
        apollo: {
        isCatalogResource: {
        query: getCiCatalogSettingsQuery,
        variables() {
        return {
        fullPath: this.fullPath,
        };
        },
        update({ project }) {
        return project?.isCatalogResource || false;
        },
        error() {
        createAlert({ message: this.$options.i18n.catalogResourceQueryError });
        },
        },
        },
        computed: {
        isLoading() {
        return this.$apollo.queries.isCatalogResource.loading;
        },
        },
        methods: {
        async markProjectAsCatalogResource() {
        try {
        const {
        data: {
        catalogResourcesCreate: { errors },
        },
        } = await this.$apollo.mutate({
        mutation: catalogResourcesCreate,
        variables: { input: { projectPath: this.fullPath } },
        });
        if (errors.length) {
        throw new Error(errors[0]);
        }
        this.isCatalogResource = true;
        createAlert({
        message: this.$options.i18n.catalogResourceMutationSuccess,
        variant: VARIANT_INFO,
        });
        } catch (error) {
        const message = error.message || this.$options.i18n.catalogResourceMutationError;
        createAlert({ message });
        }
        },
        onCatalogResourceEnabledToggled() {
        this.showCatalogResourceModal = true;
        },
        onModalCanceled() {
        this.showCatalogResourceModal = false;
        },
        },
        };
        </script>
        <template>
        <div>
        <gl-loading-icon v-if="isLoading" />
        <div v-else data-testid="ci-catalog-settings">
        <div>
        <label class="gl-mb-1 gl-mr-2">
        {{ $options.i18n.ciCatalogLabel }}
        </label>
        <gl-badge size="sm" variant="info"> {{ $options.i18n.badgeText }} </gl-badge>
        </div>
        <gl-sprintf :message="$options.i18n.ciCatalogHelpText">
        <template #link="{ content }">
        <gl-link :href="ciCatalogHelpPath" target="_blank">{{ content }}</gl-link>
        </template>
        </gl-sprintf>
        <gl-toggle
        class="gl-my-2"
        :disabled="isCatalogResource"
        :value="isCatalogResource"
        :label="$options.i18n.ciCatalogLabel"
        label-position="hidden"
        name="ci_resource_enabled"
        @change="onCatalogResourceEnabledToggled"
        />
        <div class="gl-text-secondary">
        {{ $options.i18n.readMeHelpText }}
        </div>
        <gl-modal
        :visible="showCatalogResourceModal"
        modal-id="mark-as-catalog-resource"
        size="sm"
        :title="$options.i18n.modal.title"
        :action-cancel="$options.i18n.modal.actionCancel"
        :action-primary="$options.i18n.modal.actionPrimary"
        @canceled="onModalCanceled"
        @primary="markProjectAsCatalogResource"
        >
        {{ $options.i18n.modal.body }}
        </gl-modal>
        </div>
        </div>
        </template>
        ......@@ -19,6 +19,7 @@ import {
        import { toggleHiddenClassBySelector } from '../external';
        import ProjectFeatureSetting from './project_feature_setting.vue';
        import ProjectSettingRow from './project_setting_row.vue';
        import CiCatalogSettings from './ci_catalog_settings.vue';
        const FEATURE_ACCESS_LEVEL_ANONYMOUS = [30, s__('ProjectSettings|Everyone')];
        ......@@ -33,6 +34,7 @@ export default {
        ...CVE_ID_REQUEST_BUTTON_I18N,
        analyticsLabel: s__('ProjectSettings|Analytics'),
        containerRegistryLabel: s__('ProjectSettings|Container registry'),
        ciCdLabel: __('CI/CD'),
        forksLabel: s__('ProjectSettings|Forks'),
        issuesLabel: s__('ProjectSettings|Issues'),
        lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'),
        ......@@ -57,7 +59,6 @@ export default {
        'ProjectSettings|Allow anyone to pull from Package Registry',
        ),
        pagesLabel: s__('ProjectSettings|Pages'),
        ciCdLabel: __('CI/CD'),
        repositoryLabel: s__('ProjectSettings|Repository'),
        requirementsLabel: s__('ProjectSettings|Requirements'),
        releasesLabel: s__('ProjectSettings|Releases'),
        ......@@ -78,6 +79,7 @@ export default {
        VISIBILITY_LEVEL_PUBLIC_INTEGER,
        components: {
        CiCatalogSettings,
        ProjectFeatureSetting,
        ProjectSettingRow,
        GlButton,
        ......@@ -100,6 +102,11 @@ export default {
        required: false,
        default: false,
        },
        canAddCatalogResource: {
        type: Boolean,
        required: false,
        default: false,
        },
        currentSettings: {
        type: Object,
        required: true,
        ......@@ -358,6 +365,9 @@ export default {
        packageRegistryApiForEveryoneEnabledShown() {
        return this.visibilityLevel !== VISIBILITY_LEVEL_PUBLIC_INTEGER;
        },
        monitorOperationsFeatureAccessLevelOptions() {
        return this.featureAccessLevelOptions.filter(([value]) => value <= this.monitorAccessLevel);
        },
        },
        watch: {
        ......@@ -959,6 +969,11 @@ export default {
        />
        </project-setting-row>
        </div>
        <ci-catalog-settings
        v-if="canAddCatalogResource"
        class="gl-mb-5"
        :full-path="confirmationPhrase"
        />
        <project-setting-row v-if="canDisableEmails" ref="email-settings" class="mb-3">
        <label class="js-emails-disabled">
        <input :value="emailsDisabled" type="hidden" name="project[emails_disabled]" />
        ......
        mutation catalogResourcesCreate($input: CatalogResourcesCreateInput!) {
        catalogResourcesCreate(input: $input) {
        errors
        }
        }
        query getCiCatalogSettings($fullPath: ID!) {
        project(fullPath: $fullPath) {
        id
        isCatalogResource
        }
        }
        import Vue from 'vue';
        import VueApollo from 'vue-apollo';
        import createDefaultClient from '~/lib/graphql';
        import { parseBoolean } from '~/lib/utils/common_utils';
        import settingsPanel from './components/settings_panel.vue';
        Vue.use(VueApollo);
        export default function initProjectPermissionsSettings() {
        const apolloProvider = new VueApollo({
        defaultClient: createDefaultClient(),
        });
        const mountPoint = document.querySelector('.js-project-permissions-form');
        const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
        const componentProps = JSON.parse(componentPropsEl.innerHTML);
        ......@@ -19,6 +28,8 @@ export default function initProjectPermissionsSettings() {
        return new Vue({
        el: mountPoint,
        name: 'ProjectPermissionsRoot',
        apolloProvider,
        provide: {
        additionalInformation,
        confirmDangerMessage,
        ......
        ......@@ -41,6 +41,7 @@ class ProjectsController < Projects::ApplicationController
        push_frontend_feature_flag(:synchronize_fork, @project&.fork_source)
        push_frontend_feature_flag(:remove_monitor_metrics, @project)
        push_frontend_feature_flag(:explain_code_chat, current_user)
        push_frontend_feature_flag(:ci_namespace_catalog_experimental, @project)
        push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
        push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies)
        push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
        ......
        ......@@ -3,6 +3,10 @@
        module Ci
        module Catalog
        module ResourcesHelper
        def can_add_catalog_resource?(_project)
        false
        end
        def can_view_namespace_catalog?(_project)
        false
        end
        ......
        ......@@ -421,8 +421,9 @@ def project_permissions_panel_data(project)
        packagesAvailable: ::Gitlab.config.packages.enabled,
        packagesHelpPath: help_page_path('user/packages/index'),
        currentSettings: project_permissions_settings(project),
        canDisableEmails: can_disable_emails?(project, current_user),
        canAddCatalogResource: can_add_catalog_resource?(project),
        canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
        canDisableEmails: can_disable_emails?(project, current_user),
        allowedVisibilityOptions: project_allowed_visibility_levels(project),
        visibilityHelpPath: help_page_path('user/public_access'),
        registryAvailable: Gitlab.config.registry.enabled,
        ......
        ......@@ -190,13 +190,12 @@ After components are added to a components repository, they can immediately be [
        However, this repository is not discoverable. You must mark this project as a catalog resource to allow it to be visible in the CI Catalog
        so other users can discover it.
        To mark a project as a catalog resource, run the following [graphQL](../../api/graphql/index.md)
        mutation:
        ```graphql
        mutation {
        catalogResourcesCreate(input: { projectPath: "path-to-project"}) {
        errors
        }
        }
        ```
        To mark a project as a catalog resource:
        1. On the top bar, select **Main menu > Projects** and find your project.
        1. On the left sidebar, select **Settings > General**.
        1. Expand **Visibility, project features, permissions**.
        1. Scroll down to **CI/CD Catalog resource** and select the toggle to mark the project as a catalog resource.
        NOTE:
        This action is not reversible.
        ......@@ -6,6 +6,11 @@ module Catalog
        module ResourcesHelper
        extend ::Gitlab::Utils::Override
        override :can_add_catalog_resource?
        def can_add_catalog_resource?(project)
        can?(current_user, :add_catalog_resource, project)
        end
        override :can_view_namespace_catalog?
        def can_view_namespace_catalog?(project)
        can?(current_user, :read_namespace_catalog, project)
        ......
        ......@@ -12,6 +12,61 @@
        allow(helper).to receive(:current_user).and_return(user)
        end
        describe '#can_add_catalog_resource?' do
        subject { helper.can_add_catalog_resource?(project) }
        context 'when FF `ci_namespace_catalog_experimental` is disabled' do
        before do
        stub_feature_flags(ci_namespace_catalog_experimental: false)
        stub_licensed_features(ci_namespace_catalog: true)
        project.add_owner(user)
        end
        it 'returns false' do
        expect(subject).to be false
        end
        end
        context 'when user is not an owner' do
        before do
        stub_licensed_features(ci_namespace_catalog: true)
        project.add_maintainer(user)
        end
        it 'returns false' do
        expect(subject).to be false
        end
        end
        context 'when user is an owner' do
        before do
        project.add_owner(user)
        end
        context 'when license for namespace catalog is enabled' do
        before do
        stub_licensed_features(ci_namespace_catalog: true)
        end
        it 'returns true' do
        expect(subject).to be true
        end
        end
        context 'when license for namespace catalog is not enabled' do
        before do
        stub_licensed_features(ci_namespace_catalog: false)
        end
        it 'returns false' do
        expect(subject).to be false
        end
        end
        end
        end
        describe '#can_view_namespace_catalog?' do
        subject { helper.can_view_namespace_catalog?(project) }
        ......
        ......@@ -9583,6 +9583,9 @@ msgstr ""
        msgid "Ci config already present"
        msgstr ""
         
        msgid "CiCatalog|CI/CD Catalog resource"
        msgstr ""
        msgid "CiCatalog|CI/CD catalog"
        msgstr ""
         
        ......@@ -9595,6 +9598,12 @@ msgstr ""
        msgid "CiCatalog|Learn more"
        msgstr ""
         
        msgid "CiCatalog|Mark project as a CI/CD Catalog resource"
        msgstr ""
        msgid "CiCatalog|Mark project as a CI/CD Catalog resource. %{linkStart}What is the CI/CD Catalog?%{linkEnd}"
        msgstr ""
        msgid "CiCatalog|Page %{currentPage} of %{totalPage}"
        msgstr ""
         
        ......@@ -9604,9 +9613,24 @@ msgstr ""
        msgid "CiCatalog|Repositories of pipeline components available in this namespace."
        msgstr ""
         
        msgid "CiCatalog|The project must contain a README.md file and a template.yml file. When enabled, the repository is available in the CI/CD Catalog."
        msgstr ""
        msgid "CiCatalog|There was a problem fetching the CI/CD Catalog setting."
        msgstr ""
        msgid "CiCatalog|There was a problem marking the project as a CI/CD Catalog resource."
        msgstr ""
        msgid "CiCatalog|There was an error fetching CI/CD Catalog resources."
        msgstr ""
         
        msgid "CiCatalog|This project is now a CI/CD Catalog resource."
        msgstr ""
        msgid "CiCatalog|This project will be marked as a CI/CD Catalog resource and will be visible in the CI/CD Catalog. This action is not reversible."
        msgstr ""
        msgid "CiCatalog|We want to help you create and manage pipeline component repositories, while also making it easier to reuse pipeline configurations. Let us know how we're doing!"
        msgstr ""
         
        import Vue from 'vue';
        import VueApollo from 'vue-apollo';
        import { GlBadge, GlLoadingIcon, GlModal, GlSprintf, GlToggle } from '@gitlab/ui';
        import { createAlert } from '~/alert';
        import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
        import waitForPromises from 'helpers/wait_for_promises';
        import createMockApollo from 'helpers/mock_apollo_helper';
        import catalogResourcesCreate from '~/pages/projects/shared/permissions/graphql/mutations/catalog_resources_create.mutation.graphql';
        import getCiCatalogSettingsQuery from '~/pages/projects/shared/permissions/graphql/queries/get_ci_catalog_settings.query.graphql';
        import CiCatalogSettings, {
        i18n,
        } from '~/pages/projects/shared/permissions/components/ci_catalog_settings.vue';
        import { mockCiCatalogSettingsResponse } from './mock_data';
        Vue.use(VueApollo);
        jest.mock('~/alert');
        describe('CiCatalogSettings', () => {
        let wrapper;
        let ciCatalogSettingsResponse;
        let catalogResourcesCreateResponse;
        const fullPath = 'gitlab-org/gitlab';
        const createComponent = ({ ciCatalogSettingsHandler = ciCatalogSettingsResponse } = {}) => {
        const handlers = [
        [getCiCatalogSettingsQuery, ciCatalogSettingsHandler],
        [catalogResourcesCreate, catalogResourcesCreateResponse],
        ];
        const mockApollo = createMockApollo(handlers);
        wrapper = shallowMountExtended(CiCatalogSettings, {
        propsData: {
        fullPath,
        },
        stubs: {
        GlSprintf,
        },
        apolloProvider: mockApollo,
        });
        return waitForPromises();
        };
        const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
        const findBadge = () => wrapper.findComponent(GlBadge);
        const findModal = () => wrapper.findComponent(GlModal);
        const findToggle = () => wrapper.findComponent(GlToggle);
        const findCiCatalogSettings = () => wrapper.findByTestId('ci-catalog-settings');
        beforeEach(() => {
        ciCatalogSettingsResponse = jest.fn().mockResolvedValue(mockCiCatalogSettingsResponse);
        catalogResourcesCreateResponse = jest.fn();
        });
        describe('when initial queries are loading', () => {
        beforeEach(() => {
        createComponent();
        });
        it('shows a loading icon and no CI catalog settings', () => {
        expect(findLoadingIcon().exists()).toBe(true);
        expect(findCiCatalogSettings().exists()).toBe(false);
        });
        });
        describe('when queries have loaded', () => {
        beforeEach(async () => {
        await createComponent();
        });
        it('does not show a loading icon', () => {
        expect(findLoadingIcon().exists()).toBe(false);
        });
        it('renders the CI Catalog settings', () => {
        expect(findCiCatalogSettings().exists()).toBe(true);
        });
        it('renders the experiment badge', () => {
        expect(findBadge().exists()).toBe(true);
        });
        it('renders the toggle', () => {
        expect(findToggle().exists()).toBe(true);
        });
        it('renders the modal', () => {
        expect(findModal().exists()).toBe(true);
        expect(findModal().attributes('title')).toBe(i18n.modal.title);
        });
        describe('when queries have loaded', () => {
        beforeEach(() => {
        catalogResourcesCreateResponse.mockResolvedValue(mockCiCatalogSettingsResponse);
        });
        it('shows the modal when the toggle is clicked', async () => {
        expect(findModal().props('visible')).toBe(false);
        await findToggle().vm.$emit('change', true);
        expect(findModal().props('visible')).toBe(true);
        expect(findModal().props('actionPrimary').text).toBe(i18n.modal.actionPrimary.text);
        });
        it('hides the modal when cancel is clicked', () => {
        findToggle().vm.$emit('change', true);
        findModal().vm.$emit('canceled');
        expect(findModal().props('visible')).toBe(false);
        expect(catalogResourcesCreateResponse).not.toHaveBeenCalled();
        });
        it('calls the mutation with the correct input from the modal click', async () => {
        expect(catalogResourcesCreateResponse).toHaveBeenCalledTimes(0);
        findToggle().vm.$emit('change', true);
        findModal().vm.$emit('primary');
        await waitForPromises();
        expect(catalogResourcesCreateResponse).toHaveBeenCalledTimes(1);
        expect(catalogResourcesCreateResponse).toHaveBeenCalledWith({
        input: {
        projectPath: fullPath,
        },
        });
        });
        });
        });
        describe('when the query is unsuccessful', () => {
        const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
        it('throws an error', async () => {
        await createComponent({ ciCatalogSettingsHandler: failedHandler });
        await waitForPromises();
        expect(createAlert).toHaveBeenCalledWith({ message: i18n.catalogResourceQueryError });
        });
        });
        });
        export const mockCiCatalogSettingsResponse = {
        data: {
        catalogResourcesCreate: {
        errors: [],
        },
        },
        };
        import { GlSprintf, GlToggle } from '@gitlab/ui';
        import { shallowMount, mount } from '@vue/test-utils';
        import ProjectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
        import CiCatalogSettings from '~/pages/projects/shared/permissions/components/ci_catalog_settings.vue';
        import settingsPanel from '~/pages/projects/shared/permissions/components/settings_panel.vue';
        import {
        featureAccessLevel,
        ......@@ -34,6 +35,7 @@ const defaultProps = {
        warnAboutPotentiallyUnwantedCharacters: true,
        },
        isGitlabCom: true,
        canAddCatalogResource: false,
        canDisableEmails: true,
        canChangeVisibilityLevel: true,
        allowedVisibilityOptions: [0, 10, 20],
        ......@@ -118,6 +120,7 @@ describe('Settings Panel', () => {
        const findPagesSettings = () => wrapper.findComponent({ ref: 'pages-settings' });
        const findPagesAccessLevels = () =>
        wrapper.find('[name="project[project_feature_attributes][pages_access_level]"]');
        const findCiCatalogSettings = () => wrapper.findComponent(CiCatalogSettings);
        const findEmailSettings = () => wrapper.findComponent({ ref: 'email-settings' });
        const findShowDefaultAwardEmojis = () =>
        wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]');
        ......@@ -645,6 +648,19 @@ describe('Settings Panel', () => {
        });
        });
        describe('CI Catalog Settings', () => {
        it('should show the CI Catalog settings if user has permission', () => {
        wrapper = mountComponent({ canAddCatalogResource: true });
        expect(findCiCatalogSettings().exists()).toBe(true);
        });
        it('should not show the CI Catalog settings if user does not have permission', () => {
        wrapper = mountComponent();
        expect(findCiCatalogSettings().exists()).toBe(false);
        });
        });
        describe('Email notifications', () => {
        it('should show the disable email notifications input if emails an be disabled', () => {
        wrapper = mountComponent({ canDisableEmails: true });
        ......
        ......@@ -7,6 +7,18 @@
        let_it_be(:project) { build(:project) }
        describe '#can_add_catalog_resource?' do
        subject { helper.can_add_catalog_resource?(project) }
        before do
        stub_licensed_features(ci_namespace_catalog: false)
        end
        it 'user cannot add a catalog resource in CE regardless of permissions' do
        expect(subject).to be false
        end
        end
        describe '#can_view_namespace_catalog?' do
        subject { helper.can_view_namespace_catalog?(project) }
        ......
        ......@@ -1051,6 +1051,12 @@ def license_name
        it 'includes membersPagePath' do
        expect(subject).to include(membersPagePath: project_project_members_path(project))
        end
        it 'includes canAddCatalogResource' do
        allow(helper).to receive(:can?) { false }
        expect(subject).to include(canAddCatalogResource: false)
        end
        end
        describe '#project_classes' do
        ......
        0% Loading or .
        You are about to add 0 people to the discussion. Proceed with caution.
        Finish editing this message first!
        Please register or to comment