diff options
Diffstat (limited to 'app')
54 files changed, 580 insertions, 164 deletions
diff --git a/app/assets/javascripts/api/groups_api.js b/app/assets/javascripts/api/groups_api.js index d4ba46656e6..d6c9e1d42cc 100644 --- a/app/assets/javascripts/api/groups_api.js +++ b/app/assets/javascripts/api/groups_api.js @@ -3,9 +3,9 @@ import { buildApiUrl } from './api_utils'; import { DEFAULT_PER_PAGE } from './constants'; const GROUPS_PATH = '/api/:version/groups.json'; +const DESCENDANT_GROUPS_PATH = '/api/:version/groups/:id/descendant_groups'; -export function getGroups(query, options, callback = () => {}) { - const url = buildApiUrl(GROUPS_PATH); +const axiosGet = (url, query, options, callback) => { return axios .get(url, { params: { @@ -19,4 +19,14 @@ export function getGroups(query, options, callback = () => {}) { return data; }); +}; + +export function getGroups(query, options, callback = () => {}) { + const url = buildApiUrl(GROUPS_PATH); + return axiosGet(url, query, options, callback); +} + +export function getDescendentGroups(parentGroupId, query, options, callback = () => {}) { + const url = buildApiUrl(DESCENDANT_GROUPS_PATH.replace(':id', parentGroupId)); + return axiosGet(url, query, options, callback); } diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue index f83927ea4e7..16a8a9d253f 100644 --- a/app/assets/javascripts/boards/components/board_content_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue @@ -11,6 +11,7 @@ import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assig import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { headerHeight: `${contentTop()}px`, @@ -26,7 +27,10 @@ export default { SidebarDropdownWidget, BoardSidebarWeightInput: () => import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'), + IterationSidebarDropdownWidget: () => + import('ee_component/sidebar/components/iteration_sidebar_dropdown_widget.vue'), }, + mixins: [glFeatureFlagMixin()], inject: { multipleAssigneesFeatureAvailable: { default: false, @@ -103,17 +107,31 @@ export default { :issuable-type="issuableType" data-testid="sidebar-milestones" /> - <sidebar-dropdown-widget - v-if="iterationFeatureAvailable" - :iid="activeBoardItem.iid" - issuable-attribute="iteration" - :workspace-path="projectPathForActiveIssue" - :attr-workspace-path="groupPathForActiveIssue" - :issuable-type="issuableType" - class="gl-mt-5" - data-testid="iteration-edit" - data-qa-selector="iteration_container" - /> + <template v-if="!glFeatures.iterationCadences"> + <sidebar-dropdown-widget + v-if="iterationFeatureAvailable" + :iid="activeBoardItem.iid" + issuable-attribute="iteration" + :workspace-path="projectPathForActiveIssue" + :attr-workspace-path="groupPathForActiveIssue" + :issuable-type="issuableType" + class="gl-mt-5" + data-testid="iteration-edit" + data-qa-selector="iteration_container" + /> + </template> + <template v-else> + <iteration-sidebar-dropdown-widget + v-if="iterationFeatureAvailable" + :iid="activeBoardItem.iid" + :workspace-path="projectPathForActiveIssue" + :attr-workspace-path="groupPathForActiveIssue" + :issuable-type="issuableType" + class="gl-mt-5" + data-testid="iteration-edit" + data-qa-selector="iteration_container" + /> + </template> </div> <board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" /> <sidebar-date-widget diff --git a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue b/app/assets/javascripts/content_editor/components/toolbar_link_button.vue index 369eadceff9..f706080eaa1 100644 --- a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue +++ b/app/assets/javascripts/content_editor/components/toolbar_link_button.vue @@ -6,6 +6,7 @@ import { GlFormInputGroup, GlDropdownDivider, GlDropdownItem, + GlTooltipDirective as GlTooltip, } from '@gitlab/ui'; import { Editor as TiptapEditor } from '@tiptap/vue-2'; import { hasSelection } from '../services/utils'; @@ -21,6 +22,9 @@ export default { GlDropdownItem, GlButton, }, + directives: { + GlTooltip, + }, props: { tiptapEditor: { type: TiptapEditor, @@ -68,6 +72,9 @@ export default { </script> <template> <gl-dropdown + v-gl-tooltip + :aria-label="__('Insert link')" + :title="__('Insert link')" :toggle-class="{ active: isActive }" size="small" category="tertiary" diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue index 4da230cbde6..07fdd3147e2 100644 --- a/app/assets/javascripts/content_editor/components/top_toolbar.vue +++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue @@ -72,7 +72,11 @@ export default { :tiptap-editor="contentEditor.tiptapEditor" @execute="trackToolbarControlExecution" /> - <toolbar-link-button :tiptap-editor="contentEditor.tiptapEditor" /> + <toolbar-link-button + data-testid="link" + :tiptap-editor="contentEditor.tiptapEditor" + @execute="trackToolbarControlExecution" + /> <divider /> <toolbar-button data-testid="blockquote" diff --git a/app/assets/javascripts/invite_members/components/group_select.vue b/app/assets/javascripts/invite_members/components/group_select.vue index bac8914c374..2d1e57a1177 100644 --- a/app/assets/javascripts/invite_members/components/group_select.vue +++ b/app/assets/javascripts/invite_members/components/group_select.vue @@ -7,9 +7,9 @@ import { GlSearchBoxByType, } from '@gitlab/ui'; import { debounce } from 'lodash'; -import Api from '~/api'; import { s__ } from '~/locale'; -import { SEARCH_DELAY } from '../constants'; +import { getGroups, getDescendentGroups } from '~/rest_api'; +import { SEARCH_DELAY, GROUP_FILTERS } from '../constants'; export default { name: 'GroupSelect', @@ -23,6 +23,18 @@ export default { model: { prop: 'selectedGroup', }, + props: { + groupsFilter: { + type: String, + required: false, + default: GROUP_FILTERS.ALL, + }, + parentGroupId: { + type: Number, + required: false, + default: null, + }, + }, data() { return { isFetching: false, @@ -50,7 +62,7 @@ export default { methods: { retrieveGroups: debounce(function debouncedRetrieveGroups() { this.isFetching = true; - return Api.groups(this.searchTerm, this.$options.defaultFetchOptions) + return this.fetchGroups() .then((response) => { this.groups = response.map((group) => ({ id: group.id, @@ -69,6 +81,18 @@ export default { this.$emit('input', this.selectedGroup); }, + fetchGroups() { + switch (this.groupsFilter) { + case GROUP_FILTERS.DESCENDANT_GROUPS: + return getDescendentGroups( + this.parentGroupId, + this.searchTerm, + this.$options.defaultFetchOptions, + ); + default: + return getGroups(this.searchTerm, this.$options.defaultFetchOptions); + } + }, }, i18n: { dropdownText: s__('GroupSelect|Select a group'), diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index 0db464bb657..84c8594c6b6 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -16,7 +16,7 @@ import GroupSelect from '~/invite_members/components/group_select.vue'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import { s__, sprintf } from '~/locale'; -import { INVITE_MEMBERS_IN_COMMENT } from '../constants'; +import { INVITE_MEMBERS_IN_COMMENT, GROUP_FILTERS } from '../constants'; import eventHub from '../event_hub'; export default { @@ -54,6 +54,16 @@ export default { type: Number, required: true, }, + groupSelectFilter: { + type: String, + required: false, + default: GROUP_FILTERS.ALL, + }, + groupSelectParentId: { + type: Number, + required: false, + default: null, + }, helpLink: { type: String, required: true, @@ -293,7 +303,12 @@ export default { :aria-labelledby="$options.membersTokenSelectLabelId" :placeholder="$options.labels[inviteeType].placeHolder" /> - <group-select v-if="isInviteGroup" v-model="groupToBeSharedWith" /> + <group-select + v-if="isInviteGroup" + v-model="groupToBeSharedWith" + :groups-filter="groupSelectFilter" + :parent-group-id="groupSelectParentId" + /> </div> <label class="gl-font-weight-bold gl-mt-3">{{ $options.labels.accessLevel }}</label> diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index a651b81c60e..0c5538d5b86 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -1,3 +1,8 @@ export const SEARCH_DELAY = 200; export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment'; + +export const GROUP_FILTERS = { + ALL: 'all', + DESCENDANT_GROUPS: 'descendant_groups', +}; diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index fc77bd53ba4..7501e9f4e6e 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -21,6 +21,8 @@ export default function initInviteMembersModal() { isProject: parseBoolean(el.dataset.isProject), accessLevels: JSON.parse(el.dataset.accessLevels), defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10), + groupSelectFilter: el.dataset.groupsFilter, + groupSelectParentId: parseInt(el.dataset.parentId, 10), }, }), }); diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue index 4ba2bc3a8e3..d5cab77f26c 100644 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue @@ -205,6 +205,19 @@ export default { return convertToSearchQuery(this.filterTokens) || undefined; }, searchTokens() { + let preloadedAuthors = []; + + if (gon.current_user_id) { + preloadedAuthors = [ + { + id: gon.current_user_id, + name: gon.current_user_fullname, + username: gon.current_username, + avatar_url: gon.current_user_avatar_url, + }, + ]; + } + const tokens = [ { type: TOKEN_TYPE_AUTHOR, @@ -215,6 +228,7 @@ export default { unique: true, defaultAuthors: [], fetchAuthors: this.fetchUsers, + preloadedAuthors, }, { type: TOKEN_TYPE_ASSIGNEE, @@ -225,6 +239,7 @@ export default { unique: !this.hasMultipleIssueAssigneesFeature, defaultAuthors: DEFAULT_NONE_ANY, fetchAuthors: this.fetchUsers, + preloadedAuthors, }, { type: TOKEN_TYPE_MILESTONE, diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue index edbe9441e57..6da2e3a47e8 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue @@ -9,17 +9,28 @@ import { UNAVAILABLE_ADMIN_FEATURE_TEXT, } from '~/packages_and_registries/settings/project/constants'; import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; +import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue'; +import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; import SettingsForm from './settings_form.vue'; export default { components: { + SettingsBlock, SettingsForm, + CleanupPolicyEnabledAlert, GlAlert, GlSprintf, GlLink, }, - inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries'], + inject: [ + 'projectPath', + 'isAdmin', + 'adminSettingsPath', + 'enableHistoricEntries', + 'helpPagePath', + 'showCleanupPolicyOnAlert', + ], i18n: { UNAVAILABLE_FEATURE_TITLE, UNAVAILABLE_FEATURE_INTRO_TEXT, @@ -75,32 +86,53 @@ export default { </script> <template> - <div> - <settings-form - v-if="!isDisabled" - v-model="workingCopy" - :is-loading="$apollo.queries.containerExpirationPolicy.loading" - :is-edited="isEdited" - @reset="restoreOriginal" - /> - <template v-else> - <gl-alert - v-if="showDisabledFormMessage" - :dismissible="false" - :title="$options.i18n.UNAVAILABLE_FEATURE_TITLE" - variant="tip" - > - {{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }} + <section data-testid="registry-settings-app"> + <cleanup-policy-enabled-alert v-if="showCleanupPolicyOnAlert" :project-path="projectPath" /> + <settings-block default-expanded> + <template #title> {{ __('Clean up image tags') }}</template> + <template #description> + <span data-testid="description"> + <gl-sprintf + :message=" + __( + 'Save space and find images in the container Registry. remove unneeded tags and keep only the ones you want. %{linkStart}How does cleanup work?%{linkEnd}', + ) + " + > + <template #link="{ content }"> + <gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </span> + </template> + <template #default> + <settings-form + v-if="!isDisabled" + v-model="workingCopy" + :is-loading="$apollo.queries.containerExpirationPolicy.loading" + :is-edited="isEdited" + @reset="restoreOriginal" + /> + <template v-else> + <gl-alert + v-if="showDisabledFormMessage" + :dismissible="false" + :title="$options.i18n.UNAVAILABLE_FEATURE_TITLE" + variant="tip" + > + {{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }} - <gl-sprintf :message="unavailableFeatureMessage"> - <template #link="{ content }"> - <gl-link :href="adminSettingsPath" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> - </gl-alert> - <gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false"> - <gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" /> - </gl-alert> - </template> - </div> + <gl-sprintf :message="unavailableFeatureMessage"> + <template #link="{ content }"> + <gl-link :href="adminSettingsPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </gl-alert> + <gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false"> + <gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" /> + </gl-alert> + </template> + </template> + </settings-block> + </section> </template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js index 65af6f846aa..2a3e2c28fa6 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js +++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js @@ -19,6 +19,8 @@ export default () => { projectPath, adminSettingsPath, tagsRegexHelpPagePath, + helpPagePath, + showCleanupPolicyOnAlert, } = el.dataset; return new Vue({ el, @@ -32,6 +34,8 @@ export default () => { projectPath, adminSettingsPath, tagsRegexHelpPagePath, + helpPagePath, + showCleanupPolicyOnAlert: parseBoolean(showCleanupPolicyOnAlert), }, render(createElement) { return createElement('registry-settings-app', {}); diff --git a/app/assets/javascripts/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue b/app/assets/javascripts/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue new file mode 100644 index 00000000000..d51c62e0623 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue @@ -0,0 +1,54 @@ +<script> +import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; + +export default { + components: { + GlAlert, + GlLink, + GlSprintf, + LocalStorageSync, + }, + props: { + projectPath: { + type: String, + required: true, + }, + cleanupPoliciesSettingsPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + dismissed: false, + }; + }, + computed: { + storageKey() { + return `cleanup_policy_enabled_for_project_${this.projectPath}`; + }, + }, + i18n: { + message: s__( + 'ContainerRegistry|Cleanup policies are now available for this project. %{linkStart}Click here to get started.%{linkEnd}', + ), + }, +}; +</script> + +<template> + <local-storage-sync v-model="dismissed" :storage-key="storageKey"> + <gl-alert v-if="!dismissed" class="gl-mt-2" dismissible @dismiss="dismissed = true"> + <gl-sprintf :message="$options.i18n.message"> + <template #link="{ content }"> + <gl-link v-if="cleanupPoliciesSettingsPath" :href="cleanupPoliciesSettingsPath">{{ + content + }}</gl-link> + </template> + </gl-sprintf> + </gl-alert> + </local-storage-sync> +</template> diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index 159c619e16c..d0ec5668d21 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -1,7 +1,15 @@ <script> -import { GlFormRadio, GlFormRadioGroup, GlLink, GlSprintf } from '@gitlab/ui'; +import { + GlFormRadio, + GlFormRadioGroup, + GlIcon, + GlLink, + GlSprintf, + GlTooltipDirective, +} from '@gitlab/ui'; import { getWeekdayNames } from '~/lib/utils/datetime_utility'; -import { s__, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; const KEY_EVERY_DAY = 'everyDay'; const KEY_EVERY_WEEK = 'everyWeek'; @@ -12,15 +20,25 @@ export default { components: { GlFormRadio, GlFormRadioGroup, + GlIcon, GlLink, GlSprintf, }, + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [glFeatureFlagMixin()], props: { initialCronInterval: { type: String, required: false, default: '', }, + dailyLimit: { + type: String, + required: false, + default: '', + }, }, data() { return { @@ -80,6 +98,17 @@ export default { weekday() { return getWeekdayNames()[this.randomWeekDayIndex]; }, + parsedDailyLimit() { + return this.dailyLimit ? (24 * 60) / this.dailyLimit : null; + }, + scheduleDailyLimitMsg() { + return sprintf( + __( + 'Scheduled pipelines cannot run more frequently than once per %{limit} minutes. A pipeline configured to run more frequently only starts after %{limit} minutes have elapsed since the last time it ran.', + ), + { limit: this.parsedDailyLimit }, + ); + }, }, watch: { cronInterval() { @@ -111,6 +140,11 @@ export default { generateRandomDay() { return Math.floor(Math.random() * 28); }, + showDailyLimitMessage({ value }) { + return ( + value === KEY_CUSTOM && this.glFeatures.ciDailyLimitForPipelineSchedules && this.dailyLimit + ); + }, }, }; </script> @@ -131,7 +165,15 @@ export default { </gl-link> </template> </gl-sprintf> + <template v-else>{{ option.text }}</template> + + <gl-icon + v-if="showDailyLimitMessage(option)" + v-gl-tooltip.hover + name="question" + :title="scheduleDailyLimitMsg" + /> </gl-form-radio> </gl-form-radio-group> <input diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js index ce0e573fed2..9056c76d6ca 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js @@ -12,6 +12,7 @@ Vue.use(Translate); function initIntervalPatternInput() { const intervalPatternMount = document.getElementById('interval-pattern-input'); const initialCronInterval = intervalPatternMount?.dataset?.initialInterval; + const dailyLimit = intervalPatternMount.dataset?.dailyLimit; return new Vue({ el: intervalPatternMount, @@ -22,6 +23,7 @@ function initIntervalPatternInput() { return createElement('interval-pattern-input', { props: { initialCronInterval, + dailyLimit, }, }); }, diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/registry/explorer/index.js index f66839a74bf..1f82fd7f238 100644 --- a/app/assets/javascripts/registry/explorer/index.js +++ b/app/assets/javascripts/registry/explorer/index.js @@ -34,6 +34,7 @@ export default () => { expirationPolicy, isGroupPage, isAdmin, + showCleanupPolicyOnAlert, showUnfinishedTagCleanupCallout, ...config } = el.dataset; @@ -64,6 +65,7 @@ export default () => { expirationPolicy: expirationPolicy ? JSON.parse(expirationPolicy) : undefined, isGroupPage: parseBoolean(isGroupPage), isAdmin: parseBoolean(isAdmin), + showCleanupPolicyOnAlert: parseBoolean(showCleanupPolicyOnAlert), showUnfinishedTagCleanupCallout: parseBoolean(showUnfinishedTagCleanupCallout), }, /* eslint-disable @gitlab/require-i18n-strings */ diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue index 589b88d7bbe..3c8790fa6e5 100644 --- a/app/assets/javascripts/registry/explorer/pages/list.vue +++ b/app/assets/javascripts/registry/explorer/pages/list.vue @@ -11,6 +11,7 @@ import { import { get } from 'lodash'; import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; import createFlash from '~/flash'; +import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue'; import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; import { extractFilterAndSorting } from '~/packages_and_registries/shared/utils'; import Tracking from '~/tracking'; @@ -61,6 +62,7 @@ export default { RegistryHeader, DeleteImage, RegistrySearch, + CleanupPolicyEnabledAlert, }, directives: { GlTooltip: GlTooltipDirective, @@ -283,6 +285,12 @@ export default { </gl-sprintf> </gl-alert> + <cleanup-policy-enabled-alert + v-if="config.showCleanupPolicyOnAlert" + :project-path="config.projectPath" + :cleanup-policies-settings-path="config.cleanupPoliciesSettingsPath" + /> + <gl-empty-state v-if="config.characterError" :title="$options.i18n.CONNECTION_ERROR_TITLE" diff --git a/app/assets/javascripts/security_configuration/components/redesigned_app.vue b/app/assets/javascripts/security_configuration/components/redesigned_app.vue index c2d57e8f0c8..d8a12f4a792 100644 --- a/app/assets/javascripts/security_configuration/components/redesigned_app.vue +++ b/app/assets/javascripts/security_configuration/components/redesigned_app.vue @@ -1,8 +1,10 @@ <script> import { GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui'; import { __, s__ } from '~/locale'; +import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; import FeatureCard from './feature_card.vue'; import SectionLayout from './section_layout.vue'; +import UpgradeBanner from './upgrade_banner.vue'; export const i18n = { compliance: s__('SecurityConfiguration|Compliance'), @@ -25,6 +27,8 @@ export default { GlSprintf, FeatureCard, SectionLayout, + UpgradeBanner, + UserCalloutDismisser, }, props: { augmentedSecurityFeatures: { @@ -52,6 +56,11 @@ export default { }, }, computed: { + canUpgrade() { + return [...this.augmentedSecurityFeatures, ...this.augmentedComplianceFeatures].some( + ({ available }) => !available, + ); + }, canViewCiHistory() { return Boolean(this.gitlabCiPresent && this.gitlabCiHistoryPath); }, @@ -65,6 +74,12 @@ export default { <h1 class="gl-font-size-h1">{{ $options.i18n.securityConfiguration }}</h1> </header> + <user-callout-dismisser v-if="canUpgrade" feature-name="security_configuration_upgrade_banner"> + <template #default="{ dismiss, shouldShowCallout }"> + <upgrade-banner v-if="shouldShowCallout" @close="dismiss" /> + </template> + </user-callout-dismisser> + <gl-tabs content-class="gl-pt-6"> <gl-tab data-testid="security-testing-tab" :title="$options.i18n.securityTesting"> <section-layout :heading="$options.i18n.securityTesting"> diff --git a/app/assets/javascripts/security_configuration/components/upgrade_banner.vue b/app/assets/javascripts/security_configuration/components/upgrade_banner.vue new file mode 100644 index 00000000000..ca0f9e5c85a --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/upgrade_banner.vue @@ -0,0 +1,45 @@ +<script> +import { GlBanner } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + components: { + GlBanner, + }, + inject: ['upgradePath'], + i18n: { + title: s__('SecurityConfiguration|Secure your project with Ultimate'), + bodyStart: s__( + `SecurityConfiguration|GitLab Ultimate checks your application for security vulnerabilities + that may lead to unauthorized access, data leaks, and denial of service + attacks. Its features include:`, + ), + bodyListItems: [ + s__('SecurityConfiguration|Vulnerability details and statistics in the merge request.'), + s__('SecurityConfiguration|High-level vulnerability statistics across projects and groups.'), + s__('SecurityConfiguration|Runtime security metrics for application environments.'), + ], + bodyEnd: s__( + 'SecurityConfiguration|With the information provided, you can immediately begin risk analysis and remediation within GitLab.', + ), + buttonText: s__('SecurityConfiguration|Upgrade or start a free trial'), + }, +}; +</script> + +<template> + <gl-banner + :title="$options.i18n.title" + :button-text="$options.i18n.buttonText" + :button-link="upgradePath" + v-on="$listeners" + > + <p>{{ $options.i18n.bodyStart }}</p> + <ul> + <li v-for="bodyListItem in $options.i18n.bodyListItems" :key="bodyListItem"> + {{ bodyListItem }} + </li> + </ul> + <p>{{ $options.i18n.bodyEnd }}</p> + </gl-banner> +</template> diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index 277e1400bf2..c80ccc928b3 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -293,9 +293,17 @@ export default { <span v-else-if="!currentAttribute" class="gl-text-gray-500"> {{ $options.i18n.none }} </span> - <gl-link v-else class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl"> - {{ attributeTitle }} - </gl-link> + <slot + v-else + name="value" + :attributeTitle="attributeTitle" + :attributeUrl="attributeUrl" + :currentAttribute="currentAttribute" + > + <gl-link class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl"> + {{ attributeTitle }} + </gl-link> + </slot> </div> </template> <template #default> @@ -327,16 +335,24 @@ export default { <gl-dropdown-text v-if="emptyPropsList"> {{ i18n.noAttributesFound }} </gl-dropdown-text> - <gl-dropdown-item - v-for="attrItem in attributesList" - :key="attrItem.id" - :is-check-item="true" - :is-checked="isAttributeChecked(attrItem.id)" - :data-testid="`${issuableAttribute}-items`" - @click="updateAttribute(attrItem.id)" + <slot + v-else + name="list" + :attributesList="attributesList" + :isAttributeChecked="isAttributeChecked" + :updateAttribute="updateAttribute" > - {{ attrItem.title }} - </gl-dropdown-item> + <gl-dropdown-item + v-for="attrItem in attributesList" + :key="attrItem.id" + :is-check-item="true" + :is-checked="isAttributeChecked(attrItem.id)" + :data-testid="`${issuableAttribute}-items`" + @click="updateAttribute(attrItem.id)" + > + {{ attrItem.title }} + </gl-dropdown-item> + </slot> </template> </gl-dropdown> </template> diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue index db8d67d86dc..2e7b3e149b2 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue @@ -32,14 +32,7 @@ export default { return { authors: this.config.initialAuthors || [], defaultAuthors: this.config.defaultAuthors || [DEFAULT_LABEL_ANY], - preloadedAuthors: [ - { - id: gon.current_user_id, - name: gon.current_user_fullname, - username: gon.current_username, - avatar_url: gon.current_user_avatar_url, - }, - ], + preloadedAuthors: this.config.preloadedAuthors || [], loading: false, }; }, diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue index 6bd67a4cdf0..fb6b9e4bc0d 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -83,7 +83,10 @@ export default { return Boolean(this.recentTokenValuesStorageKey); }, recentTokenIds() { - return this.recentTokenValues.map((tokenValue) => tokenValue.id || tokenValue.name); + return this.recentTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]); + }, + preloadedTokenIds() { + return this.preloadedTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]); }, currentTokenValue() { if (this.fnCurrentTokenValue) { @@ -103,7 +106,9 @@ export default { return this.searchKey ? this.tokenValues : this.tokenValues.filter( - (tokenValue) => !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]), + (tokenValue) => + !this.recentTokenIds.includes(tokenValue[this.valueIdentifier]) && + !this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]), ); }, }, @@ -125,7 +130,15 @@ export default { }, DEBOUNCE_DELAY); }, handleTokenValueSelected(activeTokenValue) { - if (this.isRecentTokenValuesEnabled && activeTokenValue) { + // Make sure that; + // 1. Recently used values feature is enabled + // 2. User has actually selected a value + // 3. Selected value is not part of preloaded list. + if ( + this.isRecentTokenValuesEnabled && + activeTokenValue && + !this.preloadedTokenIds.includes(activeTokenValue[this.valueIdentifier]) + ) { setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue); } }, diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index a6235d8fc04..3d8cdd766bf 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -10,6 +10,7 @@ class Groups::BoardsController < Groups::ApplicationController push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false) push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml) push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml) + push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml) end feature_category :boards diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index a755d242d4a..cf6d34b2042 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -33,6 +33,7 @@ class GroupsController < Groups::ApplicationController before_action do push_frontend_feature_flag(:vue_issuables_list, @group) + push_frontend_feature_flag(:iteration_cadences, @group, default_enabled: :yaml) end before_action :export_rate_limit, only: [:export, :download_export] diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index ee8bacbac61..43c9046f850 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -10,6 +10,7 @@ class Projects::BoardsController < Projects::ApplicationController push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml) push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml) push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml) + push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml) end feature_category :boards diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 848463bc3ec..295213bd38c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -46,6 +46,7 @@ class Projects::IssuesController < Projects::ApplicationController push_frontend_feature_flag(:usage_data_design_action, project, default_enabled: true) push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml) push_frontend_feature_flag(:vue_issues_list, project) + push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml) end before_action only: :show do diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index 4af7508b935..006cb8a2201 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -10,6 +10,10 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create, :play] before_action :authorize_admin_pipeline_schedule!, only: [:destroy] + before_action do + push_frontend_feature_flag(:ci_daily_limit_for_pipeline_schedules, @project, default_enabled: :yaml) + end + feature_category :continuous_integration # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/graphql/mutations/ci/runners_registration_token/reset.rb b/app/graphql/mutations/ci/runners_registration_token/reset.rb new file mode 100644 index 00000000000..e1cdd9a22a5 --- /dev/null +++ b/app/graphql/mutations/ci/runners_registration_token/reset.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module RunnersRegistrationToken + class Reset < BaseMutation + graphql_name 'RunnersRegistrationTokenReset' + + authorize :update_runners_registration_token + + ScopeID = ::GraphQL::ID_TYPE + + argument :type, ::Types::Ci::RunnerTypeEnum, + required: true, + description: 'Scope of the object to reset the token for.' + + argument :id, ScopeID, + required: false, + description: 'ID of the project or group to reset the token for. Omit if resetting instance runner token.' + + field :token, + GraphQL::STRING_TYPE, + null: true, + description: 'The runner token after mutation.' + + def resolve(**args) + { + token: reset_token(**args), + errors: [] + } + end + + private + + def find_object(type:, **args) + id = args[:id] + + case type + when 'group_type' + GitlabSchema.object_from_id(id, expected_type: ::Group) + when 'project_type' + GitlabSchema.object_from_id(id, expected_type: ::Project) + end + end + + def reset_token(type:, **args) + id = args[:id] + + case type + when 'instance_type' + raise Gitlab::Graphql::Errors::ArgumentError, "id must not be specified for '#{type}' scope" if id.present? + + authorize!(:global) + + ApplicationSetting.current.reset_runners_registration_token! + ApplicationSetting.current_without_cache.runners_registration_token + when 'group_type', 'project_type' + project_or_group = authorized_find!(type: type, id: id) + project_or_group.reset_runners_token! + project_or_group.runners_token + end + end + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 561cbf24da3..6b1146f8f09 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -101,6 +101,7 @@ module Types mount_mutation Mutations::Ci::Job::Retry mount_mutation Mutations::Ci::Runner::Update, feature_flag: :runner_graphql_query mount_mutation Mutations::Ci::Runner::Delete, feature_flag: :runner_graphql_query + mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset, feature_flag: :runner_graphql_query mount_mutation Mutations::Namespace::PackageSettings::Update mount_mutation Mutations::UserCallouts::Create end diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index 8e0511e40f2..3c290701a5f 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -31,4 +31,12 @@ module InviteMembersHelper { member_human_access: member.human_access, name: member.source.name } end end + + def group_select_data(group) + if group.root_ancestor.namespace_settings.prevent_sharing_groups_outside_hierarchy + { groups_filter: 'descendant_groups', parent_id: group.root_ancestor.id } + else + {} + end + end end diff --git a/app/helpers/nav/top_nav_helper.rb b/app/helpers/nav/top_nav_helper.rb index b511a928a2c..b8ddb932b73 100644 --- a/app/helpers/nav/top_nav_helper.rb +++ b/app/helpers/nav/top_nav_helper.rb @@ -276,7 +276,11 @@ module Nav builder = ::Gitlab::Nav::TopNavMenuBuilder.new builder.add_primary_menu_item(id: 'your', title: _('Your groups'), href: dashboard_groups_path) builder.add_primary_menu_item(id: 'explore', title: _('Explore groups'), href: explore_groups_path) - builder.add_secondary_menu_item(id: 'create', title: _('Create group'), href: new_group_path) + + if current_user.can_create_group? + builder.add_secondary_menu_item(id: 'create', title: _('Create group'), href: new_group_path) + end + builder.build end end diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb index 04465f7798c..fe41c041b4f 100644 --- a/app/helpers/packages_helper.rb +++ b/app/helpers/packages_helper.rb @@ -53,4 +53,14 @@ module PackagesHelper category = args.delete(:category) || self.class.name ::Gitlab::Tracking.event(category, event_name.to_s, **args) end + + def show_cleanup_policy_on_alert(project) + Gitlab.com? && + Gitlab.config.registry.enabled && + project.container_registry_enabled && + !Gitlab::CurrentSettings.container_expiration_policies_enable_historic_entries && + Feature.enabled?(:container_expiration_policies_historic_entry, project) && + project.container_expiration_policy.nil? && + project.container_repositories.exists? + end end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 5248a80f710..586142a7646 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -18,7 +18,6 @@ module Ci ACCESSIBILITY_REPORT_FILE_TYPES = %w[accessibility].freeze NON_ERASABLE_FILE_TYPES = %w[trace].freeze TERRAFORM_REPORT_FILE_TYPES = %w[terraform].freeze - UNSUPPORTED_FILE_TYPES = %i[license_management].freeze SAST_REPORT_TYPES = %w[sast].freeze SECRET_DETECTION_REPORT_TYPES = %w[secret_detection].freeze DEFAULT_FILE_NAMES = { @@ -35,7 +34,6 @@ module Ci dependency_scanning: 'gl-dependency-scanning-report.json', container_scanning: 'gl-container-scanning-report.json', dast: 'gl-dast-report.json', - license_management: 'gl-license-management-report.json', license_scanning: 'gl-license-scanning-report.json', performance: 'performance.json', browser_performance: 'browser-performance.json', @@ -74,7 +72,6 @@ module Ci dependency_scanning: :raw, container_scanning: :raw, dast: :raw, - license_management: :raw, license_scanning: :raw, # All these file formats use `raw` as we need to store them uncompressed @@ -102,7 +99,6 @@ module Ci dependency_scanning dotenv junit - license_management license_scanning lsif metrics @@ -124,7 +120,6 @@ module Ci mount_file_store_uploader JobArtifactUploader validates :file_format, presence: true, unless: :trace?, on: :create - validate :validate_supported_file_format!, on: :create validate :validate_file_format!, unless: :trace?, on: :create before_save :set_size, if: :file_changed? @@ -199,8 +194,7 @@ module Ci container_scanning: 7, ## EE-specific dast: 8, ## EE-specific codequality: 9, ## EE-specific - license_management: 10, ## EE-specific - license_scanning: 101, ## EE-specific till 13.0 + license_scanning: 101, ## EE-specific performance: 11, ## EE-specific till 13.2 metrics: 12, ## EE-specific metrics_referee: 13, ## runner referees @@ -233,14 +227,6 @@ module Ci hashed_path: 2 } - def validate_supported_file_format! - return if Feature.disabled?(:drop_license_management_artifact, project, default_enabled: true) - - if UNSUPPORTED_FILE_TYPES.include?(self.file_type&.to_sym) - errors.add(:base, _("File format is no longer supported")) - end - end - def validate_file_format! unless TYPE_AND_FORMAT_PAIRS[self.file_type&.to_sym] == self.file_format&.to_sym errors.add(:base, _('Invalid file format with specified file type')) diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index b405be8620c..effe2d95a99 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -63,6 +63,10 @@ module Ci .execute(self, fallback_method: method(:calculate_next_run_at)) end + def daily_limit + project.actual_limits.limit_for(:ci_daily_pipeline_schedule_triggers) + end + private def worker_cron_expression diff --git a/app/models/project.rb b/app/models/project.rb index f7eba76849d..3a89a85d65d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -421,12 +421,12 @@ class Project < ApplicationRecord delegate :last_pipeline, to: :commit, allow_nil: true delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true delegate :dashboard_timezone, to: :metrics_setting, allow_nil: true, prefix: true - delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci - delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings, prefix: :ci + delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci, allow_nil: true + delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings, prefix: :ci, allow_nil: true delegate :job_token_scope_enabled, :job_token_scope_enabled=, :job_token_scope_enabled?, to: :ci_cd_settings, prefix: :ci - delegate :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, :keep_latest_artifacts_available?, to: :ci_cd_settings + delegate :keep_latest_artifact, :keep_latest_artifact=, :keep_latest_artifact?, :keep_latest_artifacts_available?, to: :ci_cd_settings, allow_nil: true delegate :restrict_user_defined_variables, :restrict_user_defined_variables=, :restrict_user_defined_variables?, - to: :ci_cd_settings + to: :ci_cd_settings, allow_nil: true delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true delegate :allow_merge_on_skipped_pipeline, :allow_merge_on_skipped_pipeline?, :allow_merge_on_skipped_pipeline=, :has_confluence?, :allow_editing_commit_messages?, diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index 48386a84e39..2e8ff1b7b49 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -30,7 +30,8 @@ class UserCallout < ApplicationRecord eoa_bronze_plan_banner: 28, # EE-only pipeline_needs_banner: 29, pipeline_needs_hover_tip: 30, - web_ide_ci_environments_guidance: 31 + web_ide_ci_environments_guidance: 31, + security_configuration_upgrade_banner: 32 } validates :user, presence: true diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 73757891cd6..35d38bac7fa 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -115,6 +115,7 @@ class GlobalPolicy < BasePolicy enable :approve_user enable :reject_user enable :read_usage_trends_measurement + enable :update_runners_registration_token end # We can't use `read_statistics` because the user may have different permissions for different projects diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index dc8ecfa4333..41c5757854e 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -144,6 +144,7 @@ class GroupPolicy < BasePolicy enable :admin_cluster enable :read_deploy_token enable :create_jira_connect_subscription + enable :update_runners_registration_token end rule { owner }.policy do diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 4dfdbd87a34..e93c60c3710 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -419,6 +419,7 @@ class ProjectPolicy < BasePolicy enable :update_freeze_period enable :destroy_freeze_period enable :admin_feature_flags_client + enable :update_runners_registration_token end rule { public_project & metrics_dashboard_allowed }.policy do diff --git a/app/services/ci/pipeline_schedules/calculate_next_run_service.rb b/app/services/ci/pipeline_schedules/calculate_next_run_service.rb index 4a9ca5d3045..9978b2d4775 100644 --- a/app/services/ci/pipeline_schedules/calculate_next_run_service.rb +++ b/app/services/ci/pipeline_schedules/calculate_next_run_service.rb @@ -41,11 +41,11 @@ module Ci def plan_cron strong_memoize(:plan_cron) do - daily_scheduled_pipeline_limit = project.actual_limits.limit_for(:ci_daily_pipeline_schedule_triggers) + daily_limit = @schedule.daily_limit - next unless daily_scheduled_pipeline_limit + next unless daily_limit - every_x_minutes = (1.day.in_minutes / daily_scheduled_pipeline_limit).to_i + every_x_minutes = (1.day.in_minutes / daily_limit).to_i Gitlab::Ci::CronParser.parse_natural("every #{every_x_minutes} minutes", Time.zone.name) end diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index f258aa13376..77d2139b3d1 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -91,12 +91,10 @@ class WebHookService end def async_execute - if rate_limited?(hook) - log_rate_limit(hook) - else - Gitlab::ApplicationContext.with_context(hook.application_context) do - WebHookWorker.perform_async(hook.id, data, hook_name) - end + Gitlab::ApplicationContext.with_context(hook.application_context) do + break log_rate_limit if rate_limited? + + WebHookWorker.perform_async(hook.id, data, hook_name) end end @@ -177,7 +175,7 @@ class WebHookService response.body.encode('UTF-8', invalid: :replace, undef: :replace, replace: '') end - def rate_limited?(hook) + def rate_limited? return false unless Feature.enabled?(:web_hooks_rate_limit, default_enabled: :yaml) return false if rate_limit.nil? @@ -192,18 +190,13 @@ class WebHookService @rate_limit ||= hook.rate_limit end - def log_rate_limit(hook) - payload = { + def log_rate_limit + Gitlab::AuthLogger.error( message: 'Webhook rate limit exceeded', hook_id: hook.id, hook_type: hook.type, - hook_name: hook_name - } - - Gitlab::AuthLogger.error(payload) - - # Also log into application log for now, so we can use this information - # to determine suitable limits for gitlab.com - Gitlab::AppLogger.error(payload) + hook_name: hook_name, + **Gitlab::ApplicationContext.current + ) end end diff --git a/app/views/admin/application_settings/_diff_limits.html.haml b/app/views/admin/application_settings/_diff_limits.html.haml index ff22f6181b3..5351ac5abd1 100644 --- a/app/views/admin/application_settings/_diff_limits.html.haml +++ b/app/views/admin/application_settings/_diff_limits.html.haml @@ -3,10 +3,10 @@ %fieldset .form-group - = f.label :diff_max_patch_bytes, 'Maximum diff patch size in bytes', class: 'label-light' + = f.label :diff_max_patch_bytes, _('Maximum diff patch size in bytes'), class: 'label-light' = f.number_field :diff_max_patch_bytes, class: 'form-control gl-form-input' %span.form-text.text-muted - Collapse diffs larger than this size, and show a 'too large' message instead. + = _("Collapse diffs larger than this size, and show a 'too large' message instead.") = link_to sprite_icon('question-o'), help_page_path('user/admin_area/diff_limits') diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index fe5759ecdbf..b68c22b6942 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -1,10 +1,12 @@ .broadcast-message.broadcast-banner-message.gl-alert-warning.js-broadcast-banner-message-preview.gl-mt-3{ style: broadcast_message_style(@broadcast_message), class: ('gl-display-none' unless @broadcast_message.banner? ) } - = sprite_icon('bullhorn', css_class: 'vertical-align-text-top') - .js-broadcast-message-preview - - if @broadcast_message.message.present? - = render_broadcast_message(@broadcast_message) - - else - = _('Your message here') + .gl-alert-container + = sprite_icon('bullhorn', css_class: 'vertical-align-text-top') + .js-broadcast-message-preview + .gl-alert-content + - if @broadcast_message.message.present? + = render_broadcast_message(@broadcast_message) + - else + = _('Your message here') .d-flex.justify-content-center .broadcast-message.broadcast-notification-message.preview.js-broadcast-notification-message-preview.mt-2{ class: ('hidden' unless @broadcast_message.notification? ) } = sprite_icon('bullhorn', css_class: 'vertical-align-text-top') diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 84a9b988d22..e7e0e58f6fb 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -27,10 +27,12 @@ - if @group.new_record? .form-group.row .offset-sm-2.col-sm-10 - .gl-alert.gl-alert-info - = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') - .gl-alert-body - = render 'shared/group_tips' + .gl-alert.gl-alert- + .gl-alert-container + = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') + .gl-alert-content + .gl-alert-body + = render 'shared/group_tips' .form-actions = f.submit _('Create group'), class: "gl-button btn btn-confirm" = link_to _('Cancel'), admin_groups_path, class: "gl-button btn btn-default btn-cancel" diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml index 69ed94e99cc..f4f3c8ce8f7 100644 --- a/app/views/groups/_invite_members_modal.html.haml +++ b/app/views/groups/_invite_members_modal.html.haml @@ -4,4 +4,4 @@ is_project: 'false', access_levels: GroupMember.access_level_roles.to_json, default_access_level: Gitlab::Access::GUEST, - help_link: help_page_url('user/permissions') } } + help_link: help_page_url('user/permissions') }.merge(group_select_data(group)) } diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 2305bbd96f3..c5b8c5e25a3 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,6 +1,6 @@ - add_page_specific_style 'page_bundles/members' - page_title _('Group members') -- groups_select_tag_data = @group.root_ancestor.namespace_settings.prevent_sharing_groups_outside_hierarchy ? { groups_filter: 'descendant_groups', parent_id: @group.root_ancestor.id, skip_groups: @skip_groups } : { skip_groups: @skip_groups } +- groups_select_tag_data = group_select_data(@group).merge({ skip_groups: @skip_groups }) .js-remove-member-modal .row.gl-mt-3 diff --git a/app/views/layouts/nav/groups_dropdown/_show.html.haml b/app/views/layouts/nav/groups_dropdown/_show.html.haml index 1ace402fbed..d7b0c7150d4 100644 --- a/app/views/layouts/nav/groups_dropdown/_show.html.haml +++ b/app/views/layouts/nav/groups_dropdown/_show.html.haml @@ -12,11 +12,12 @@ = nav_link(path: 'groups#explore') do = link_to explore_groups_path, data: { track_label: "groups_dropdown_explore_groups", track_event: "click_link" } do = _('Explore groups') - = nav_link(path: 'groups/new#create-group-pane', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do - = link_to new_group_path(anchor: 'create-group-pane'), data: { track_label: "groups_dropdown_create_group", track_event: "click_link" } do - = _('Create group') - = nav_link(path: 'groups/new#import-group-pane') do - = link_to new_group_path(anchor: 'import-group-pane'), data: { track_label: "groups_dropdown_import_group", track_event: "click_link" } do - = _('Import group') + - if current_user.can_create_group? + = nav_link(path: 'groups/new#create-group-pane', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do + = link_to new_group_path(anchor: 'create-group-pane'), data: { track_label: "groups_dropdown_create_group", track_event: "click_link" } do + = _('Create group') + = nav_link(path: 'groups/new#import-group-pane') do + = link_to new_group_path(anchor: 'import-group-pane'), data: { track_label: "groups_dropdown_import_group", track_event: "click_link" } do + = _('Import group') .frequent-items-dropdown-content #js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } } diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index e6c9a7166a9..9e0dd93c683 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -35,8 +35,10 @@ - if hidden > 0 %li.gl-alert.gl-alert-warning - = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') - = n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden) + .gl-alert-container + = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') + .gl-alert-content + = n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden) - if project.context_commits_enabled? && can_update_merge_request && context_commits&.empty? %button.gl-button.btn.btn-default.mt-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'true' } } diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index 628c4780cf2..66aee7dedf3 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -7,7 +7,7 @@ .form-group.row .col-md-9 = f.label :cron, _('Interval Pattern'), class: 'label-bold' - #interval-pattern-input{ data: { initial_interval: @schedule.cron } } + #interval-pattern-input{ data: { initial_interval: @schedule.cron, daily_limit: @schedule.daily_limit } } .form-group.row .col-md-9 = f.label :cron_timezone, _('Cron Timezone'), class: 'label-bold' diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index f56fd7f557d..bdb5f021b70 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -18,6 +18,8 @@ "project_path": @project.full_path, "gid_prefix": container_repository_gid_prefix, "is_admin": current_user&.admin.to_s, + "show_cleanup_policy_on_alert": show_cleanup_policy_on_alert(@project).to_s, + "cleanup_policies_settings_path": project_settings_packages_and_registries_path(@project), character_error: @character_error.to_s, user_callouts_path: user_callouts_path, user_callout_id: UserCalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT, diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index d955dabd04c..ade3d40a8df 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -76,17 +76,7 @@ = render 'projects/triggers/index' - if settings_container_registry_expiration_policy_available?(@project) - %section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = _("Clean up image tags") - %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _("Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want.") - = link_to _('How does cleanup work?'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy'), target: '_blank', rel: 'noopener noreferrer' - .settings-content - = render 'projects/registry/settings/index' + = render 'projects/registry/settings/index' = render_if_exists 'projects/settings/ci_cd/auto_rollback', expanded: expanded diff --git a/app/views/projects/settings/packages_and_registries/show.html.haml b/app/views/projects/settings/packages_and_registries/show.html.haml index 561ac7b347d..626ddc20431 100644 --- a/app/views/projects/settings/packages_and_registries/show.html.haml +++ b/app/views/projects/settings/packages_and_registries/show.html.haml @@ -1,16 +1,15 @@ - breadcrumb_title _('Packages & Registries') - page_title _('Packages & Registries') - @content_class = 'limit-container-width' unless fluid_layout -- expanded = true -%section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) } - .settings-header - %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only - = _("Clean up image tags") - %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _("Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want.") - = link_to _('How does cleanup work?'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy'), target: '_blank', rel: 'noopener noreferrer' - .settings-content - = render 'projects/registry/settings/index' +#js-registry-settings{ data: { project_id: @project.id, + project_path: @project.full_path, + cadence_options: cadence_options.to_json, + keep_n_options: keep_n_options.to_json, + older_than_options: older_than_options.to_json, + is_admin: current_user&.admin.to_s, + admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'), + enable_historic_entries: container_expiration_policies_historic_entry_enabled?(@project).to_s, + help_page_path: help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy'), + show_cleanup_policy_on_alert: show_cleanup_policy_on_alert(@project).to_s, + tags_regex_help_page_path: help_page_path('user/packages/container_registry/index', anchor: 'regex-pattern-examples') } } diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 59e6cd43325..109b08bf0ec 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1428,7 +1428,7 @@ :urgency: :high :resource_boundary: :cpu :weight: 3 - :idempotent: true + :idempotent: :tags: [] - :name: pipeline_creation:create_pipeline :worker_name: CreatePipelineWorker @@ -1610,7 +1610,7 @@ :urgency: :high :resource_boundary: :unknown :weight: 5 - :idempotent: + :idempotent: true :tags: [] - :name: pipeline_processing:stage_update :worker_name: StageUpdateWorker diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb index 3c48c4ba3cd..9702fac39ba 100644 --- a/app/workers/expire_pipeline_cache_worker.rb +++ b/app/workers/expire_pipeline_cache_worker.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop: disable Scalability/IdempotentWorker class ExpirePipelineCacheWorker include ApplicationWorker @@ -9,8 +10,12 @@ class ExpirePipelineCacheWorker queue_namespace :pipeline_cache urgency :high worker_resource_boundary :cpu + data_consistency :delayed, feature_flag: :load_balancing_for_expire_pipeline_cache_worker - idempotent! + # This worker _should_ be idempotent, but due to us moving this to data_consistency :delayed + # and an ongoing incompatibility between the two switches, we need to disable this. + # Uncomment once https://gitlab.com/gitlab-org/gitlab/-/issues/325291 is resolved + # idempotent! # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id) @@ -21,3 +26,4 @@ class ExpirePipelineCacheWorker end # rubocop: enable CodeReuse/ActiveRecord end +# rubocop:enable Scalability/IdempotentWorker diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb index 43aaac4e311..a35b32c35f2 100644 --- a/app/workers/pipeline_process_worker.rb +++ b/app/workers/pipeline_process_worker.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class PipelineProcessWorker # rubocop:disable Scalability/IdempotentWorker +class PipelineProcessWorker include ApplicationWorker sidekiq_options retry: 3 @@ -10,7 +10,9 @@ class PipelineProcessWorker # rubocop:disable Scalability/IdempotentWorker feature_category :continuous_integration urgency :high loggable_arguments 1 - data_consistency :delayed, feature_flag: :load_balancing_for_pipeline_process_worker + + idempotent! + deduplicate :until_executing, feature_flag: :ci_idempotent_pipeline_process_worker # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id) |