diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-02 12:10:59 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-02 12:10:59 +0300 |
commit | 78bc39880c4b06b2fbe682e0201722a11237a425 (patch) | |
tree | 00500cb71d9e86a404ec42264cc3b4992e5610ce /app | |
parent | 377b57afa8292caa96921fac7daf6279e12304de (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
21 files changed, 251 insertions, 62 deletions
diff --git a/app/assets/javascripts/environments/components/new_environments_app.vue b/app/assets/javascripts/environments/components/new_environments_app.vue index a5526f9cd71..bfb5689d623 100644 --- a/app/assets/javascripts/environments/components/new_environments_app.vue +++ b/app/assets/javascripts/environments/components/new_environments_app.vue @@ -1,6 +1,7 @@ <script> import { GlBadge, GlTab, GlTabs } from '@gitlab/ui'; -import environmentAppQuery from '../graphql/queries/environmentApp.query.graphql'; +import environmentAppQuery from '../graphql/queries/environment_app.query.graphql'; +import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql'; import EnvironmentFolder from './new_environment_folder.vue'; export default { @@ -13,7 +14,16 @@ export default { apollo: { environmentApp: { query: environmentAppQuery, + pollInterval() { + return this.interval; + }, }, + interval: { + query: pollIntervalQuery, + }, + }, + data() { + return { interval: undefined }; }, computed: { folders() { diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js index c734c2fba0c..c019b4d16f3 100644 --- a/app/assets/javascripts/environments/graphql/client.js +++ b/app/assets/javascripts/environments/graphql/client.js @@ -1,6 +1,6 @@ import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import environmentApp from './queries/environmentApp.query.graphql'; +import environmentApp from './queries/environment_app.query.graphql'; import { resolvers } from './resolvers'; import typeDefs from './typedefs.graphql'; diff --git a/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql index faa76c0a42c..faa76c0a42c 100644 --- a/app/assets/javascripts/environments/graphql/queries/environmentApp.query.graphql +++ b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql diff --git a/app/assets/javascripts/environments/graphql/queries/poll_interval.query.graphql b/app/assets/javascripts/environments/graphql/queries/poll_interval.query.graphql new file mode 100644 index 00000000000..28afc30a0dd --- /dev/null +++ b/app/assets/javascripts/environments/graphql/queries/poll_interval.query.graphql @@ -0,0 +1,3 @@ +query pollInterval { + interval @client +} diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js index 8322b806370..9bb00f92ac4 100644 --- a/app/assets/javascripts/environments/graphql/resolvers.js +++ b/app/assets/javascripts/environments/graphql/resolvers.js @@ -1,5 +1,12 @@ import axios from '~/lib/utils/axios_utils'; +import { s__ } from '~/locale'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import pollIntervalQuery from './queries/poll_interval.query.graphql'; + +const buildErrors = (errors = []) => ({ + errors, + __typename: 'LocalEnvironmentErrors', +}); const mapNestedEnvironment = (env) => ({ ...convertObjectPropsToCamelCase(env, { deep: true }), @@ -12,17 +19,27 @@ const mapEnvironment = (env) => ({ export const resolvers = (endpoint) => ({ Query: { - environmentApp() { - return axios.get(endpoint, { params: { nested: true } }).then((res) => ({ - availableCount: res.data.available_count, - environments: res.data.environments.map(mapNestedEnvironment), - reviewApp: { - ...convertObjectPropsToCamelCase(res.data.review_app), - __typename: 'ReviewApp', - }, - stoppedCount: res.data.stopped_count, - __typename: 'LocalEnvironmentApp', - })); + environmentApp(_context, _variables, { cache }) { + return axios.get(endpoint, { params: { nested: true } }).then((res) => { + const interval = res.headers['poll-interval']; + + if (interval) { + cache.writeQuery({ query: pollIntervalQuery, data: { interval } }); + } else { + cache.writeQuery({ query: pollIntervalQuery, data: { interval: undefined } }); + } + + return { + availableCount: res.data.available_count, + environments: res.data.environments.map(mapNestedEnvironment), + reviewApp: { + ...convertObjectPropsToCamelCase(res.data.review_app), + __typename: 'ReviewApp', + }, + stoppedCount: res.data.stopped_count, + __typename: 'LocalEnvironmentApp', + }; + }); }, folder(_, { environment: { folderPath } }) { return axios.get(folderPath, { params: { per_page: 3 } }).then((res) => ({ @@ -32,19 +49,51 @@ export const resolvers = (endpoint) => ({ __typename: 'LocalEnvironmentFolder', })); }, + isLastDeployment(_, { environment }) { + // eslint-disable-next-line @gitlab/require-i18n-strings + return environment?.lastDeployment?.['last?']; + }, }, - Mutations: { - stopEnvironment(_, { environment: { stopPath } }) { - return axios.post(stopPath); + Mutation: { + stopEnvironment(_, { environment }) { + return axios + .post(environment.stopPath) + .then(() => buildErrors()) + .catch(() => { + return buildErrors([ + s__('Environments|An error occurred while stopping the environment, please try again'), + ]); + }); }, deleteEnvironment(_, { environment: { deletePath } }) { return axios.delete(deletePath); }, - rollbackEnvironment(_, { environment: { retryUrl } }) { - return axios.post(retryUrl); + rollbackEnvironment(_, { environment, isLastDeployment }) { + return axios + .post(environment?.retryUrl) + .then(() => buildErrors()) + .catch(() => { + buildErrors([ + isLastDeployment + ? s__( + 'Environments|An error occurred while re-deploying the environment, please try again', + ) + : s__( + 'Environments|An error occurred while rolling back the environment, please try again', + ), + ]); + }); }, cancelAutoStop(_, { environment: { autoStopPath } }) { - return axios.post(autoStopPath); + return axios + .post(autoStopPath) + .then(() => buildErrors()) + .catch((err) => + buildErrors([ + err?.response?.data?.message || + s__('Environments|An error occurred while canceling the auto stop, please try again'), + ]), + ); }, }, }); diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql index 49ea719449e..f0172765ebe 100644 --- a/app/assets/javascripts/environments/graphql/typedefs.graphql +++ b/app/assets/javascripts/environments/graphql/typedefs.graphql @@ -33,3 +33,20 @@ type LocalEnvironmentApp { environments: [NestedLocalEnvironment!]! reviewApp: ReviewApp! } + +type LocalErrors { + errors: [String!]! +} + +extend type Query { + environmentApp: LocalEnvironmentApp + folder(environment: NestedLocalEnvironment): LocalEnvironmentFolder + isLastDeployment: Boolean +} + +extend type Mutation { + stopEnvironment(environment: LocalEnvironment): LocalErrors + deleteEnvironment(environment: LocalEnvironment): LocalErrors + rollbackEnvironment(environment: LocalEnvironment): LocalErrors + cancelAutoStop(environment: LocalEnvironment): LocalErrors +} diff --git a/app/assets/javascripts/jira_connect/branches/index.js b/app/assets/javascripts/jira_connect/branches/index.js index 04510fcff4b..a9a56a6362e 100644 --- a/app/assets/javascripts/jira_connect/branches/index.js +++ b/app/assets/javascripts/jira_connect/branches/index.js @@ -5,7 +5,7 @@ import createDefaultClient from '~/lib/graphql'; Vue.use(VueApollo); -export default async function initJiraConnectBranches() { +export default function initJiraConnectBranches() { const el = document.querySelector('.js-jira-connect-create-branch'); if (!el) { return null; diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue index c0504cbb645..7fd4cc38f11 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue @@ -7,6 +7,7 @@ import { SET_ALERT } from '../store/mutation_types'; import SubscriptionsList from './subscriptions_list.vue'; import AddNamespaceButton from './add_namespace_button.vue'; import SignInButton from './sign_in_button.vue'; +import UserLink from './user_link.vue'; export default { name: 'JiraConnectApp', @@ -18,6 +19,7 @@ export default { SubscriptionsList, AddNamespaceButton, SignInButton, + UserLink, }, inject: { usersPath: { @@ -74,6 +76,8 @@ export default { </template> </gl-alert> + <user-link :user-signed-in="userSignedIn" :has-subscriptions="hasSubscriptions" /> + <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> <div class="jira-connect-app-body gl-mx-auto gl-px-5 gl-mb-7"> <template v-if="hasSubscriptions"> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue b/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue new file mode 100644 index 00000000000..fad3d2616d8 --- /dev/null +++ b/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue @@ -0,0 +1,67 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils'; + +export default { + components: { + GlLink, + GlSprintf, + }, + inject: { + usersPath: { + default: '', + }, + gitlabUserPath: { + default: '', + }, + }, + props: { + userSignedIn: { + type: Boolean, + required: true, + }, + hasSubscriptions: { + type: Boolean, + required: true, + }, + }, + data() { + return { + signInURL: '', + }; + }, + computed: { + gitlabUserHandle() { + return `@${gon.current_username}`; + }, + }, + async created() { + this.signInURL = await getGitlabSignInURL(this.usersPath); + }, + i18n: { + signInText: __('Sign in to GitLab'), + signedInAsUserText: __('Signed in to GitLab as %{user_link}'), + }, +}; +</script> +<template> + <div class="jira-connect-user gl-font-base"> + <gl-sprintf v-if="userSignedIn" :message="$options.i18n.signedInAsUserText"> + <template #user_link> + <gl-link data-testid="gitlab-user-link" :href="gitlabUserPath" target="_blank"> + {{ gitlabUserHandle }} + </gl-link> + </template> + </gl-sprintf> + + <gl-link + v-else-if="hasSubscriptions" + data-testid="sign-in-link" + :href="signInURL" + target="_blank" + > + {{ $options.i18n.signInText }} + </gl-link> + </div> +</template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/index.js b/app/assets/javascripts/jira_connect/subscriptions/index.js index 8a7a80d885d..cd1fc1d4455 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/index.js +++ b/app/assets/javascripts/jira_connect/subscriptions/index.js @@ -7,25 +7,11 @@ import Translate from '~/vue_shared/translate'; import JiraConnectApp from './components/app.vue'; import createStore from './store'; -import { getGitlabSignInURL, sizeToParent } from './utils'; +import { sizeToParent } from './utils'; const store = createStore(); -/** - * Add `return_to` query param to all HAML-defined GitLab sign in links. - */ -const updateSignInLinks = async () => { - await Promise.all( - Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).map(async (el) => { - const updatedLink = await getGitlabSignInURL(el.getAttribute('href')); - el.setAttribute('href', updatedLink); - }), - ); -}; - -export async function initJiraConnect() { - await updateSignInLinks(); - +export function initJiraConnect() { const el = document.querySelector('.js-jira-connect-app'); if (!el) { return null; @@ -35,7 +21,7 @@ export async function initJiraConnect() { Vue.use(Translate); Vue.use(GlFeatureFlagsPlugin); - const { groupsPath, subscriptions, subscriptionsPath, usersPath } = el.dataset; + const { groupsPath, subscriptions, subscriptionsPath, usersPath, gitlabUserPath } = el.dataset; sizeToParent(); return new Vue({ @@ -46,6 +32,7 @@ export async function initJiraConnect() { subscriptions: JSON.parse(subscriptions), subscriptionsPath, usersPath, + gitlabUserPath, }, render(createElement) { return createElement(JiraConnectApp); diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3bec7928058..89949b82ae5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -282,7 +282,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo if merge_request.errors.present? render json: @merge_request.errors, status: :bad_request else - render json: serializer.represent(@merge_request, serializer: 'basic') + render json: serializer.represent(@merge_request, serializer: params[:serializer] || 'basic') end end end diff --git a/app/helpers/jira_connect_helper.rb b/app/helpers/jira_connect_helper.rb index 475469a6df9..9a0f0944fd1 100644 --- a/app/helpers/jira_connect_helper.rb +++ b/app/helpers/jira_connect_helper.rb @@ -8,7 +8,8 @@ module JiraConnectHelper groups_path: api_v4_groups_path(params: { min_access_level: Gitlab::Access::MAINTAINER, skip_groups: skip_groups }), subscriptions: subscriptions.map { |s| serialize_subscription(s) }.to_json, subscriptions_path: jira_connect_subscriptions_path, - users_path: current_user ? nil : jira_connect_users_path + users_path: current_user ? nil : jira_connect_users_path, # users_path is used to determine if user is signed in + gitlab_user_path: current_user ? user_path(current_user) : nil } end diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index 1d8b657025c..f2e1d158c2d 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -40,7 +40,9 @@ module SystemNoteHelper 'new_alert_added' => 'warning', 'severity' => 'information-o', 'cloned' => 'documents', - 'issue_type' => 'pencil-square' + 'issue_type' => 'pencil-square', + 'attention_requested' => 'user', + 'attention_request_removed' => 'user' }.freeze def system_note_icon_name(note) diff --git a/app/models/concerns/merge_request_reviewer_state.rb b/app/models/concerns/merge_request_reviewer_state.rb index 216a3a0bd64..5859f43a70c 100644 --- a/app/models/concerns/merge_request_reviewer_state.rb +++ b/app/models/concerns/merge_request_reviewer_state.rb @@ -15,11 +15,5 @@ module MergeRequestReviewerState inclusion: { in: self.states.keys } after_initialize :set_state, unless: :persisted? - - def set_state - if Feature.enabled?(:mr_attention_requests, self.merge_request&.project, default_enabled: :yaml) - self.state = :attention_requested - end - end end end diff --git a/app/models/merge_request_assignee.rb b/app/models/merge_request_assignee.rb index fd8e5860040..77b46fa50f4 100644 --- a/app/models/merge_request_assignee.rb +++ b/app/models/merge_request_assignee.rb @@ -10,6 +10,12 @@ class MergeRequestAssignee < ApplicationRecord scope :in_projects, ->(project_ids) { joins(:merge_request).where(merge_requests: { target_project_id: project_ids }) } + def set_state + if Feature.enabled?(:mr_attention_requests, self.merge_request&.project, default_enabled: :yaml) + self.state = MergeRequestReviewer.find_by(user_id: self.user_id, merge_request_id: self.merge_request_id)&.state || :attention_requested + end + end + def cache_key [model_name.cache_key, id, state, assignee.cache_key] end diff --git a/app/models/merge_request_reviewer.rb b/app/models/merge_request_reviewer.rb index 4abf0fa09f0..8c75fb2e4e6 100644 --- a/app/models/merge_request_reviewer.rb +++ b/app/models/merge_request_reviewer.rb @@ -6,6 +6,12 @@ class MergeRequestReviewer < ApplicationRecord belongs_to :merge_request belongs_to :reviewer, class_name: 'User', foreign_key: :user_id, inverse_of: :merge_request_reviewers + def set_state + if Feature.enabled?(:mr_attention_requests, self.merge_request&.project, default_enabled: :yaml) + self.state = MergeRequestAssignee.find_by(user_id: self.user_id, merge_request_id: self.merge_request_id)&.state || :attention_requested + end + end + def cache_key [model_name.cache_key, id, state, reviewer.cache_key] end diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 749b9dce97c..7b13109dbc4 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -24,6 +24,7 @@ class SystemNoteMetadata < ApplicationRecord opened closed merged duplicate locked unlocked outdated reviewer tag due_date pinned_embed cherry_pick health_status approved unapproved status alert_issue_added relate unrelate new_alert_added severity + attention_requested attention_request_removed ].freeze validates :note, presence: true, unless: :importing? diff --git a/app/services/merge_requests/toggle_attention_requested_service.rb b/app/services/merge_requests/toggle_attention_requested_service.rb index 4e36ae065bb..fd24e87454c 100644 --- a/app/services/merge_requests/toggle_attention_requested_service.rb +++ b/app/services/merge_requests/toggle_attention_requested_service.rb @@ -19,7 +19,10 @@ module MergeRequests update_state(assignee) if reviewer&.attention_requested? || assignee&.attention_requested? + create_attention_request_note notity_user + else + create_remove_attention_request_note end success @@ -35,6 +38,14 @@ module MergeRequests todo_service.create_attention_requested_todo(merge_request, current_user, user) end + def create_attention_request_note + SystemNoteService.request_attention(merge_request, merge_request.project, current_user, user) + end + + def create_remove_attention_request_note + SystemNoteService.remove_attention_request(merge_request, merge_request.project, current_user, user) + end + def assignee merge_request.find_assignee(user) end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e98dfb872fe..0d13c73d49d 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -115,6 +115,14 @@ module SystemNoteService ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_status(status, source) end + def request_attention(noteable, project, author, user) + ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).request_attention(user) + end + + def remove_attention_request(noteable, project, author, user) + ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).remove_attention_request(user) + end + # Called when 'merge when pipeline succeeds' is executed def merge_when_pipeline_succeeds(noteable, project, author, sha) ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).merge_when_pipeline_succeeds(sha) diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb index 92540f957c8..d33dcd65589 100644 --- a/app/services/system_notes/issuables_service.rb +++ b/app/services/system_notes/issuables_service.rb @@ -323,23 +323,34 @@ module SystemNotes existing_mentions_for(mentioned_in, noteable, notes).exists? end - # Called when a Noteable has been marked as a duplicate of another Issue + # Called when a user's attention has been requested for a Notable # - # canonical_issue - Issue that this is a duplicate of + # user - User's whos attention has been requested # # Example Note text: # - # "marked this issue as a duplicate of #1234" - # - # "marked this issue as a duplicate of other_project#5678" + # "requested attention from @eli.wisoky" # # Returns the created Note object - def mark_duplicate_issue(canonical_issue) - body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" + def request_attention(user) + body = "requested attention from #{user.to_reference}" - issue_activity_counter.track_issue_marked_as_duplicate_action(author: author) if noteable.is_a?(Issue) + create_note(NoteSummary.new(noteable, project, author, body, action: 'attention_requested')) + end - create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) + # Called when a user's attention request has been removed for a Notable + # + # user - User's whos attention request has been removed + # + # Example Note text: + # + # "removed attention request from @eli.wisoky" + # + # Returns the created Note object + def remove_attention_request(user) + body = "removed attention request from #{user.to_reference}" + + create_note(NoteSummary.new(noteable, project, author, body, action: 'attention_request_removed')) end # Called when a Noteable has been marked as the canonical Issue of a duplicate @@ -358,6 +369,25 @@ module SystemNotes create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) end + # Called when a Noteable has been marked as a duplicate of another Issue + # + # canonical_issue - Issue that this is a duplicate of + # + # Example Note text: + # + # "marked this issue as a duplicate of #1234" + # + # "marked this issue as a duplicate of other_project#5678" + # + # Returns the created Note object + def mark_duplicate_issue(canonical_issue) + body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" + + issue_activity_counter.track_issue_marked_as_duplicate_action(author: author) if noteable.is_a?(Issue) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) + end + def add_email_participants(body) create_note(NoteSummary.new(noteable, project, author, body)) end diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml index be2be7288f8..d92c30c8840 100644 --- a/app/views/jira_connect/subscriptions/index.html.haml +++ b/app/views/jira_connect/subscriptions/index.html.haml @@ -1,13 +1,6 @@ %header.jira-connect-header.gl-display-flex.gl-align-items-center.gl-justify-content-center.gl-px-5.gl-border-b-solid.gl-border-b-gray-100.gl-border-b-1.gl-bg-white = link_to brand_header_logo, Gitlab.config.gitlab.url, target: '_blank', rel: 'noopener noreferrer' -.jira-connect-user.gl-font-base - - if current_user - - user_link = link_to(current_user.to_reference, jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in') - = _('Signed in to GitLab as %{user_link}').html_safe % { user_link: user_link } - - elsif @subscriptions.present? - = link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in' - %main.jira-connect-app.gl-px-5.gl-pt-7.gl-mx-auto .js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) } |