diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-12 21:11:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-12 21:11:09 +0300 |
commit | 60eaf3d90650086dedb6fd94d6169dc5ab1f8d1e (patch) | |
tree | ff943955f1c424787ba65ff7c1607c522c103115 /app | |
parent | 1c8734ca5c2981e62b9c1162851ed136de86bbbf (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/work_items/components/work_item_actions.vue | 55 | ||||
-rw-r--r-- | app/assets/javascripts/work_items/components/work_item_detail.vue | 79 | ||||
-rw-r--r-- | app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue | 15 | ||||
-rw-r--r-- | app/assets/javascripts/work_items/graphql/work_item.fragment.graphql | 2 | ||||
-rw-r--r-- | app/assets/javascripts/work_items/graphql/work_item_links.query.graphql | 1 | ||||
-rw-r--r-- | app/graphql/mutations/ci/job/retry.rb | 11 | ||||
-rw-r--r-- | app/graphql/types/ci/job_type.rb | 2 | ||||
-rw-r--r-- | app/graphql/types/ci/variable_input_type.rb | 13 | ||||
-rw-r--r-- | app/models/ci/build.rb | 11 | ||||
-rw-r--r-- | app/models/ci/job_artifact.rb | 3 | ||||
-rw-r--r-- | app/models/ci/processable.rb | 2 | ||||
-rw-r--r-- | app/models/container_repository.rb | 42 | ||||
-rw-r--r-- | app/services/ci/retry_job_service.rb | 12 |
13 files changed, 227 insertions, 21 deletions
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue index 77002eeaf55..2130eb4b729 100644 --- a/app/assets/javascripts/work_items/components/work_item_actions.vue +++ b/app/assets/javascripts/work_items/components/work_item_actions.vue @@ -1,15 +1,24 @@ <script> -import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui'; +import { + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + GlModal, + GlModalDirective, +} from '@gitlab/ui'; import { s__ } from '~/locale'; import Tracking from '~/tracking'; export default { i18n: { deleteTask: s__('WorkItem|Delete task'), + enableTaskConfidentiality: s__('WorkItem|Turn on confidentiality'), + disableTaskConfidentiality: s__('WorkItem|Turn off confidentiality'), }, components: { GlDropdown, GlDropdownItem, + GlDropdownDivider, GlModal, }, directives: { @@ -22,14 +31,33 @@ export default { required: false, default: null, }, + canUpdate: { + type: Boolean, + required: false, + default: false, + }, canDelete: { type: Boolean, required: false, default: false, }, + isConfidential: { + type: Boolean, + required: false, + default: false, + }, + isParentConfidential: { + type: Boolean, + required: false, + default: false, + }, }, - emits: ['deleteWorkItem'], + emits: ['deleteWorkItem', 'toggleWorkItemConfidentiality'], methods: { + handleToggleWorkItemConfidentiality() { + this.track('click_toggle_work_item_confidentiality'); + this.$emit('toggleWorkItemConfidentiality', !this.isConfidential); + }, handleDeleteWorkItem() { this.track('click_delete_work_item'); this.$emit('deleteWorkItem'); @@ -44,7 +72,7 @@ export default { </script> <template> - <div v-if="canDelete"> + <div> <gl-dropdown icon="ellipsis_v" text-sr-only @@ -53,9 +81,24 @@ export default { no-caret right > - <gl-dropdown-item v-gl-modal="'work-item-confirm-delete'">{{ - $options.i18n.deleteTask - }}</gl-dropdown-item> + <template v-if="canUpdate && !isParentConfidential"> + <gl-dropdown-item + data-testid="confidentiality-toggle-action" + @click="handleToggleWorkItemConfidentiality" + >{{ + isConfidential + ? $options.i18n.disableTaskConfidentiality + : $options.i18n.enableTaskConfidentiality + }}</gl-dropdown-item + > + <gl-dropdown-divider /> + </template> + <gl-dropdown-item + v-if="canDelete" + v-gl-modal="'work-item-confirm-delete'" + data-testid="delete-action" + >{{ $options.i18n.deleteTask }}</gl-dropdown-item + > </gl-dropdown> <gl-modal modal-id="work-item-confirm-delete" diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 23e2842743a..44c9ef9e44b 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -1,5 +1,13 @@ <script> -import { GlAlert, GlSkeletonLoader, GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { + GlAlert, + GlSkeletonLoader, + GlLoadingIcon, + GlIcon, + GlBadge, + GlButton, + GlTooltipDirective, +} from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import { @@ -11,8 +19,12 @@ import { WIDGET_TYPE_HIERARCHY, WORK_ITEM_VIEWED_STORAGE_KEY, } from '../constants'; + import workItemQuery from '../graphql/work_item.query.graphql'; import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql'; +import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; +import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql'; + import WorkItemActions from './work_item_actions.vue'; import WorkItemState from './work_item_state.vue'; import WorkItemTitle from './work_item_title.vue'; @@ -29,7 +41,9 @@ export default { }, components: { GlAlert, + GlBadge, GlButton, + GlLoadingIcon, GlSkeletonLoader, GlIcon, WorkItemAssignees, @@ -65,6 +79,7 @@ export default { error: undefined, workItem: {}, showInfoBanner: true, + updateInProgress: false, }; }, apollo: { @@ -125,6 +140,9 @@ export default { parentWorkItem() { return this.workItemHierarchy?.parent; }, + parentWorkItemConfidentiality() { + return this.parentWorkItem?.confidential; + }, parentUrl() { return `../../issues/${this.parentWorkItem?.iid}`; }, @@ -138,6 +156,54 @@ export default { dismissBanner() { this.showInfoBanner = false; }, + toggleConfidentiality(confidentialStatus) { + this.updateInProgress = true; + let updateMutation = updateWorkItemMutation; + let inputVariables = { + id: this.workItemId, + confidential: confidentialStatus, + }; + + if (this.parentWorkItem) { + updateMutation = updateWorkItemTaskMutation; + inputVariables = { + id: this.parentWorkItem.id, + taskData: { + id: this.workItemId, + confidential: confidentialStatus, + }, + }; + } + + this.$apollo + .mutate({ + mutation: updateMutation, + variables: { + input: inputVariables, + }, + }) + .then( + ({ + data: { + workItemUpdate: { errors, workItem, task }, + }, + }) => { + if (errors?.length) { + throw new Error(errors[0]); + } + + this.$emit('workItemUpdated', { + confidential: workItem?.confidential || task?.confidential, + }); + }, + ) + .catch((error) => { + this.error = error.message; + }) + .finally(() => { + this.updateInProgress = false; + }); + }, }, WORK_ITEM_VIEWED_STORAGE_KEY, }; @@ -156,7 +222,7 @@ export default { </gl-skeleton-loader> </div> <template v-else> - <div class="gl-display-flex gl-align-items-center"> + <div class="gl-display-flex gl-align-items-center" data-testid="work-item-body"> <ul v-if="parentWorkItem" class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0" @@ -187,10 +253,19 @@ export default { data-testid="work-item-type" >{{ workItemType }}</span > + <gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" /> + <gl-badge v-if="workItem.confidential" variant="warning" icon="eye-slash" class="gl-mr-3">{{ + __('Confidential') + }}</gl-badge> <work-item-actions + v-if="canUpdate || canDelete" :work-item-id="workItem.id" :can-delete="canDelete" + :can-update="canUpdate" + :is-confidential="workItem.confidential" + :is-parent-confidential="parentWorkItemConfidentiality" @deleteWorkItem="$emit('deleteWorkItem')" + @toggleWorkItemConfidentiality="toggleConfidentiality" @error="error = $event" /> <gl-button diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index aa9cd2bbe4b..372bf7ec749 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlBadge, GlIcon, GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlBadge, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui'; import { produce } from 'immer'; import { s__ } from '~/locale'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -30,6 +30,9 @@ export default { WorkItemLinksMenu, WorkItemDetailModal, }, + directives: { + GlTooltip: GlTooltipDirective, + }, inject: ['projectPath'], props: { workItemId: { @@ -284,7 +287,15 @@ export default { class="gl-relative gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32" data-testid="links-child" > - <div class="gl-overflow-hidden"> + <div class="gl-overflow-hidden gl-display-flex gl-align-items-center"> + <gl-icon + v-if="child.confidential" + v-gl-tooltip.top + name="eye-slash" + class="gl-mr-3 gl-text-orange-500" + data-testid="confidential-icon" + :title="__('Confidential')" + /> <gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-3 gl-text-gray-700" /> <gl-button :href="childPath(child.id)" diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql index 7d08cbaca48..dacc1527a43 100644 --- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql @@ -5,6 +5,7 @@ fragment WorkItem on WorkItem { title state description + confidential workItemType { id name @@ -34,6 +35,7 @@ fragment WorkItem on WorkItem { id iid title + confidential } children { nodes { diff --git a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql index c30c907baeb..df62ca1c143 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql @@ -20,6 +20,7 @@ query workItemQuery($id: WorkItemID!) { children { nodes { id + confidential workItemType { id } diff --git a/app/graphql/mutations/ci/job/retry.rb b/app/graphql/mutations/ci/job/retry.rb index 50e9c51c9e7..bfb9b902cc5 100644 --- a/app/graphql/mutations/ci/job/retry.rb +++ b/app/graphql/mutations/ci/job/retry.rb @@ -11,13 +11,20 @@ module Mutations null: true, description: 'Job after the mutation.' + argument :variables, [::Types::Ci::VariableInputType], + required: false, + default_value: [], + replace_null_with_default: true, + description: 'Variables to use when retrying a manual job.' + authorize :update_build - def resolve(id:) + def resolve(id:, variables:) job = authorized_find!(id: id) project = job.project + variables = variables.map(&:to_h) - response = ::Ci::RetryJobService.new(project, current_user).execute(job) + response = ::Ci::RetryJobService.new(project, current_user).execute(job, variables: variables) if response.success? { diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 3444c76da08..4ea9a016e74 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -194,7 +194,7 @@ module Types end def manual_variables - if object.manual? && object.respond_to?(:job_variables) + if object.action? && object.respond_to?(:job_variables) object.job_variables else [] diff --git a/app/graphql/types/ci/variable_input_type.rb b/app/graphql/types/ci/variable_input_type.rb new file mode 100644 index 00000000000..193ca6ffe4e --- /dev/null +++ b/app/graphql/types/ci/variable_input_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module Ci + class VariableInputType < BaseInputObject + graphql_name 'CiVariableInput' + description 'Attributes for defining a CI/CD variable.' + + argument :key, GraphQL::Types::String, description: 'Name of the variable.' + argument :value, GraphQL::Types::String, description: 'Value of the variable.' + end + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b267ea8f93f..b5dcb863e5b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1161,6 +1161,17 @@ module Ci end end + def clone(current_user:, new_job_variables_attributes: []) + new_build = super + + if action? && new_job_variables_attributes.any? + new_build.job_variables = [] + new_build.job_variables_attributes = new_job_variables_attributes + end + + new_build + end + protected def run_status_commit_hooks! diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 8b41ade90df..71d33f0bb63 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -22,7 +22,8 @@ module Ci accessibility: %w[accessibility], coverage: %w[cobertura], codequality: %w[codequality], - terraform: %w[terraform] + terraform: %w[terraform], + sbom: %w[cyclonedx] }.freeze DEFAULT_FILE_NAMES = { diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb index f666629c8fd..a2ff49077be 100644 --- a/app/models/ci/processable.rb +++ b/app/models/ci/processable.rb @@ -101,7 +101,7 @@ module Ci :merge_train_pipeline?, to: :pipeline - def clone(current_user:) + def clone(current_user:, new_job_variables_attributes: []) new_attributes = self.class.clone_accessors.to_h do |attribute| [attribute, public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend end diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 1aca763d57f..e10452c1081 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -19,6 +19,8 @@ class ContainerRepository < ApplicationRecord MIGRATION_PHASE_1_STARTED_AT = Date.new(2021, 11, 4).freeze MIGRATION_PHASE_1_ENDED_AT = Date.new(2022, 01, 23).freeze + MAX_TAGS_PAGES = 2000 + TooManyImportsError = Class.new(StandardError) belongs_to :project @@ -377,6 +379,10 @@ class ContainerRepository < ApplicationRecord migration_retries_count >= ContainerRegistry::Migration.max_retries - 1 end + def migrated? + MIGRATION_PHASE_1_ENDED_AT < self.created_at || import_done? + end + def last_import_step_done_at [migration_pre_import_done_at, migration_import_done_at, migration_aborted_at, migration_skipped_at].compact.max end @@ -427,6 +433,32 @@ class ContainerRepository < ApplicationRecord end end + def each_tags_page(page_size: 100, &block) + raise ArgumentError, 'not a migrated repository' unless migrated? + raise ArgumentError, 'block not given' unless block + + # dummy uri to initialize the loop + next_page_uri = URI('') + page_count = 0 + + while next_page_uri && page_count < MAX_TAGS_PAGES + last = Rack::Utils.parse_nested_query(next_page_uri.query)['last'] + current_page = gitlab_api_client.tags(self.path, page_size: page_size, last: last) + + if current_page&.key?(:response_body) + yield transform_tags_page(current_page[:response_body]) + next_page_uri = current_page.dig(:pagination, :next, :uri) + else + # no current page. Break the loop + next_page_uri = nil + end + + page_count += 1 + end + + raise 'too many pages requested' if page_count >= MAX_TAGS_PAGES + end + def tags_count return 0 unless manifest && manifest['tags'] @@ -559,6 +591,16 @@ class ContainerRepository < ApplicationRecord self.migration_skipped_reason = reason finish_import end + + def transform_tags_page(tags_response_body) + return [] unless tags_response_body + + tags_response_body.map do |raw_tag| + tag = ContainerRegistry::Tag.new(self, raw_tag['name']) + tag.force_created_at_from_iso8601(raw_tag['created_at']) + tag + end + end end ContainerRepository.prepend_mod_with('ContainerRepository') diff --git a/app/services/ci/retry_job_service.rb b/app/services/ci/retry_job_service.rb index e0ced3d0197..25bda8a6380 100644 --- a/app/services/ci/retry_job_service.rb +++ b/app/services/ci/retry_job_service.rb @@ -4,10 +4,10 @@ module Ci class RetryJobService < ::BaseService include Gitlab::Utils::StrongMemoize - def execute(job) + def execute(job, variables: []) if job.retryable? job.ensure_scheduling_type! - new_job = retry_job(job) + new_job = retry_job(job, variables: variables) ServiceResponse.success(payload: { job: new_job }) else @@ -19,7 +19,7 @@ module Ci end # rubocop: disable CodeReuse/ActiveRecord - def clone!(job) + def clone!(job, variables: []) # Cloning a job requires a strict type check to ensure # the attributes being used for the clone are taken straight # from the model and not overridden by other abstractions. @@ -27,7 +27,7 @@ module Ci check_access!(job) - new_job = job.clone(current_user: current_user) + new_job = job.clone(current_user: current_user, new_job_variables_attributes: variables) new_job.run_after_commit do ::Ci::CopyCrossDatabaseAssociationsService.new.execute(job, new_job) @@ -55,8 +55,8 @@ module Ci def check_assignable_runners!(job); end - def retry_job(job) - clone!(job).tap do |new_job| + def retry_job(job, variables: []) + clone!(job, variables: variables).tap do |new_job| check_assignable_runners!(new_job) if new_job.is_a?(Ci::Build) next if new_job.failed? |