diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-11 15:09:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-11 15:09:49 +0300 |
commit | dcf94a76413ddb50148bdac7b189afb7bffa7580 (patch) | |
tree | b5ecff1d1aea4d3ad95d728531f95f80c00a47ca | |
parent | a350f877c4246fee981690388239d1e19e17202a (diff) |
Add latest changes from gitlab-org/gitlab@master
53 files changed, 544 insertions, 417 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index dc1923a22c3..affbeedc25a 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -321,6 +321,8 @@ changes: *ci-build-images-patterns - <<: *if-dot-com-gitlab-org-and-security-merge-request changes: *code-qa-patterns + - <<: *if-dot-com-gitlab-org-default-branch + changes: *code-qa-patterns - <<: *if-dot-com-gitlab-org-schedule .build-images:rules:build-assets-image: diff --git a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue index 85f001d9d61..2aee84b805f 100644 --- a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue +++ b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue @@ -1,21 +1,25 @@ <script> import { GlButton } from '@gitlab/ui'; import { mapActions } from 'vuex'; +import Tracking from '~/tracking'; export default { components: { GlButton, }, + mixins: [Tracking.mixin()], methods: { ...mapActions(['setAddColumnFormVisibility']), + handleClick() { + this.setAddColumnFormVisibility(true); + this.track('click_button', { label: 'create_list' }); + }, }, }; </script> <template> <div class="gl-ml-3 gl-display-flex gl-align-items-center" data-testid="boards-create-list"> - <gl-button variant="confirm" @click="setAddColumnFormVisibility(true)" - >{{ __('Create list') }} - </gl-button> + <gl-button variant="confirm" @click="handleClick">{{ __('Create list') }} </gl-button> </div> </template> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 53c893e0734..1e780f9ef84 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,5 +1,6 @@ <script> import { mapActions, mapState } from 'vuex'; +import Tracking from '~/tracking'; import BoardCardInner from './board_card_inner.vue'; export default { @@ -7,6 +8,7 @@ export default { components: { BoardCardInner, }, + mixins: [Tracking.mixin()], props: { list: { type: Object, @@ -58,6 +60,7 @@ export default { this.toggleBoardItemMultiSelection(this.item); } else { this.toggleBoardItem({ boardItem: this.item }); + this.track('click_card', { label: 'right_sidebar' }); } }, }, diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue index 8fd72cfceb5..f83927ea4e7 100644 --- a/app/assets/javascripts/boards/components/board_content_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue @@ -2,7 +2,6 @@ import { GlDrawer } from '@gitlab/ui'; import { mapState, mapActions, mapGetters } from 'vuex'; import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue'; -import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; @@ -10,6 +9,7 @@ import { ISSUABLE } from '~/boards/constants'; import { contentTop } from '~/lib/utils/common_utils'; import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; 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'; export default { @@ -18,10 +18,10 @@ export default { GlDrawer, BoardSidebarTitle, SidebarAssigneesWidget, + SidebarDateWidget, SidebarConfidentialityWidget, BoardSidebarTimeTracker, BoardSidebarLabelsSelect, - BoardSidebarDueDate, SidebarSubscriptionsWidget, SidebarDropdownWidget, BoardSidebarWeightInput: () => @@ -116,7 +116,12 @@ export default { /> </div> <board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" /> - <board-sidebar-due-date /> + <sidebar-date-widget + :iid="activeBoardItem.iid" + :full-path="fullPath" + :issuable-type="issuableType" + data-testid="sidebar-due-date" + /> <board-sidebar-labels-select class="labels" /> <board-sidebar-weight-input v-if="weightFeatureAvailable" class="weight" /> <sidebar-confidentiality-widget diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 4f396bddb90..81740b5cd17 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -5,6 +5,7 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options'; import { sprintf, __ } from '~/locale'; import defaultSortableConfig from '~/sortable/sortable_config'; +import Tracking from '~/tracking'; import eventHub from '../eventhub'; import BoardCard from './board_card.vue'; import BoardNewIssue from './board_new_issue.vue'; @@ -23,6 +24,7 @@ export default { GlLoadingIcon, GlIntersectionObserver, }, + mixins: [Tracking.mixin()], inject: { canAdminList: { default: false, @@ -155,6 +157,7 @@ export default { }, handleDragOnStart() { sortableStart(); + this.track('drag_card', { label: 'board' }); }, handleDragOnEnd(params) { sortableEnd(); diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index a73786dd613..bf8396f52a6 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -14,6 +14,7 @@ import { isScopedLabel, parseBoolean } from '~/lib/utils/common_utils'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { n__, s__, __ } from '~/locale'; import sidebarEventHub from '~/sidebar/event_hub'; +import Tracking from '~/tracking'; import AccessorUtilities from '../../lib/utils/accessor'; import { inactiveId, LIST, ListType } from '../constants'; import eventHub from '../eventhub'; @@ -38,6 +39,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [Tracking.mixin()], inject: { boardId: { default: '', @@ -155,6 +157,8 @@ export default { } this.setActiveId({ id: this.list.id, sidebarType: LIST }); + + this.track('click_button', { label: 'list_settings' }); }, showScopedLabels(label) { return this.scopedLabelsAvailable && isScopedLabel(label); @@ -176,6 +180,11 @@ export default { // When expanding/collapsing, the tooltip on the caret button sometimes stays open. // Close all tooltips manually to prevent dangling tooltips. this.$root.$emit(BV_HIDE_TOOLTIP); + + this.track('click_toggle_button', { + label: 'toggle_list', + property: collapsed ? 'closed' : 'open', + }); }, addToLocalStorage() { if (AccessorUtilities.isLocalStorageAccessSafe()) { diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index 3d7f1f38a34..75975c77df5 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -6,6 +6,7 @@ import boardsStore from '~/boards/stores/boards_store'; import { isScopedLabel } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import eventHub from '~/sidebar/event_hub'; +import Tracking from '~/tracking'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; // NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options. @@ -21,7 +22,7 @@ export default { BoardSettingsListTypes: () => import('ee_component/boards/components/board_settings_list_types.vue'), }, - mixins: [glFeatureFlagMixin()], + mixins: [glFeatureFlagMixin(), Tracking.mixin()], inject: ['canAdminList'], data() { return { @@ -72,6 +73,7 @@ export default { // eslint-disable-next-line no-alert if (window.confirm(__('Are you sure you want to remove this list?'))) { if (this.shouldUseGraphQL || this.isEpicBoard) { + this.track('click_button', { label: 'remove_list' }); this.removeList(this.activeId); } else { this.activeList.destroy(); diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue index fdb60d0ae6a..30e304b8a65 100644 --- a/app/assets/javascripts/boards/components/config_toggle.vue +++ b/app/assets/javascripts/boards/components/config_toggle.vue @@ -3,6 +3,7 @@ import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; import { formType } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; import { s__, __ } from '~/locale'; +import Tracking from '~/tracking'; export default { components: { @@ -12,6 +13,7 @@ export default { GlTooltip: GlTooltipDirective, GlModalDirective, }, + mixins: [Tracking.mixin()], props: { boardsStore: { type: Object, @@ -37,6 +39,7 @@ export default { }, methods: { showPage() { + this.track('click_button', { label: 'edit_board' }); eventHub.$emit('showBoardModal', formType.edit); if (this.boardsStore) { this.boardsStore.showPage(formType.edit); diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue deleted file mode 100644 index 87ae17025ea..00000000000 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue +++ /dev/null @@ -1,109 +0,0 @@ -<script> -import { GlButton, GlDatepicker } from '@gitlab/ui'; -import { mapGetters, mapActions } from 'vuex'; -import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; -import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility'; -import { __ } from '~/locale'; - -export default { - components: { - BoardEditableItem, - GlButton, - GlDatepicker, - }, - data() { - return { - loading: false, - }; - }, - computed: { - ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']), - hasDueDate() { - return this.activeBoardItem.dueDate != null; - }, - parsedDueDate() { - if (!this.hasDueDate) { - return null; - } - - return parsePikadayDate(this.activeBoardItem.dueDate); - }, - formattedDueDate() { - if (!this.hasDueDate) { - return ''; - } - - return dateInWords(this.parsedDueDate, true); - }, - }, - methods: { - ...mapActions(['setActiveIssueDueDate', 'setError']), - async openDatePicker() { - await this.$nextTick(); - this.$refs.datePicker.calendar.show(); - }, - async setDueDate(date) { - this.loading = true; - this.$refs.sidebarItem.collapse(); - - try { - const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null; - await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPathForActiveIssue }); - } catch (e) { - this.setError({ message: this.$options.i18n.updateDueDateError }); - } finally { - this.loading = false; - } - }, - }, - i18n: { - dueDate: __('Due date'), - removeDueDate: __('remove due date'), - updateDueDateError: __('An error occurred when updating the issue due date'), - }, -}; -</script> - -<template> - <board-editable-item - ref="sidebarItem" - class="board-sidebar-due-date" - data-testid="sidebar-due-date" - :title="$options.i18n.dueDate" - :loading="loading" - @open="openDatePicker" - > - <template v-if="hasDueDate" #collapsed> - <div class="gl-display-flex gl-align-items-center"> - <strong class="gl-text-gray-900">{{ formattedDueDate }}</strong> - <span class="gl-mx-2">-</span> - <gl-button - variant="link" - class="gl-text-gray-500!" - data-testid="reset-button" - :disabled="loading" - @click="setDueDate(null)" - > - {{ $options.i18n.removeDueDate }} - </gl-button> - </div> - </template> - <gl-datepicker - ref="datePicker" - :value="parsedDueDate" - show-clear-button - @input="setDueDate" - @clear="setDueDate(null)" - /> - </board-editable-item> -</template> -<style> -/* - * This can be removed after closing: - * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1048 - */ -.board-sidebar-due-date .gl-datepicker, -.board-sidebar-due-date .gl-datepicker-input { - width: 100%; -} -</style> diff --git a/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql deleted file mode 100644 index bbea248cf85..00000000000 --- a/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql +++ /dev/null @@ -1,8 +0,0 @@ -mutation issueSetDueDate($input: UpdateIssueInput!) { - updateIssue(input: $input) { - issue { - dueDate - } - errors - } -} diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 694483e92ea..d4893f9eca7 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -35,7 +35,6 @@ import { import boardLabelsQuery from '../graphql/board_labels.query.graphql'; import groupProjectsQuery from '../graphql/group_projects.query.graphql'; import issueCreateMutation from '../graphql/issue_create.mutation.graphql'; -import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql'; import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql'; import listsIssuesQuery from '../graphql/lists_issues.query.graphql'; import * as types from './mutation_types'; @@ -559,30 +558,6 @@ export default { }); }, - setActiveIssueDueDate: async ({ commit, getters }, input) => { - const { activeBoardItem } = getters; - const { data } = await gqlClient.mutate({ - mutation: issueSetDueDateMutation, - variables: { - input: { - iid: String(activeBoardItem.iid), - projectPath: input.projectPath, - dueDate: input.dueDate, - }, - }, - }); - - if (data.updateIssue?.errors?.length > 0) { - throw new Error(data.updateIssue.errors); - } - - commit(types.UPDATE_BOARD_ITEM_BY_ID, { - itemId: activeBoardItem.id, - prop: 'dueDate', - value: data.updateIssue.issue.dueDate, - }); - }, - setActiveItemSubscribed: async ({ commit, getters, state }, input) => { const { activeBoardItem, isEpicBoard } = getters; const { fullPath, issuableType } = state; diff --git a/app/assets/javascripts/nav/utils/has_menu_expanded.js b/app/assets/javascripts/nav/utils/has_menu_expanded.js index 4e4d6c7c71e..5f126bbdf76 100644 --- a/app/assets/javascripts/nav/utils/has_menu_expanded.js +++ b/app/assets/javascripts/nav/utils/has_menu_expanded.js @@ -1,5 +1,2 @@ -export const hasMenuExpanded = () => { - const header = document.querySelector('.header-content'); - - return Boolean(header?.classList.contains('menu-expanded')); -}; +export const hasMenuExpanded = () => + Boolean(document.querySelector('.header-content.menu-expanded')); diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue index 22dffb7d2db..ca5711de49c 100644 --- a/app/assets/javascripts/repository/components/table/index.vue +++ b/app/assets/javascripts/repository/components/table/index.vue @@ -51,6 +51,9 @@ export default { }; }, computed: { + totalEntries() { + return Object.values(this.entries).flat().length; + }, tableCaption() { if (this.isLoading) { return sprintf( @@ -111,6 +114,7 @@ export default { :submodule-tree-url="entry.treeUrl" :lfs-oid="entry.lfsOid" :loading-path="loadingPath" + :total-entries="totalEntries" /> </template> <template v-if="isLoading"> diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue index 1b7fb6a63df..62f863db871 100644 --- a/app/assets/javascripts/repository/components/table/row.vue +++ b/app/assets/javascripts/repository/components/table/row.vue @@ -43,12 +43,17 @@ export default { type: this.type, path: this.currentPath, projectPath: this.projectPath, + maxOffset: this.totalEntries, }; }, }, }, mixins: [getRefMixin, glFeatureFlagMixin()], props: { + totalEntries: { + type: Number, + required: true, + }, id: { type: String, required: true, diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js index 4a4b9d115b7..4892e54ebef 100644 --- a/app/assets/javascripts/repository/graphql.js +++ b/app/assets/javascripts/repository/graphql.js @@ -17,15 +17,21 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({ const defaultClient = createDefaultClient( { Query: { - commit(_, { path, fileName, type }) { + commit(_, { path, fileName, type, maxOffset }) { return new Promise((resolve) => { - fetchLogsTree(defaultClient, path, '0', { - resolve, - entry: { - name: fileName, - type, + fetchLogsTree( + defaultClient, + path, + '0', + { + resolve, + entry: { + name: fileName, + type, + }, }, - }); + maxOffset, + ); }); }, readme(_, { url }) { diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js index 9001bcd8fc3..7d9d962b6f4 100644 --- a/app/assets/javascripts/repository/log_tree.js +++ b/app/assets/javascripts/repository/log_tree.js @@ -7,6 +7,7 @@ import refQuery from './queries/ref.query.graphql'; const fetchpromises = {}; const resolvers = {}; +let maxOffset; export function resolveCommit(commits, path, { resolve, entry }) { const commit = commits.find( @@ -18,7 +19,15 @@ export function resolveCommit(commits, path, { resolve, entry }) { } } -export function fetchLogsTree(client, path, offset, resolver = null) { +export function fetchLogsTree(client, path, offset, resolver = null, _maxOffset = null) { + if (_maxOffset) { + maxOffset = _maxOffset; + } + + if (Number(offset) > maxOffset) { + return Promise.resolve(); + } + if (resolver) { if (!resolvers[path]) { resolvers[path] = [resolver]; @@ -60,6 +69,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) { fetchLogsTree(client, path, headerLogsOffset); } else { delete resolvers[path]; + maxOffset = null; } }); diff --git a/app/assets/javascripts/repository/queries/commit.query.graphql b/app/assets/javascripts/repository/queries/commit.query.graphql index e4aeaaff8fe..7ae4a3b984a 100644 --- a/app/assets/javascripts/repository/queries/commit.query.graphql +++ b/app/assets/javascripts/repository/queries/commit.query.graphql @@ -1,7 +1,7 @@ #import "ee_else_ce/repository/queries/commit.fragment.graphql" -query getCommit($fileName: String!, $type: String!, $path: String!) { - commit(path: $path, fileName: $fileName, type: $type) @client { +query getCommit($fileName: String!, $type: String!, $path: String!, $maxOffset: Number!) { + commit(path: $path, fileName: $fileName, type: $type, maxOffset: $maxOffset) @client { ...TreeEntryCommit } } diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue index 6a68e914b84..c3dfa5f8b14 100644 --- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue +++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue @@ -112,6 +112,9 @@ export default { dateValue() { return this.issuable?.[this.dateType] || null; }, + firstDay() { + return gon.first_day_of_week; + }, isLoading() { return this.$apollo.queries.issuable.loading || this.loading; }, @@ -286,6 +289,7 @@ export default { ref="datePicker" class="gl-relative" :default-date="parsedDate" + :first-day="firstDay" show-clear-button autocomplete="off" @input="setDate" diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 1be0d626b19..2d28a81f462 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -24,8 +24,15 @@ class ContainerRepository < ApplicationRecord scope :for_group_and_its_subgroups, ->(group) do project_scope = Project .for_group_and_its_subgroups(group) - .with_container_registry - .select(:id) + + project_scope = + if Feature.enabled?(:read_container_registry_access_level, group, default_enabled: :yaml) + project_scope.with_feature_enabled(:container_registry) + else + project_scope.with_container_registry + end + + project_scope = project_scope.select(:id) joins("INNER JOIN (#{project_scope.to_sql}) projects on projects.id=container_repositories.project_id") end diff --git a/app/models/project.rb b/app/models/project.rb index 3af1e671f1c..6895bba7cf7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2612,6 +2612,15 @@ class Project < ApplicationRecord !!read_attribute(:merge_requests_author_approval) end + def container_registry_enabled + if Feature.enabled?(:read_container_registry_access_level, self.namespace, default_enabled: :yaml) + project_feature.container_registry_enabled? + else + read_attribute(:container_registry_enabled) + end + end + alias_method :container_registry_enabled?, :container_registry_enabled + private def set_container_registry_access_level diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index eb4ad327438..f6e889396c6 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -24,7 +24,11 @@ class ProjectFeature < ApplicationRecord set_available_features(FEATURES) - PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER, metrics_dashboard: Gitlab::Access::REPORTER }.freeze + PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { + merge_requests: Gitlab::Access::REPORTER, + metrics_dashboard: Gitlab::Access::REPORTER, + container_registry: Gitlab::Access::REPORTER + }.freeze PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze class << self @@ -92,7 +96,7 @@ class ProjectFeature < ApplicationRecord def set_container_registry_access_level self.container_registry_access_level = - if project&.container_registry_enabled + if project&.read_attribute(:container_registry_enabled) ENABLED else DISABLED diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 164d73fe332..4dfdbd87a34 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -51,7 +51,11 @@ class ProjectPolicy < BasePolicy desc "Container registry is disabled" condition(:container_registry_disabled, scope: :subject) do - !project.container_registry_enabled + if ::Feature.enabled?(:read_container_registry_access_level, @subject&.namespace, default_enabled: :yaml) + !access_allowed_to?(:container_registry) + else + !project.container_registry_enabled + end end desc "Project has an external wiki" diff --git a/app/services/github.rb b/app/services/github.rb deleted file mode 100644 index e76e7351ab9..00000000000 --- a/app/services/github.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -module Github -end diff --git a/app/workers/concerns/security_scans_queue.rb b/app/workers/concerns/security_scans_queue.rb index f731317bb37..27e97169926 100644 --- a/app/workers/concerns/security_scans_queue.rb +++ b/app/workers/concerns/security_scans_queue.rb @@ -8,6 +8,6 @@ module SecurityScansQueue included do queue_namespace :security_scans - feature_category :static_application_security_testing + feature_category :vulnerability_management end end diff --git a/config/feature_flags/development/read_container_registry_access_level.yml b/config/feature_flags/development/read_container_registry_access_level.yml new file mode 100644 index 00000000000..9f4a223a169 --- /dev/null +++ b/config/feature_flags/development/read_container_registry_access_level.yml @@ -0,0 +1,8 @@ +--- +name: read_container_registry_access_level +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55071 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332751 +milestone: '14.0' +type: development +group: group::package +default_enabled: false diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 557fbd0a107..c9b056ce956 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -505,7 +505,7 @@ production: &base ee_cron_jobs: # Schedule snapshots for all devops adoption segments analytics_devops_adoption_create_all_snapshots_worker: - cron: 0 4 * * 0 + cron: 0 0 1 * * # Snapshot active users statistics historical_data_worker: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index d589bc6069a..8f4c6492cad 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -586,7 +586,7 @@ end Gitlab.ee do Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({}) - Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker']['cron'] ||= '0 4 * * 0' + Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker']['cron'] ||= '0 0 1 * *' Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker']['job_class'] = 'Analytics::DevopsAdoption::CreateAllSnapshotsWorker' Settings.cron_jobs['active_user_count_threshold_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['active_user_count_threshold_worker']['cron'] ||= '0 12 * * *' diff --git a/config/metrics/settings/20210204124908_mattermost_enabled.yml b/config/metrics/settings/20210204124908_mattermost_enabled.yml index a3c88cf976e..14303726d1f 100644 --- a/config/metrics/settings/20210204124908_mattermost_enabled.yml +++ b/config/metrics/settings/20210204124908_mattermost_enabled.yml @@ -1,16 +1,18 @@ --- key_path: mattermost_enabled description: Whether Mattermost is enabled -product_section: growth -product_stage: growth -product_group: group::product intelligence -product_category: collection +product_section: dev +product_stage: create +product_group: group::ecosystem +product_category: integrations value_type: boolean status: data_available time_frame: none data_source: system distribution: - ce +- ee tier: - free -skip_validation: true +- premium +- ultimate diff --git a/db/post_migrate/20210606143426_add_index_for_container_registry_access_level.rb b/db/post_migrate/20210606143426_add_index_for_container_registry_access_level.rb new file mode 100644 index 00000000000..64d37054eb8 --- /dev/null +++ b/db/post_migrate/20210606143426_add_index_for_container_registry_access_level.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class AddIndexForContainerRegistryAccessLevel < ActiveRecord::Migration[6.1] + include Gitlab::Database::SchemaHelpers + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + INDEX = 'index_project_features_on_project_id_include_container_registry' + + def up + if index_exists_by_name?('project_features', INDEX) + Gitlab::AppLogger.warn "Index not created because it already exists (this may be due to an aborted migration or similar): table_name: project_features, index_name: #{INDEX}" + return + end + + begin + disable_statement_timeout do + execute "CREATE UNIQUE INDEX CONCURRENTLY #{INDEX} ON project_features " \ + 'USING btree (project_id) INCLUDE (container_registry_access_level)' + end + rescue ActiveRecord::StatementInvalid => ex + raise "The index #{INDEX} couldn't be added: #{ex.message}" + end + + create_comment( + 'INDEX', + INDEX, + 'Included column (container_registry_access_level) improves performance of the ContainerRepository.for_group_and_its_subgroups scope query' + ) + end + + def down + remove_concurrent_index_by_name('project_features', INDEX) + end +end diff --git a/db/schema_migrations/20210606143426 b/db/schema_migrations/20210606143426 new file mode 100644 index 00000000000..a8a2d7d784c --- /dev/null +++ b/db/schema_migrations/20210606143426 @@ -0,0 +1 @@ +1f99d446428ddac2a0fa7d64bdce9fc300bf02e88c35cdb3d726c501641e721d
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 49563bdb895..7220869c5ea 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -24144,6 +24144,10 @@ CREATE UNIQUE INDEX index_project_features_on_project_id ON project_features USI CREATE INDEX index_project_features_on_project_id_bal_20 ON project_features USING btree (project_id) WHERE (builds_access_level = 20); +CREATE UNIQUE INDEX index_project_features_on_project_id_include_container_registry ON project_features USING btree (project_id) INCLUDE (container_registry_access_level); + +COMMENT ON INDEX index_project_features_on_project_id_include_container_registry IS 'Included column (container_registry_access_level) improves performance of the ContainerRepository.for_group_and_its_subgroups scope query'; + CREATE INDEX index_project_features_on_project_id_ral_20 ON project_features USING btree (project_id) WHERE (repository_access_level = 20); CREATE INDEX index_project_group_links_on_group_id ON project_group_links USING btree (group_id); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 4bf40e1c69a..be544331e8b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -5033,6 +5033,29 @@ The edge type for [`DevopsAdoptionEnabledNamespace`](#devopsadoptionenablednames | <a id="devopsadoptionenablednamespaceedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="devopsadoptionenablednamespaceedgenode"></a>`node` | [`DevopsAdoptionEnabledNamespace`](#devopsadoptionenablednamespace) | The item at the end of the edge. | +#### `DevopsAdoptionSnapshotConnection` + +The connection type for [`DevopsAdoptionSnapshot`](#devopsadoptionsnapshot). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="devopsadoptionsnapshotconnectionedges"></a>`edges` | [`[DevopsAdoptionSnapshotEdge]`](#devopsadoptionsnapshotedge) | A list of edges. | +| <a id="devopsadoptionsnapshotconnectionnodes"></a>`nodes` | [`[DevopsAdoptionSnapshot]`](#devopsadoptionsnapshot) | A list of nodes. | +| <a id="devopsadoptionsnapshotconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `DevopsAdoptionSnapshotEdge` + +The edge type for [`DevopsAdoptionSnapshot`](#devopsadoptionsnapshot). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="devopsadoptionsnapshotedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | +| <a id="devopsadoptionsnapshotedgenode"></a>`node` | [`DevopsAdoptionSnapshot`](#devopsadoptionsnapshot) | The item at the end of the edge. | + #### `DiscussionConnection` The connection type for [`Discussion`](#discussion). @@ -8191,9 +8214,28 @@ Enabled namespace for DevopsAdoption. | ---- | ---- | ----------- | | <a id="devopsadoptionenablednamespacedisplaynamespace"></a>`displayNamespace` | [`Namespace`](#namespace) | Namespace where data should be displayed. | | <a id="devopsadoptionenablednamespaceid"></a>`id` | [`ID!`](#id) | ID of the enabled namespace. | -| <a id="devopsadoptionenablednamespacelatestsnapshot"></a>`latestSnapshot` | [`DevopsAdoptionSnapshot`](#devopsadoptionsnapshot) | The latest adoption metrics for the enabled namespace. | +| <a id="devopsadoptionenablednamespacelatestsnapshot"></a>`latestSnapshot` | [`DevopsAdoptionSnapshot`](#devopsadoptionsnapshot) | Metrics snapshot for previous month for the enabled namespace. | | <a id="devopsadoptionenablednamespacenamespace"></a>`namespace` | [`Namespace`](#namespace) | Namespace which should be calculated. | +#### Fields with arguments + +##### `DevopsAdoptionEnabledNamespace.snapshots` + +Data snapshots of the namespace. + +Returns [`DevopsAdoptionSnapshotConnection`](#devopsadoptionsnapshotconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="devopsadoptionenablednamespacesnapshotsendtimeafter"></a>`endTimeAfter` | [`Time`](#time) | Filter to snapshots with month end after the provided date. | +| <a id="devopsadoptionenablednamespacesnapshotsendtimebefore"></a>`endTimeBefore` | [`Time`](#time) | Filter to snapshots with month end before the provided date. | + ### `DevopsAdoptionSnapshot` Snapshot. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index f4275914b0b..ae59c35318c 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1518,7 +1518,8 @@ job: Glob patterns are interpreted with Ruby [`File.fnmatch`](https://docs.ruby-lang.org/en/2.7.0/File.html#method-c-fnmatch) with the flags `File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB`. -For performance reasons, GitLab matches a maximum of 10,000 `exists` patterns. After the 10,000th check, rules with patterned globs always match. +For performance reasons, GitLab matches a maximum of 10,000 `exists` patterns or file paths. After the 10,000th check, rules with patterned globs always match. +In other words, the `exists` rule always assumes a match in projects with more than 10,000 files. #### `rules:allow_failure` diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md index fb1e61642d7..31621a4bb0c 100644 --- a/doc/development/background_migrations.md +++ b/doc/development/background_migrations.md @@ -7,28 +7,25 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo # Background migrations -Background migrations can be used to perform data migrations that would -otherwise take a very long time (hours, days, years, etc) to complete. For -example, you can use background migrations to migrate data so that instead of -storing data in a single JSON column the data is stored in a separate table. +Background migrations should be used to perform data migrations whenever a +migration exceeds [the time limits in our guidelines](database_review.md#timing-guidelines-for-migrations). For example, you can use background +migrations to migrate data that's stored in a single JSON column +to a separate table instead. If the database cluster is considered to be in an unhealthy state, background migrations automatically reschedule themselves for a later point in time. ## When To Use Background Migrations -In the vast majority of cases you will want to use a regular Rails migration -instead. Background migrations should be used when migrating _data_ in -tables that have so many rows this process would take hours when performed in a -regular Rails migration. +You should use a background migration when you migrate _data_ in tables that have +so many rows that the process would exceed [the time limits in our guidelines](database_review.md#timing-guidelines-for-migrations) if performed using a regular Rails migration. -Background migrations _may_ also be used when executing numerous single-row queries +- Background migrations should be used when migrating data in [high-traffic tables](migration_style_guide.md#high-traffic-tables). +- Background migrations may also be used when executing numerous single-row queries for every item on a large dataset. Typically, for single-record patterns, runtime is largely dependent on the size of the dataset, hence it should be split accordingly and put into background migrations. - -Background migrations _may not_ be used to perform schema migrations, they -should only be used for data migrations. +- Background migrations should not be used to perform schema migrations. Some examples where background migrations can be useful: diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index ea9d43ace8c..aae62801b62 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -7348,11 +7348,11 @@ Whether Mattermost is enabled [YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/settings/20210204124908_mattermost_enabled.yml) -Group: `group::product intelligence` +Group: `group::ecosystem` Status: `data_available` -Tiers: `free` +Tiers: `free`, `premium`, `ultimate` ### `object_store.artifacts.enabled` diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index fabad33f52a..5bfbac4270f 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -43,7 +43,6 @@ module API expose :visibility expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :resolve_outdated_diff_discussions - expose :container_registry_enabled expose :container_expiration_policy, using: Entities::ContainerExpirationPolicy, if: -> (project, _) { project.container_expiration_policy } @@ -54,6 +53,13 @@ module API expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } + expose(:container_registry_enabled) do |project, options| + if ::Feature.enabled?(:read_container_registry_access_level, project.namespace, default_enabled: :yaml) + project.feature_available?(:container_registry, options[:current_user]) + else + project.read_attribute(:container_registry_enabled) + end + end expose :service_desk_enabled expose :service_desk_address diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5d197a876bd..24abd92df62 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3450,9 +3450,6 @@ msgstr "" msgid "An error occurred when toggling the notification subscription" msgstr "" -msgid "An error occurred when updating the issue due date" -msgstr "" - msgid "An error occurred when updating the issue weight" msgstr "" @@ -11294,7 +11291,7 @@ msgstr "" msgid "DevopsAdoption|DevOps adoption tracks the use of key features across your favorite groups. Add a group to the table to begin." msgstr "" -msgid "DevopsAdoption|Feature adoption is based on usage in the current calendar month. Last updated: %{timestamp}." +msgid "DevopsAdoption|Feature adoption is based on usage in the previous calendar month. Last updated: %{timestamp}." msgstr "" msgid "DevopsAdoption|Filter by name" diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml index 29c8e5dc2ba..bb4d5392b3b 100644 --- a/scripts/review_apps/base-config.yaml +++ b/scripts/review_apps/base-config.yaml @@ -5,9 +5,9 @@ global: ingress: annotations: external-dns.alpha.kubernetes.io/ttl: 10 - cert-manager.io/cluster-issuer: review-apps-route53-dns01-wildcard-cluster-issuer - kubernetes.io/tls-acme: true configureCertmanager: false + tls: + secretName: review-apps-tls initialRootPassword: secret: shared-gitlab-initial-root-password certmanager: diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 78a62cf0a29..6fb83e79f7f 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -161,6 +161,15 @@ function ensure_namespace() { kubectl describe namespace "${namespace}" || kubectl create namespace "${namespace}" } +function label_namespace() { + local namespace="${1}" + local label="${2}" + + echoinfo "Labeling the ${namespace} namespace with ${label}" true + + kubectl label namespace "${namespace}" "${label}" +} + function install_external_dns() { local namespace="${KUBE_NAMESPACE}" local release="dns-gitlab-review-app-helm3" @@ -302,6 +311,7 @@ function deploy() { gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-ee" ensure_namespace "${namespace}" + label_namespace "${namespace}" "tls=review-apps-tls" # label namespace for kubed to sync tls create_application_secret @@ -319,9 +329,6 @@ HELM_CMD=$(cat << EOF --set releaseOverride="${release}" \ --set global.hosts.hostSuffix="${HOST_SUFFIX}" \ --set global.hosts.domain="${REVIEW_APPS_DOMAIN}" \ - --set gitlab.webservice.ingress.tls.secretName="${release}-gitlab-tls" \ - --set registry.ingress.tls.secretName="${release}-registry-tls" \ - --set minio.ingress.tls.secretName="${release}-minio-tls" \ --set gitlab.migrations.image.repository="${gitlab_migrations_image_repository}" \ --set gitlab.migrations.image.tag="${CI_COMMIT_REF_SLUG}" \ --set gitlab.gitaly.image.repository="${gitlab_gitaly_image_repository}" \ diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js index e97bdba5fea..10d739c65f5 100644 --- a/spec/frontend/boards/components/board_content_sidebar_spec.js +++ b/spec/frontend/boards/components/board_content_sidebar_spec.js @@ -4,10 +4,10 @@ import Vuex from 'vuex'; import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue'; import { stubComponent } from 'helpers/stub_component'; import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; -import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import { ISSUABLE } from '~/boards/constants'; +import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue'; import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data'; @@ -109,8 +109,8 @@ describe('BoardContentSidebar', () => { expect(wrapper.findComponent(BoardSidebarTitle).exists()).toBe(true); }); - it('renders BoardSidebarDueDate', () => { - expect(wrapper.findComponent(BoardSidebarDueDate).exists()).toBe(true); + it('renders SidebarDateWidget', () => { + expect(wrapper.findComponent(SidebarDateWidget).exists()).toBe(true); }); it('renders BoardSidebarSubscription', () => { diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js deleted file mode 100644 index 2f91beda275..00000000000 --- a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js +++ /dev/null @@ -1,135 +0,0 @@ -import { GlDatepicker } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; -import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; -import { createStore } from '~/boards/stores'; - -const TEST_DUE_DATE = '2020-02-20'; -const TEST_FORMATTED_DUE_DATE = 'Feb 20, 2020'; -const TEST_PARSED_DATE = new Date(2020, 1, 20); -const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, dueDate: null, referencePath: 'h/b#2' }; - -describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => { - let wrapper; - let store; - - afterEach(() => { - wrapper.destroy(); - store = null; - wrapper = null; - }); - - const createWrapper = ({ dueDate = null } = {}) => { - store = createStore(); - store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } }; - store.state.activeId = TEST_ISSUE.id; - - wrapper = shallowMount(BoardSidebarDueDate, { - store, - provide: { - canUpdate: true, - }, - stubs: { - 'board-editable-item': BoardEditableItem, - }, - }); - }; - - const findDatePicker = () => wrapper.find(GlDatepicker); - const findResetButton = () => wrapper.find('[data-testid="reset-button"]'); - const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); - - it('renders "None" when no due date is set', () => { - createWrapper(); - - expect(findCollapsed().text()).toBe('None'); - expect(findResetButton().exists()).toBe(false); - }); - - it('renders formatted due date with reset button when set', () => { - createWrapper({ dueDate: TEST_DUE_DATE }); - - expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE); - expect(findResetButton().exists()).toBe(true); - }); - - describe('when due date is submitted', () => { - beforeEach(async () => { - createWrapper(); - - jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { - store.state.boardItems[TEST_ISSUE.id].dueDate = TEST_DUE_DATE; - }); - findDatePicker().vm.$emit('input', TEST_PARSED_DATE); - await wrapper.vm.$nextTick(); - }); - - it('collapses sidebar and renders formatted due date with reset button', () => { - expect(findCollapsed().isVisible()).toBe(true); - expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE); - expect(findResetButton().exists()).toBe(true); - }); - - it('commits change to the server', () => { - expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalledWith({ - dueDate: TEST_DUE_DATE, - projectPath: 'h/b', - }); - }); - }); - - describe('when due date is cleared', () => { - beforeEach(async () => { - createWrapper(); - - jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { - store.state.boardItems[TEST_ISSUE.id].dueDate = null; - }); - findDatePicker().vm.$emit('clear'); - await wrapper.vm.$nextTick(); - }); - - it('collapses sidebar and renders "None"', () => { - expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled(); - expect(findCollapsed().isVisible()).toBe(true); - expect(findCollapsed().text()).toBe('None'); - }); - }); - - describe('when due date is resetted', () => { - beforeEach(async () => { - createWrapper({ dueDate: TEST_DUE_DATE }); - - jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { - store.state.boardItems[TEST_ISSUE.id].dueDate = null; - }); - findResetButton().vm.$emit('click'); - await wrapper.vm.$nextTick(); - }); - - it('collapses sidebar and renders "None"', () => { - expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled(); - expect(findCollapsed().isVisible()).toBe(true); - expect(findCollapsed().text()).toBe('None'); - }); - }); - - describe('when the mutation fails', () => { - beforeEach(async () => { - createWrapper({ dueDate: TEST_DUE_DATE }); - - jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => { - throw new Error(['failed mutation']); - }); - jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {}); - findDatePicker().vm.$emit('input', 'Invalid date'); - await wrapper.vm.$nextTick(); - }); - - it('collapses sidebar and renders former issue due date', () => { - expect(findCollapsed().isVisible()).toBe(true); - expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE); - expect(wrapper.vm.setError).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 63569333408..b28412f2127 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1386,57 +1386,6 @@ describe('setActiveIssueLabels', () => { }); }); -describe('setActiveIssueDueDate', () => { - const state = { boardItems: { [mockIssue.id]: mockIssue } }; - const getters = { activeBoardItem: mockIssue }; - const testDueDate = '2020-02-20'; - const input = { - dueDate: testDueDate, - projectPath: 'h/b', - }; - - it('should commit due date after setting the issue', (done) => { - jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ - data: { - updateIssue: { - issue: { - dueDate: testDueDate, - }, - errors: [], - }, - }, - }); - - const payload = { - itemId: getters.activeBoardItem.id, - prop: 'dueDate', - value: testDueDate, - }; - - testAction( - actions.setActiveIssueDueDate, - input, - { ...state, ...getters }, - [ - { - type: types.UPDATE_BOARD_ITEM_BY_ID, - payload, - }, - ], - [], - done, - ); - }); - - it('throws error if fails', async () => { - jest - .spyOn(gqlClient, 'mutate') - .mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } }); - - await expect(actions.setActiveIssueDueDate({ getters }, input)).rejects.toThrow(Error); - }); -}); - describe('setActiveItemSubscribed', () => { const state = { boardItems: { diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js index 4d7f053e43b..7221ea2c5cd 100644 --- a/spec/frontend/nav/components/responsive_app_spec.js +++ b/spec/frontend/nav/components/responsive_app_spec.js @@ -8,6 +8,11 @@ import { resetMenuItemsActive } from '~/nav/utils/reset_menu_items_active'; import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue'; import { TEST_NAV_DATA } from '../mock_data'; +const HTML_HEADER_CONTENT = '<div class="header-content"></div>'; +const HTML_MENU_EXPANDED = '<div class="menu-expanded"></div>'; +const HTML_HEADER_WITH_MENU_EXPANDED = + '<div></div><div class="header-content menu-expanded"></div>'; + describe('~/nav/components/responsive_app.vue', () => { let wrapper; @@ -53,11 +58,11 @@ describe('~/nav/components/responsive_app.vue', () => { }); it.each` - bodyHtml | expectation - ${''} | ${false} - ${'<div class="header-content"></div>'} | ${false} - ${'<div class="menu-expanded"></div>'} | ${false} - ${'<div></div><div class="header-content menu-expanded"></div>}'} | ${true} + bodyHtml | expectation + ${''} | ${false} + ${HTML_HEADER_CONTENT} | ${false} + ${HTML_MENU_EXPANDED} | ${false} + ${HTML_HEADER_WITH_MENU_EXPANDED} | ${true} `( 'with responsive toggle event and html set to $bodyHtml, responsive open = $expectation', ({ bodyHtml, expectation }) => { @@ -93,7 +98,7 @@ describe('~/nav/components/responsive_app.vue', () => { describe('with menu expanded in body', () => { beforeEach(() => { - document.body.innerHTML = '<div></div><div class="header-content menu-expanded"></div>'; + document.body.innerHTML = HTML_HEADER_WITH_MENU_EXPANDED; createComponent(); }); diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js index 254e2fc07b4..da28c9873d9 100644 --- a/spec/frontend/repository/components/table/row_spec.js +++ b/spec/frontend/repository/components/table/row_spec.js @@ -19,6 +19,7 @@ function factory(propsData = {}) { name: propsData.path, projectPath: 'gitlab-org/gitlab-ce', url: `https://test.com`, + totalEntries: 10, }, directives: { GlHoverLoad: createMockDirective(), diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js index a842053caad..d338af88ce3 100644 --- a/spec/frontend/repository/log_tree_spec.js +++ b/spec/frontend/repository/log_tree_spec.js @@ -69,6 +69,11 @@ describe('fetchLogsTree', () => { mock.restore(); }); + it('does not call axios get if offset is larger than the maximum offset', () => + fetchLogsTree(client, '', '1000', resolver, 900).then(() => { + expect(axios.get).not.toHaveBeenCalled(); + })); + it('calls axios get', () => fetchLogsTree(client, '', '0', resolver).then(() => { expect(axios.get).toHaveBeenCalledWith('/gitlab-org/gitlab-foss/-/refs/main/logs_tree/', { diff --git a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js index 91cbcc6cc27..619e89beb23 100644 --- a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js +++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js @@ -22,6 +22,10 @@ describe('Sidebar date Widget', () => { let fakeApollo; const date = '2021-04-15'; + window.gon = { + first_day_of_week: 1, + }; + const findEditableItem = () => wrapper.findComponent(SidebarEditableItem); const findPopoverIcon = () => wrapper.find('[data-testid="inherit-date-popover"]'); const findDatePicker = () => wrapper.find(GlDatepicker); @@ -119,11 +123,12 @@ describe('Sidebar date Widget', () => { expect(wrapper.emitted('dueDateUpdated')).toEqual([[date]]); }); - it('uses a correct prop to set the initial date for GlDatePicker', () => { + it('uses a correct prop to set the initial date and first day of the week for GlDatePicker', () => { expect(findDatePicker().props()).toMatchObject({ value: null, autocomplete: 'off', defaultDate: expect.any(Object), + firstDay: window.gon.first_day_of_week, }); }); diff --git a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb index 731dd5eca23..cc4760e69e5 100644 --- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do - let(:project) { build(:project) } + let_it_be(:project) { create(:project) } + let(:user) { project.owner } let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb index 88f2df6cd84..6817f0e6ed6 100644 --- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe Sidebars::Projects::Menus::SettingsMenu do - let(:project) { build(:project) } + let_it_be(:project) { create(:project) } + let(:user) { project.owner } let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb index fa19d512b8d..3232a559d0b 100644 --- a/spec/models/container_repository_spec.rb +++ b/spec/models/container_repository_spec.rb @@ -331,6 +331,40 @@ RSpec.describe ContainerRepository do it { is_expected.to eq([]) } end + + context 'with read_container_registry_access_level disabled' do + before do + stub_feature_flags(read_container_registry_access_level: false) + end + + context 'in a group' do + let(:test_group) { group } + + it { is_expected.to contain_exactly(repository) } + end + + context 'with a subgroup' do + let(:test_group) { create(:group) } + let(:another_project) { create(:project, path: 'test', group: test_group) } + + let(:another_repository) do + create(:container_repository, name: 'my_image', project: another_project) + end + + before do + group.parent = test_group + group.save! + end + + it { is_expected.to contain_exactly(repository, another_repository) } + end + + context 'group without container_repositories' do + let(:test_group) { create(:group) } + + it { is_expected.to eq([]) } + end + end end describe '.search_by_name' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 48353323fb2..0e6f662da50 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -653,6 +653,16 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) } it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) } it { is_expected.to delegate_method(:allow_editing_commit_messages?).to(:project_setting) } + it { is_expected.to delegate_method(:container_registry_enabled?).to(:project_feature) } + it { is_expected.to delegate_method(:container_registry_access_level).to(:project_feature) } + + context 'when read_container_registry_access_level is disabled' do + before do + stub_feature_flags(read_container_registry_access_level: false) + end + + it { is_expected.not_to delegate_method(:container_registry_enabled?).to(:project_feature) } + end end describe 'reference methods' do @@ -2285,35 +2295,55 @@ RSpec.describe Project, factory_default: :keep do it 'updates project_feature', :aggregate_failures do # Simulate an existing project that has container_registry enabled project.update_column(:container_registry_enabled, true) - project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED) - - expect(project.container_registry_enabled).to eq(true) - expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::DISABLED) + project.project_feature.update_column(:container_registry_access_level, ProjectFeature::ENABLED) project.update!(container_registry_enabled: false) - expect(project.container_registry_enabled).to eq(false) + expect(project.read_attribute(:container_registry_enabled)).to eq(false) expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::DISABLED) project.update!(container_registry_enabled: true) - expect(project.container_registry_enabled).to eq(true) + expect(project.read_attribute(:container_registry_enabled)).to eq(true) expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::ENABLED) end it 'rollsback both projects and project_features row in case of error', :aggregate_failures do project.update_column(:container_registry_enabled, true) - project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED) - - expect(project.container_registry_enabled).to eq(true) - expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::DISABLED) + project.project_feature.update_column(:container_registry_access_level, ProjectFeature::ENABLED) allow(project).to receive(:valid?).and_return(false) expect { project.update!(container_registry_enabled: false) }.to raise_error(ActiveRecord::RecordInvalid) - expect(project.reload.container_registry_enabled).to eq(true) - expect(project.project_feature.reload.container_registry_access_level).to eq(ProjectFeature::DISABLED) + expect(project.reload.read_attribute(:container_registry_enabled)).to eq(true) + expect(project.project_feature.reload.container_registry_access_level).to eq(ProjectFeature::ENABLED) + end + end + + describe '#container_registry_enabled' do + let_it_be_with_reload(:project) { create(:project) } + + it 'delegates to project_feature', :aggregate_failures do + project.update_column(:container_registry_enabled, true) + project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED) + + expect(project.container_registry_enabled).to eq(false) + expect(project.container_registry_enabled?).to eq(false) + end + + context 'with read_container_registry_access_level disabled' do + before do + stub_feature_flags(read_container_registry_access_level: false) + end + + it 'reads project.container_registry_enabled' do + project.update_column(:container_registry_enabled, true) + project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED) + + expect(project.container_registry_enabled).to eq(true) + expect(project.container_registry_enabled?).to eq(true) + end end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 53d9bdb01a3..2b4501a71a5 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -1434,4 +1434,165 @@ RSpec.describe ProjectPolicy do end end end + + describe 'container_image policies' do + using RSpec::Parameterized::TableSyntax + + let(:guest_operations_permissions) { [:read_container_image] } + + let(:developer_operations_permissions) do + guest_operations_permissions + [ + :create_container_image, :update_container_image, :destroy_container_image + ] + end + + let(:maintainer_operations_permissions) do + developer_operations_permissions + [ + :admin_container_image + ] + end + + where(:project_visibility, :access_level, :role, :allowed) do + :public | ProjectFeature::ENABLED | :maintainer | true + :public | ProjectFeature::ENABLED | :developer | true + :public | ProjectFeature::ENABLED | :reporter | true + :public | ProjectFeature::ENABLED | :guest | true + :public | ProjectFeature::ENABLED | :anonymous | true + :public | ProjectFeature::PRIVATE | :maintainer | true + :public | ProjectFeature::PRIVATE | :developer | true + :public | ProjectFeature::PRIVATE | :reporter | true + :public | ProjectFeature::PRIVATE | :guest | false + :public | ProjectFeature::PRIVATE | :anonymous | false + :public | ProjectFeature::DISABLED | :maintainer | false + :public | ProjectFeature::DISABLED | :developer | false + :public | ProjectFeature::DISABLED | :reporter | false + :public | ProjectFeature::DISABLED | :guest | false + :public | ProjectFeature::DISABLED | :anonymous | false + :internal | ProjectFeature::ENABLED | :maintainer | true + :internal | ProjectFeature::ENABLED | :developer | true + :internal | ProjectFeature::ENABLED | :reporter | true + :internal | ProjectFeature::ENABLED | :guest | true + :internal | ProjectFeature::ENABLED | :anonymous | false + :internal | ProjectFeature::PRIVATE | :maintainer | true + :internal | ProjectFeature::PRIVATE | :developer | true + :internal | ProjectFeature::PRIVATE | :reporter | true + :internal | ProjectFeature::PRIVATE | :guest | false + :internal | ProjectFeature::PRIVATE | :anonymous | false + :internal | ProjectFeature::DISABLED | :maintainer | false + :internal | ProjectFeature::DISABLED | :developer | false + :internal | ProjectFeature::DISABLED | :reporter | false + :internal | ProjectFeature::DISABLED | :guest | false + :internal | ProjectFeature::DISABLED | :anonymous | false + :private | ProjectFeature::ENABLED | :maintainer | true + :private | ProjectFeature::ENABLED | :developer | true + :private | ProjectFeature::ENABLED | :reporter | true + :private | ProjectFeature::ENABLED | :guest | false + :private | ProjectFeature::ENABLED | :anonymous | false + :private | ProjectFeature::PRIVATE | :maintainer | true + :private | ProjectFeature::PRIVATE | :developer | true + :private | ProjectFeature::PRIVATE | :reporter | true + :private | ProjectFeature::PRIVATE | :guest | false + :private | ProjectFeature::PRIVATE | :anonymous | false + :private | ProjectFeature::DISABLED | :maintainer | false + :private | ProjectFeature::DISABLED | :developer | false + :private | ProjectFeature::DISABLED | :reporter | false + :private | ProjectFeature::DISABLED | :guest | false + :private | ProjectFeature::DISABLED | :anonymous | false + end + + with_them do + let(:current_user) { send(role) } + let(:project) { send("#{project_visibility}_project") } + + it 'allows/disallows the abilities based on the container_registry feature access level' do + project.project_feature.update!(container_registry_access_level: access_level) + + if allowed + expect_allowed(*permissions_abilities(role)) + else + expect_disallowed(*permissions_abilities(role)) + end + end + + def permissions_abilities(role) + case role + when :maintainer + maintainer_operations_permissions + when :developer + developer_operations_permissions + when :reporter, :guest, :anonymous + guest_operations_permissions + else + raise "Unknown role #{role}" + end + end + end + + context 'with read_container_registry_access_level disabled' do + before do + stub_feature_flags(read_container_registry_access_level: false) + end + + where(:project_visibility, :container_registry_enabled, :role, :allowed) do + :public | true | :maintainer | true + :public | true | :developer | true + :public | true | :reporter | true + :public | true | :guest | true + :public | true | :anonymous | true + :public | false | :maintainer | false + :public | false | :developer | false + :public | false | :reporter | false + :public | false | :guest | false + :public | false | :anonymous | false + :internal | true | :maintainer | true + :internal | true | :developer | true + :internal | true | :reporter | true + :internal | true | :guest | true + :internal | true | :anonymous | false + :internal | false | :maintainer | false + :internal | false | :developer | false + :internal | false | :reporter | false + :internal | false | :guest | false + :internal | false | :anonymous | false + :private | true | :maintainer | true + :private | true | :developer | true + :private | true | :reporter | true + :private | true | :guest | false + :private | true | :anonymous | false + :private | false | :maintainer | false + :private | false | :developer | false + :private | false | :reporter | false + :private | false | :guest | false + :private | false | :anonymous | false + end + + with_them do + let(:current_user) { send(role) } + let(:project) { send("#{project_visibility}_project") } + + it 'allows/disallows the abilities based on container_registry_enabled' do + project.update_column(:container_registry_enabled, container_registry_enabled) + + if allowed + expect_allowed(*permissions_abilities(role)) + else + expect_disallowed(*permissions_abilities(role)) + end + end + + def permissions_abilities(role) + case role + when :maintainer + maintainer_operations_permissions + when :developer + developer_operations_permissions + when :reporter, :guest, :anonymous + guest_operations_permissions + else + raise "Unknown role #{role}" + end + end + end + end + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a1532ea464e..6ec48f88e93 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -184,6 +184,32 @@ RSpec.describe API::Projects do end end + it 'includes correct value of container_registry_enabled', :aggregate_failures do + project.update_column(:container_registry_enabled, true) + project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED) + + get api('/projects', user) + project_response = json_response.find { |p| p['id'] == project.id } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(project_response['container_registry_enabled']).to eq(false) + end + + it 'reads projects.container_registry_enabled when read_container_registry_access_level is disabled' do + stub_feature_flags(read_container_registry_access_level: false) + + project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED) + project.update_column(:container_registry_enabled, true) + + get api('/projects', user) + project_response = json_response.find { |p| p['id'] == project.id } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to be_an Array + expect(project_response['container_registry_enabled']).to eq(true) + end + it 'includes project topics' do get api('/projects', user) diff --git a/workhorse/internal/httprs/httprs_test.go b/workhorse/internal/httprs/httprs_test.go index 62279d895c9..e26d2d21215 100644 --- a/workhorse/internal/httprs/httprs_test.go +++ b/workhorse/internal/httprs/httprs_test.go @@ -53,6 +53,10 @@ func (f *fakeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { if err != nil { return nil, err } + if err := os.Remove(fw.tmp.Name()); err != nil { + return nil, err + } + if f.downgradeZeroToNoRange { // There are implementations that downgrades bytes=0- to a normal un-ranged GET if r.Header.Get("Range") == "bytes=0-" { @@ -79,6 +83,10 @@ func newRSFactory(flags int) RSFactory { if err != nil { return nil } + if err := os.Remove(tmp.Name()); err != nil { + return nil + } + for i := 0; i < SZ; i++ { tmp.WriteString(fmt.Sprintf("%04d", i)) } |