Commit 65f5f75e authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖

Add latest changes from gitlab-org/[email protected]

parent 333f76ab
......@@ -13,11 +13,35 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql';
import { __, sprintf } from '~/locale';
import Api from '~/api';
let eTagPoll;
export const updateConfidentialityOnIssue = ({ commit, getters }, { confidential, fullPath }) => {
const { iid } = getters.getNoteableData;
return utils.gqClient
.mutate({
mutation: updateIssueConfidentialMutation,
variables: {
input: {
projectPath: fullPath,
iid: String(iid),
confidential,
},
},
})
.then(({ data }) => {
const {
issueSetConfidential: { issue },
} = data;
commit(types.SET_ISSUE_CONFIDENTIAL, issue.confidential);
});
};
export const expandDiscussion = ({ commit, dispatch }, data) => {
if (data.discussionId) {
dispatch('diffs/renderFileForDiscussionId', data.discussionId, { root: true });
......@@ -32,6 +56,8 @@ export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, d
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setConfidentiality = ({ commit }, data) => commit(types.SET_ISSUE_CONFIDENTIAL, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
......
......@@ -39,6 +39,7 @@ export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING';
export const SET_ISSUE_CONFIDENTIAL = 'SET_ISSUE_CONFIDENTIAL';
// Description version
export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION';
......
......@@ -95,6 +95,10 @@ export default {
Object.assign(state, { noteableData: data });
},
[types.SET_ISSUE_CONFIDENTIAL](state, data) {
state.noteableData.confidential = data;
},
[types.SET_USER_DATA](state, data) {
Object.assign(state, { userData: data });
},
......
import AjaxCache from '~/lib/utils/ajax_cache';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
import { sprintf, __ } from '~/locale';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
// factory function because global flag makes RegExp stateful
const createQuickActionsRegex = () => /^\/\w+.*$/gm;
......@@ -34,3 +35,10 @@ export const stripQuickActions = note => note.replace(createQuickActionsRegex(),
export const prepareDiffLines = diffLines =>
diffLines.map(line => ({ ...trimFirstCharOfLineContent(line) }));
export const gqClient = createGqClient(
{},
{
fetchPolicy: fetchPolicies.NO_CACHE,
},
);
......@@ -33,7 +33,7 @@ export default {
errorTexts: {
[LOAD_FAILURE]: __('We are currently unable to fetch data for this graph.'),
[PARSE_FAILURE]: __('There was an error parsing the data for this graph.'),
[UNSUPPORTED_DATA]: __('A DAG must have two dependent jobs to be visualized on this tab.'),
[UNSUPPORTED_DATA]: __('DAG visualization requires at least 3 dependent jobs.'),
[DEFAULT]: __('An unknown error occurred while loading this graph.'),
},
computed: {
......
<script>
import { mapState } from 'vuex';
import { mapState, mapActions } from 'vuex';
import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
......@@ -18,6 +18,10 @@ export default {
},
mixins: [recaptchaModalImplementor],
props: {
fullPath: {
required: true,
type: String,
},
isEditable: {
required: true,
type: Boolean,
......@@ -42,16 +46,24 @@ export default {
},
},
created() {
eventHub.$on('updateConfidentialAttribute', this.updateConfidentialAttribute);
eventHub.$on('closeConfidentialityForm', this.toggleForm);
},
beforeDestroy() {
eventHub.$off('updateConfidentialAttribute', this.updateConfidentialAttribute);
eventHub.$off('closeConfidentialityForm', this.toggleForm);
},
methods: {
...mapActions(['setConfidentiality']),
toggleForm() {
this.edit = !this.edit;
},
updateConfidentialAttribute(confidential) {
closeForm() {
this.edit = false;
},
updateConfidentialAttribute() {
// TODO: rm when FF is defaulted to on.
const confidential = !this.confidential;
this.service
.update('issue', { confidential })
.then(({ data }) => this.checkForSpam(data))
......@@ -97,12 +109,8 @@ export default {
>
</div>
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="edit"
:is-confidential="confidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
<div v-if="!confidential" class="no-value sidebar-item-value">
<edit-form v-if="edit" :is-confidential="confidential" :full-path="fullPath" />
<div v-if="!confidential" class="no-value sidebar-item-value" data-testid="not-confidential">
<icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" />
{{ __('Not confidential') }}
</div>
......
......@@ -11,9 +11,9 @@ export default {
required: true,
type: Boolean,
},
updateConfidentialAttribute: {
fullPath: {
required: true,
type: Function,
type: String,
},
},
computed: {
......@@ -37,10 +37,7 @@ export default {
<div>
<p v-if="!isConfidential" v-html="confidentialityOnWarning"></p>
<p v-else v-html="confidentialityOffWarning"></p>
<edit-form-buttons
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
<edit-form-buttons :full-path="fullPath" />
</div>
</div>
</div>
......
<script>
import $ from 'jquery';
import eventHub from '../../event_hub';
import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Flash from '~/flash';
import eventHub from '../../event_hub';
export default {
components: {
GlLoadingIcon,
},
mixins: [glFeatureFlagsMixin()],
props: {
isConfidential: {
required: true,
type: Boolean,
},
updateConfidentialAttribute: {
fullPath: {
required: true,
type: Function,
type: String,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
...mapState({ confidential: ({ noteableData }) => noteableData.confidential }),
toggleButtonText() {
return this.isConfidential ? __('Turn Off') : __('Turn On');
},
updateConfidentialBool() {
return !this.isConfidential;
if (this.isLoading) {
return __('Applying');
}
return this.confidential ? __('Turn Off') : __('Turn On');
},
},
methods: {
...mapActions(['updateConfidentialityOnIssue']),
closeForm() {
eventHub.$emit('closeConfidentialityForm');
$(this.$el).trigger('hidden.gl.dropdown');
},
submitForm() {
this.closeForm();
this.updateConfidentialAttribute(this.updateConfidentialBool);
this.isLoading = true;
const confidential = !this.confidential;
if (this.glFeatures.confidentialApolloSidebar) {
this.updateConfidentialityOnIssue({ confidential, fullPath: this.fullPath })
.catch(() => {
Flash(__('Something went wrong trying to change the confidentiality of this issue'));
})
.finally(() => {
this.closeForm();
this.isLoading = false;
});
} else {
eventHub.$emit('updateConfidentialAttribute');
}
},
},
};
......@@ -44,8 +69,10 @@ export default {
type="button"
class="btn btn-close"
data-testid="confidential-toggle"
:disabled="isLoading"
@click.prevent="submitForm"
>
<gl-loading-icon v-if="isLoading" inline />
{{ toggleButtonText }}
</button>
</div>
......
mutation updateIssueConfidential($input: IssueSetConfidentialInput!) {
issueSetConfidential(input: $input) {
issue {
confidential
}
}
}
......@@ -52,20 +52,30 @@ function mountAssigneesComponent(mediator) {
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
const { fullPath, iid } = getSidebarOptions();
if (!el) return;
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
new ConfidentialComp({
// eslint-disable-next-line no-new
new Vue({
el,
store,
propsData: {
isEditable: initialData.is_editable,
service: mediator.service,
components: {
ConfidentialIssueSidebar,
},
}).$mount(el);
render: createElement =>
createElement('confidential-issue-sidebar', {
props: {
iid: String(iid),
fullPath,
isEditable: initialData.is_editable,
service: mediator.service,
},
}),
});
}
function mountLockComponent(mediator) {
......
......@@ -415,7 +415,6 @@ img.emoji {
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-right-48 { margin-right: 48px; }
.prepend-right-32 { margin-right: 32px; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
......
......@@ -51,6 +51,8 @@ class Projects::IssuesController < Projects::ApplicationController
before_action only: :show do
push_frontend_feature_flag(:real_time_issue_sidebar, @project)
push_frontend_feature_flag(:confidential_notes, @project)
push_frontend_feature_flag(:confidential_apollo_sidebar, @project)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
......
......@@ -320,7 +320,9 @@ module Ci
# ref - The ref to scope the data to (e.g. "master"). If the ref is not
# given we simply get the latest pipelines for the commits, regardless
# of what refs the pipelines belong to.
def self.latest_pipeline_per_commit(commits, ref = nil)
# project_key - Support `commits` from different projects, returns results
# keyed by `hash[project_id][commit_id]`
def self.latest_pipeline_per_commit(commits, ref = nil, project_key: false)
p1 = arel_table
p2 = arel_table.alias
......@@ -341,7 +343,13 @@ module Ci
relation = relation.where(ref: ref) if ref
relation.each_with_object({}) do |pipeline, hash|
hash[pipeline.sha] = pipeline
commits = if project_key
hash[pipeline.project_id] ||= {}
else
hash
end
commits[pipeline.sha] = pipeline
end
end
......@@ -349,6 +357,10 @@ module Ci
success.group(:project_id).select('max(id) as id')
end
def self.last_finished_for_ref_id(ci_ref_id)
where(ci_ref_id: ci_ref_id).ci_sources.finished.order(id: :desc).select(:id).take
end
def self.truncate_sha(sha)
sha[0...8]
end
......
......@@ -45,7 +45,8 @@ module Ci
webide_source: 3,
remote_source: 4,
external_project_source: 5,
bridge_source: 6
bridge_source: 6,
parameter_source: 7
}
end
......
......@@ -43,7 +43,7 @@ module Ci
end
def last_finished_pipeline_id
Ci::Pipeline.where(ci_ref_id: self.id).finished.order(id: :desc).select(:id).take&.id
Ci::Pipeline.last_finished_for_ref_id(self.id)&.id
end
def update_status_by!(pipeline)
......
# frozen_string_literal: true
# A collection of Commit instances for a specific container and Git reference.
# A collection of Commit instances for a specific Git reference.
class CommitCollection
include Enumerable
include Gitlab::Utils::StrongMemoize
attr_reader :container, :ref, :commits
delegate :repository, to: :container, allow_nil: true
delegate :project, to: :repository, allow_nil: true
# container - The object the commits belong to.
# container - The object the commits belong to (each commit project will be used if not provided).
# commits - The Commit instances to store.
# ref - The name of the ref (e.g. "master").
def initialize(container, commits, ref = nil)
......@@ -42,12 +39,13 @@ class CommitCollection
# Setting the pipeline for each commit ahead of time removes the need for running
# a query for every commit we're displaying.
def with_latest_pipeline(ref = nil)
return self unless project
pipelines = project.ci_pipelines.latest_pipeline_per_commit(map(&:id), ref)
# since commit ids are not unique across all projects, use project_key = true to get commits by project
pipelines = ::Ci::Pipeline.ci_sources.latest_pipeline_per_commit(map(&:id), ref, project_key: true)
# set the pipeline for each commit by project_id and commit for the latest pipeline for ref
each do |commit|
commit.set_latest_pipeline_for_ref(ref, pipelines[commit.id])
project_id = container&.id || commit.project_id
commit.set_latest_pipeline_for_ref(ref, pipelines.dig(project_id, commit.id))
end
self
......@@ -64,16 +62,19 @@ class CommitCollection
# Batch load any commits that are not backed by full gitaly data, and
# replace them in the collection.
def enrich!
return self if fully_enriched?
# Batch load full Commits from the repository
# and map to a Hash of id => Commit
# A container is needed in order to fetch data from gitaly. Containers
# can be absent from commits in certain rare situations (like when
# viewing a MR of a deleted fork). In these cases, assume that the
# enriched data is not needed.
return self if container.blank? || fully_enriched?
# Batch load full Commits from the repository
# and map to a Hash of id => Commit
replacements = Hash[unenriched.map do |c|
[c.id, Commit.lazy(container, c.id)]
commits_to_enrich = unenriched.select { |c| container.present? || c.container.present? }
replacements = Hash[commits_to_enrich.map do |c|
commit_container = container || c.container
[c.id, Commit.lazy(commit_container, c.id)]
end.compact]
# Replace the commits, keeping the same order
......
......@@ -520,6 +520,10 @@ class Project < ApplicationRecord
group: :ip_restrictions, namespace: [:route, :owner])
}
scope :with_api_commit_entity_associations, -> {
preload(:project_feature, :route, namespace: [:route, :owner])
}
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout,
......@@ -1699,7 +1703,7 @@ class Project < ApplicationRecord
def pages_url
url = pages_group_url
url_path = full_path.partition('/').last
url_path = full_path.partition('/').last.downcase
# If the project path is the same as host, we serve it as group page
return url if url == "#{Settings.pages.protocol}://#{url_path}"
......
......@@ -23,6 +23,24 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::Activity,
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
# Create a new pipeline in the specified project.
#
# @param [Symbol] source What event (Ci::Pipeline.sources) triggers the pipeline
# creation.
# @param [Boolean] ignore_skip_ci Whether skipping a pipeline creation when `[skip ci]` comment
# is present in the commit body
# @param [Boolean] save_on_errors Whether persisting an invalid pipeline when it encounters an
# error during creation (e.g. invalid yaml)
# @param [Ci::TriggerRequest] trigger_request The pipeline trigger triggers the pipeline creation.
# @param [Ci::PipelineSchedule] schedule The pipeline schedule triggers the pipeline creation.
# @param [MergeRequest] merge_request The merge request triggers the pipeline creation.
# @param [ExternalPullRequest] external_pull_request The external pull request triggers the pipeline creation.
# @param [Ci::Bridge] bridge The bridge job that triggers the downstream pipeline creation.
# @param [String] content The content of .gitlab-ci.yml to override the default config
# contents (e.g. .gitlab-ci.yml in repostiry). Mainly used for
# generating a dangling pipeline.
#
# @return [Ci::Pipeline] The created Ci::Pipeline object.
# rubocop: disable Metrics/ParameterLists
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block)
@pipeline = Ci::Pipeline.new
......@@ -122,13 +140,8 @@ module Ci
end
end
def extra_options(options = {})
# In Ruby 2.4, even when options is empty, f(**options) doesn't work when f
# doesn't have any parameters. We reproduce the Ruby 2.5 behavior by
# checking explicitly that no arguments are given.
raise ArgumentError if options.any?
{} # overridden in EE
def extra_options(content: nil)
{ content: content }
end
end
end
......
......@@ -76,7 +76,7 @@ module Projects
end
def parsed_payload
Gitlab::Alerting::NotificationPayloadParser.call(params.to_h)
Gitlab::Alerting::NotificationPayloadParser.call(params.to_h, project)
end
def valid_token?(token)
......
---
title: Edit copy of DAG unsupported data alert
merge_request: 35170
author:
type: other
---
title: Fix pages_url for projects with mixed case path
merge_request: 35300
author:
type: fixed
---
title: Ability to use an arbitrary YAML blob to create CI pipelines
merge_request: 34706
author:
type: added
......@@ -1346,6 +1346,51 @@ enum CommitEncoding {
TEXT
}
"""
Represents a ComplianceFramework associated with a Project
"""
type ComplianceFramework {
"""
Name of the compliance framework
"""
name: ProjectSettingEnum!
}
"""
The connection type for ComplianceFramework.
"""
type ComplianceFrameworkConnection {
"""
A list of edges.
"""
edges: [ComplianceFrameworkEdge]
"""
A list of nodes.
"""
nodes: [ComplianceFramework]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type ComplianceFrameworkEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: ComplianceFramework
}
"""
A tag expiration policy designed to keep only the images that matter most
"""
......@@ -8750,6 +8795,31 @@ type Project {
last: Int
): BoardConnection
"""
Compliance frameworks associated with the project
"""
complianceFrameworks(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): ComplianceFrameworkConnection
"""
The container expiration policy of the project
"""
......@@ -10026,6 +10096,17 @@ type ProjectPermissions {
uploadFile: Boolean!
}
"""
Names of compliance frameworks that can be assigned to a Project
"""
enum ProjectSettingEnum {
gdpr
hipaa
pci_dss
soc_2
sox