diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-08 09:09:17 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-08 09:09:17 +0000 |
commit | 21341457a8c422d890a9ec30838b597dea565d62 (patch) | |
tree | aa8aca2a9bce4e16936cc8d7b40aa1c79ca82e35 /app | |
parent | 0a319374e7784aa5c2d1c30dd832d2a0509edbab (diff) | |
download | gitlab-ce-21341457a8c422d890a9ec30838b597dea565d62.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
22 files changed, 344 insertions, 83 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue index 7dde9edbd27..63f3b3bc1f0 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue @@ -5,18 +5,21 @@ import { GlForm, GlFormGroup, GlFormInput, + GlFormInputGroup, + GlFormTextarea, GlLink, GlModal, GlModalDirective, GlSprintf, GlFormSelect, } from '@gitlab/ui'; +import { debounce } from 'lodash'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ToggleButton from '~/vue_shared/components/toggle_button.vue'; import csrf from '~/lib/utils/csrf'; import service from '../services'; -import { i18n, serviceOptions } from '../constants'; +import { i18n, serviceOptions, JSON_VALIDATE_DELAY } from '../constants'; export default { i18n, @@ -27,7 +30,9 @@ export default { GlForm, GlFormGroup, GlFormInput, + GlFormInputGroup, GlFormSelect, + GlFormTextarea, GlLink, GlModal, GlSprintf, @@ -73,6 +78,11 @@ export default { feedbackMessage: null, isFeedbackDismissed: false, }, + testAlert: { + json: null, + error: null, + }, + canSaveForm: false, }; }, computed: { @@ -109,12 +119,32 @@ export default { showFeedbackMsg() { return this.feedback.feedbackMessage && !this.isFeedbackDismissed; }, + showAlertSave() { + return ( + this.feedback.feedbackMessage === this.$options.i18n.testAlertFailed && + !this.isFeedbackDismissed + ); + }, prometheusInfo() { return !this.isGeneric ? this.$options.i18n.prometheusInfo : ''; }, prometheusFeatureEnabled() { return !this.isGeneric && this.glFeatures.alertIntegrationsDropdown; }, + jsonIsValid() { + return this.testAlert.error === null; + }, + canTestAlert() { + return this.selectedService.active && this.testAlert.json !== null; + }, + canSaveConfig() { + return !this.loading && this.canSaveForm; + }, + }, + watch: { + 'testAlert.json': debounce(function debouncedJsonValidate() { + this.validateJson(); + }, JSON_VALIDATE_DELAY), }, created() { if (this.glFeatures.alertIntegrationsDropdown) { @@ -126,6 +156,9 @@ export default { } }, methods: { + clearJson() { + this.testAlert.json = null; + }, dismissFeedback() { this.feedback = { ...this.feedback, feedbackMessage: null }; this.isFeedbackDismissed = false; @@ -135,6 +168,7 @@ export default { .updateGenericKey({ endpoint: this.generic.formPath, params: { service: { token: '' } } }) .then(({ data: { token } }) => { this.authorizationKey.generic = token; + this.setFeedback({ feedbackMessage: this.$options.i18n.authKeyRest, variant: 'success' }); }) .catch(() => { this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' }); @@ -145,11 +179,24 @@ export default { .updatePrometheusKey({ endpoint: this.prometheus.prometheusResetKeyPath }) .then(({ data: { token } }) => { this.authorizationKey.prometheus = token; + this.setFeedback({ feedbackMessage: this.$options.i18n.authKeyRest, variant: 'success' }); }) .catch(() => { this.setFeedback({ feedbackMessage: this.$options.i18n.errorKeyMsg, variant: 'danger' }); }); }, + toggleService(value) { + this.canSaveForm = true; + if (!this.glFeatures.alertIntegrationsDropdown) { + this.toggleActivated(value); + } + + if (this.isGeneric) { + this.activated.generic = value; + } else { + this.activated.prometheus = value; + } + }, toggleActivated(value) { return this.isGeneric ? this.toggleGenericActivated(value) @@ -164,15 +211,14 @@ export default { }) .then(() => { this.activated.generic = value; - - if (value) { - this.setFeedback({ - feedbackMessage: this.$options.i18n.endPointActivated, - variant: 'success', - }); - } + this.toggleSuccess(value); + }) + .catch(() => { + this.setFeedback({ + feedbackMessage: this.$options.i18n.errorMsg, + variant: 'danger', + }); }) - .catch(() => {}) .finally(() => { this.loading = false; }); @@ -191,12 +237,7 @@ export default { }) .then(() => { this.activated.prometheus = value; - if (value) { - this.setFeedback({ - feedbackMessage: this.$options.i18n.endPointActivated, - variant: 'success', - }); - } + this.toggleSuccess(value); }) .catch(() => { this.setFeedback({ @@ -208,16 +249,61 @@ export default { this.loading = false; }); }, + toggleSuccess(value) { + if (value) { + this.setFeedback({ + feedbackMessage: this.$options.i18n.endPointActivated, + variant: 'info', + }); + } else { + this.setFeedback({ + feedbackMessage: this.$options.i18n.changesSaved, + variant: 'info', + }); + } + }, setFeedback({ feedbackMessage, variant }) { this.feedback = { feedbackMessage, variant }; }, - onSubmit(evt) { - // TODO: Add form submit as part of https://gitlab.com/gitlab-org/gitlab/-/issues/215356 - evt.preventDefault(); + validateJson() { + this.testAlert.error = null; + try { + JSON.parse(this.testAlert.json); + } catch (e) { + this.testAlert.error = JSON.stringify(e.message); + } }, - onReset(evt) { - // TODO: Add form reset as part of https://gitlab.com/gitlab-org/gitlab/-/issues/215356 - evt.preventDefault(); + validateTestAlert() { + this.loading = true; + this.validateJson(); + return service + .updateTestAlert({ + endpoint: this.selectedService.url, + data: this.testAlert.json, + authKey: this.selectedService.authKey, + }) + .then(() => { + this.setFeedback({ + feedbackMessage: this.$options.i18n.testAlertSuccess, + variant: 'success', + }); + }) + .catch(() => { + this.setFeedback({ + feedbackMessage: this.$options.i18n.testAlertFailed, + variant: 'danger', + }); + }) + .finally(() => { + this.loading = false; + }); + }, + onSubmit() { + this.toggleActivated(this.selectedService.active); + }, + onReset() { + this.testAlert.json = null; + this.dismissFeedback(); }, }, }; @@ -227,6 +313,15 @@ export default { <div> <gl-alert v-if="showFeedbackMsg" :variant="feedback.variant" @dismiss="dismissFeedback"> {{ feedback.feedbackMessage }} + <gl-button + v-if="showAlertSave" + variant="danger" + category="primary" + class="gl-display-block gl-mt-3" + @click="toggleActivated(selectedService.active)" + > + {{ __('Save anyway') }} + </gl-button> </gl-alert> <div data-testid="alert-settings-description" class="gl-mt-5"> <p v-for="section in sections" :key="section.text"> @@ -237,7 +332,7 @@ export default { </gl-sprintf> </p> </div> - <gl-form @submit="onSubmit" @reset="onReset"> + <gl-form @submit.prevent="onSubmit" @reset.prevent="onReset"> <gl-form-group v-if="glFeatures.alertIntegrationsDropdown" :label="$options.i18n.integrationsLabel" @@ -248,6 +343,7 @@ export default { v-model="selectedEndpoint" :options="options" data-testid="alert-settings-select" + @change="clearJson" /> <span class="gl-text-gray-400"> <gl-sprintf :message="$options.i18n.integrationsInfo"> @@ -272,7 +368,7 @@ export default { :disabled-input="loading" :is-loading="loading" :value="selectedService.active" - @change="toggleActivated" + @change="toggleService" /> </gl-form-group> <gl-form-group @@ -293,12 +389,15 @@ export default { </span> </gl-form-group> <gl-form-group :label="$options.i18n.urlLabel" label-for="url" label-class="label-bold"> - <div class="input-group"> - <gl-form-input id="url" :readonly="true" :value="selectedService.url" /> - <span class="input-group-append"> - <clipboard-button :text="selectedService.url" :title="$options.i18n.copyToClipboard" /> - </span> - </div> + <gl-form-input-group id="url" :readonly="true" :value="selectedService.url"> + <template #append> + <clipboard-button + :text="selectedService.url" + :title="$options.i18n.copyToClipboard" + class="gl-m-0!" + /> + </template> + </gl-form-input-group> <span class="gl-text-gray-400"> {{ prometheusInfo }} </span> @@ -308,15 +407,20 @@ export default { label-for="authorization-key" label-class="label-bold" > - <div class="input-group"> - <gl-form-input id="authorization-key" :readonly="true" :value="selectedService.authKey" /> - <span class="input-group-append"> + <gl-form-input-group + id="authorization-key" + class="gl-mb-2" + :readonly="true" + :value="selectedService.authKey" + > + <template #append> <clipboard-button :text="selectedService.authKey" :title="$options.i18n.copyToClipboard" + class="gl-m-0!" /> - </span> - </div> + </template> + </gl-form-input-group> <gl-button v-gl-modal.authKeyModal class="gl-mt-3">{{ $options.i18n.resetKey }}</gl-button> <gl-modal modal-id="authKeyModal" @@ -328,11 +432,32 @@ export default { {{ $options.i18n.restKeyInfo }} </gl-modal> </gl-form-group> + <gl-form-group + v-if="glFeatures.alertIntegrationsDropdown" + :label="$options.i18n.alertJson" + label-for="alert-json" + label-class="label-bold" + :invalid-feedback="testAlert.error" + > + <gl-form-textarea + id="alert-json" + v-model.trim="testAlert.json" + :disabled="!selectedService.active" + :state="jsonIsValid" + :placeholder="$options.i18n.alertJsonPlaceholder" + rows="6" + max-rows="10" + /> + </gl-form-group> + <gl-button :disabled="!canTestAlert" @click="validateTestAlert">{{ + $options.i18n.testAlertInfo + }}</gl-button> <div - class="footer-block row-content-block gl-display-flex gl-justify-content-space-between d-none" + v-if="glFeatures.alertIntegrationsDropdown" + class="footer-block row-content-block gl-display-flex gl-justify-content-space-between" > - <gl-button type="submit" variant="success" category="primary"> - {{ __('Save and test changes') }} + <gl-button type="submit" variant="success" category="primary" :disabled="!canSaveConfig"> + {{ __('Save changes') }} </gl-button> <gl-button type="reset" variant="default" category="primary"> {{ __('Cancel') }} diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js index 15618978145..0a022a07352 100644 --- a/app/assets/javascripts/alerts_settings/constants.js +++ b/app/assets/javascripts/alerts_settings/constants.js @@ -21,6 +21,7 @@ export const i18n = { 'AlertSettings|Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.', ), endPointActivated: s__('AlertSettings|Alerts endpoint successfully activated.'), + changesSaved: s__('AlertSettings|Your changes were successfully updated.'), prometheusInfo: s__('AlertSettings|Add URL and auth key to your Prometheus config file'), integrationsInfo: s__( 'AlertSettings|Learn more about our %{linkStart}upcoming integrations%{linkEnd}', @@ -32,10 +33,20 @@ export const i18n = { authKeyLabel: s__('AlertSettings|Authorization key'), urlLabel: s__('AlertSettings|Webhook URL'), activeLabel: s__('AlertSettings|Active'), - apiBaseUrlHelpText: s__(' AlertSettings|URL cannot be blank and must start with http or https'), + apiBaseUrlHelpText: s__('AlertSettings|URL cannot be blank and must start with http or https'), + testAlertInfo: s__('AlertSettings|Test alert payload'), + alertJson: s__('AlertSettings|Alert test payload'), + alertJsonPlaceholder: s__('AlertSettings|Enter test alert JSON....'), + testAlertFailed: s__('AlertSettings|Test failed. Do you still want to save your changes anyway?'), + testAlertSuccess: s__( + 'AlertSettings|Test alert sent successfully. If you have made other changes, please save them now.', + ), + authKeyRest: s__('AlertSettings|Authorization key has been successfully reset'), }; export const serviceOptions = [ { value: 'generic', text: s__('AlertSettings|Generic') }, { value: 'prometheus', text: s__('AlertSettings|External Prometheus') }, ]; + +export const JSON_VALIDATE_DELAY = 250; diff --git a/app/assets/javascripts/alerts_settings/services/index.js b/app/assets/javascripts/alerts_settings/services/index.js index 669c40bc86b..c49992d4f57 100644 --- a/app/assets/javascripts/alerts_settings/services/index.js +++ b/app/assets/javascripts/alerts_settings/services/index.js @@ -1,3 +1,4 @@ +/* eslint-disable @gitlab/require-i18n-strings */ import axios from '~/lib/utils/axios_utils'; export default { @@ -24,4 +25,12 @@ export default { }, }); }, + updateTestAlert({ endpoint, data, authKey }) { + return axios.post(endpoint, data, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authKey}`, + }, + }); + }, }; diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index e9f84eb8648..55b3eaf9737 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,6 +1,6 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; -import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import { modalTypes } from '../constants'; import FindFile from '~/vue_shared/components/file_finder/index.vue'; @@ -24,7 +24,7 @@ export default { FindFile, ErrorMessage, CommitEditorHeader, - GlDeprecatedButton, + GlButton, GlLoadingIcon, RightPane, }, @@ -121,15 +121,16 @@ export default { ) }} </p> - <gl-deprecated-button + <gl-button variant="success" + category="primary" :title="__('New file')" :aria-label="__('New file')" data-qa-selector="first_file_button" @click="createNewFile()" > {{ __('New file') }} - </gl-deprecated-button> + </gl-button> </template> <gl-loading-icon v-else-if="!currentTree || currentTree.loading" size="md" /> <p v-else> diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue index b36a83c4974..9af1887ef12 100644 --- a/app/assets/javascripts/issuables_list/components/issuable.vue +++ b/app/assets/javascripts/issuables_list/components/issuable.vue @@ -7,6 +7,7 @@ // TODO: need to move this component to graphql - https://gitlab.com/gitlab-org/gitlab/-/issues/221246 import { escape, isNumber } from 'lodash'; import { GlLink, GlTooltipDirective as GlTooltip, GlSprintf, GlLabel, GlIcon } from '@gitlab/ui'; +import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg'; import { dateInWords, formatDate, @@ -25,6 +26,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { i18n: { openedAgo: __('opened %{timeAgoString} by %{user}'), + openedAgoJira: __('opened %{timeAgoString} by %{user} in Jira'), }, components: { IssueAssignees, @@ -60,6 +62,11 @@ export default { }, }, }, + data() { + return { + jiraLogo, + }; + }, computed: { milestoneLink() { const { title } = this.issuable.milestone; @@ -87,6 +94,9 @@ export default { isClosed() { return this.issuable.state === 'closed'; }, + isJiraIssue() { + return this.issuable.external_tracker === 'jira'; + }, issueCreatedToday() { return getDayDifference(new Date(this.issuable.created_at), new Date()) < 1; }, @@ -223,7 +233,18 @@ export default { :title="$options.confidentialTooltipText" :aria-label="$options.confidentialTooltipText" ></i> - <gl-link :href="issuable.web_url">{{ issuable.title }}</gl-link> + <gl-link + :href="issuable.web_url" + :target="isJiraIssue ? '_blank' : null" + data-testid="issuable-title" + > + {{ issuable.title }} + <gl-icon + v-if="isJiraIssue" + name="external-link" + class="gl-vertical-align-text-bottom" + /> + </gl-link> </span> <span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block"> {{ issuable.task_status }} @@ -231,11 +252,21 @@ export default { </div> <div class="issuable-info"> - <span class="js-ref-path">{{ referencePath }}</span> + <span class="js-ref-path"> + <span + v-if="isJiraIssue" + class="svg-container jira-logo-container" + data-testid="jira-logo" + v-html="jiraLogo" + ></span> + {{ referencePath }} + </span> <span data-testid="openedByMessage" class="d-none d-sm-inline-block mr-1"> · - <gl-sprintf :message="$options.i18n.openedAgo"> + <gl-sprintf + :message="isJiraIssue ? $options.i18n.openedAgoJira : $options.i18n.openedAgo" + > <template #timeAgoString> <span>{{ issuableCreatedAt }}</span> </template> @@ -302,6 +333,7 @@ export default { <!-- Issuable meta --> <div class="flex-shrink-0 d-flex flex-column align-items-end justify-content-center"> <div class="controls d-flex"> + <span v-if="isJiraIssue"> </span> <span v-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span> <issue-assignees @@ -326,6 +358,7 @@ export default { </template> <gl-link + v-if="!isJiraIssue" v-gl-tooltip class="ml-2 js-notes" :href="`${issuable.web_url}#notes`" diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue index 1c395fd9795..db18bcbce09 100644 --- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue @@ -118,6 +118,29 @@ export default { baseUrl() { return window.location.href.replace(/(\?.*)?(#.*)?$/, ''); }, + paginationNext() { + return this.page + 1; + }, + paginationPrev() { + return this.page - 1; + }, + paginationProps() { + const paginationProps = { value: this.page }; + + if (this.totalItems) { + return { + ...paginationProps, + perPage: this.itemsPerPage, + totalItems: this.totalItems, + }; + } + + return { + ...paginationProps, + prevPage: this.paginationPrev, + nextPage: this.paginationNext, + }; + }, }, watch: { selection() { @@ -272,11 +295,8 @@ export default { </ul> <div class="mt-3"> <gl-pagination - v-if="totalItems" - :value="page" - :per-page="itemsPerPage" - :total-items="totalItems" - class="justify-content-center" + v-bind="paginationProps" + class="gl-justify-content-center" @input="onPaginate" /> </div> diff --git a/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js b/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js new file mode 100644 index 00000000000..260ba69b4bc --- /dev/null +++ b/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js @@ -0,0 +1,5 @@ +import initIssuablesList from '~/issuables_list'; + +document.addEventListener('DOMContentLoaded', () => { + initIssuablesList(); +}); diff --git a/app/assets/stylesheets/pages/issues/issues_list.scss b/app/assets/stylesheets/pages/issues/issues_list.scss new file mode 100644 index 00000000000..c0af7a6af6d --- /dev/null +++ b/app/assets/stylesheets/pages/issues/issues_list.scss @@ -0,0 +1,5 @@ +.svg-container.jira-logo-container { + svg { + vertical-align: text-bottom; + } +} diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index e02b4eff9f0..94af1df2ccb 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -117,8 +117,3 @@ .gl-border-b-2 { border-bottom-width: $gl-border-size-2; } - -// Remove once this MR has been merged in GitLab UI > https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1539 -.gl-min-w-full { - min-width: 100%; -} diff --git a/app/finders/ci/variables_finder.rb b/app/finders/ci/variables_finder.rb new file mode 100644 index 00000000000..d933643ffb2 --- /dev/null +++ b/app/finders/ci/variables_finder.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Ci + class VariablesFinder + attr_reader :project, :params + + def initialize(project, params) + @project, @params = project, params + + raise ArgumentError, 'Please provide params[:key]' if params[:key].blank? + end + + def execute + variables = project.variables + variables = by_key(variables) + variables = by_environment_scope(variables) + variables + end + + private + + def by_key(variables) + variables.by_key(params[:key]) + end + + def by_environment_scope(variables) + environment_scope = params.dig(:filter, :environment_scope) + environment_scope.present? ? variables.by_environment_scope(environment_scope) : variables + end + end +end diff --git a/app/helpers/operations_helper.rb b/app/helpers/operations_helper.rb index 7500f7b1ff4..419fdd521c2 100644 --- a/app/helpers/operations_helper.rb +++ b/app/helpers/operations_helper.rb @@ -17,7 +17,7 @@ module OperationsHelper def alerts_settings_data { - 'prometheus_activated' => prometheus_service.activated?.to_s, + 'prometheus_activated' => prometheus_service.manual_configuration?.to_s, 'activated' => alerts_service.activated?.to_s, 'prometheus_form_path' => scoped_integration_path(prometheus_service), 'form_path' => scoped_integration_path(alerts_service), diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb index 8245729a884..628749b32cb 100644 --- a/app/models/ci/instance_variable.rb +++ b/app/models/ci/instance_variable.rb @@ -45,13 +45,5 @@ module Ci end end end - - private - - def validate_plan_limit_not_exceeded - if Gitlab::Ci::Features.instance_level_variables_limit_enabled? - super - end - end end end diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 08d39595c61..13358b95a47 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -18,5 +18,7 @@ module Ci } scope :unprotected, -> { where(protected: false) } + scope :by_key, -> (key) { where(key: key) } + scope :by_environment_scope, -> (environment_scope) { where(environment_scope: environment_scope) } end end diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb index 7ea5382a4fa..10df5e1a8dc 100644 --- a/app/models/concerns/ci/contextable.rb +++ b/app/models/concerns/ci/contextable.rb @@ -84,8 +84,6 @@ module Ci end def secret_instance_variables - return [] unless ::Feature.enabled?(:ci_instance_level_variables, project, default_enabled: true) - project.ci_instance_variables_for(ref: git_ref) end diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb index 6cf012680d8..c0fa14d3369 100644 --- a/app/models/concerns/update_project_statistics.rb +++ b/app/models/concerns/update_project_statistics.rb @@ -35,8 +35,8 @@ module UpdateProjectStatistics @project_statistics_name = project_statistics_name @statistic_attribute = statistic_attribute - after_save(:update_project_statistics_after_save, if: :update_project_statistics_attribute_changed?) - after_destroy(:update_project_statistics_after_destroy, unless: :project_destroyed?) + after_save(:update_project_statistics_after_save, if: :update_project_statistics_after_save?) + after_destroy(:update_project_statistics_after_destroy, if: :update_project_statistics_after_destroy?) end private :update_project_statistics @@ -45,6 +45,14 @@ module UpdateProjectStatistics included do private + def update_project_statistics_after_save? + update_project_statistics_attribute_changed? + end + + def update_project_statistics_after_destroy? + !project_destroyed? + end + def update_project_statistics_after_save attr = self.class.statistic_attribute delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 55c7c6ab682..f153bfe3f5b 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -12,7 +12,7 @@ class ProjectStatistics < ApplicationRecord before_save :update_storage_size COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size].freeze - INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze + INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size], snippets_size: %i[storage_size] }.freeze NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size].freeze scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) } diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 5f45407c05e..eb3960ff12b 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -44,7 +44,9 @@ class Snippet < ApplicationRecord has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :user_mentions, class_name: "SnippetUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_one :snippet_repository, inverse_of: :snippet - has_one :statistics, class_name: 'SnippetStatistics' + + # We need to add the `dependent` in order to call the after_destroy callback + has_one :statistics, class_name: 'SnippetStatistics', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent delegate :name, :email, to: :author, prefix: true, allow_nil: true diff --git a/app/models/snippet_statistics.rb b/app/models/snippet_statistics.rb index 8030328ebe4..7439f98d114 100644 --- a/app/models/snippet_statistics.rb +++ b/app/models/snippet_statistics.rb @@ -1,11 +1,19 @@ # frozen_string_literal: true class SnippetStatistics < ApplicationRecord + include AfterCommitQueue + include UpdateProjectStatistics + belongs_to :snippet validates :snippet, presence: true - delegate :repository, to: :snippet + update_project_statistics project_statistics_name: :snippets_size, statistic_attribute: :repository_size + + delegate :repository, :project, :project_id, to: :snippet + + after_save :update_author_root_storage_statistics, if: :update_author_root_storage_statistics? + after_destroy :update_author_root_storage_statistics, unless: :project_snippet? def update_commit_count self.commit_count = repository.commit_count @@ -32,4 +40,30 @@ class SnippetStatistics < ApplicationRecord save! end + + private + + alias_method :original_update_project_statistics_after_save?, :update_project_statistics_after_save? + def update_project_statistics_after_save? + project_snippet? && original_update_project_statistics_after_save? + end + + alias_method :original_update_project_statistics_after_destroy?, :update_project_statistics_after_destroy? + def update_project_statistics_after_destroy? + project_snippet? && original_update_project_statistics_after_destroy? + end + + def update_author_root_storage_statistics? + !project_snippet? && saved_change_to_repository_size? + end + + def update_author_root_storage_statistics + run_after_commit do + Namespaces::ScheduleAggregationWorker.perform_async(snippet.author.namespace_id) + end + end + + def project_snippet? + snippet.is_a?(ProjectSnippet) + end end diff --git a/app/services/snippets/destroy_service.rb b/app/services/snippets/destroy_service.rb index 146a0b53fc1..977626fcf17 100644 --- a/app/services/snippets/destroy_service.rb +++ b/app/services/snippets/destroy_service.rb @@ -27,11 +27,6 @@ module Snippets attempt_destroy! - # Update project statistics if the snippet is a Project one - if snippet.project_id - ProjectCacheWorker.perform_async(snippet.project_id, [], [:snippets_size]) - end - ServiceResponse.success(message: 'Snippet was deleted.') rescue DestroyError service_response_error('Failed to remove snippet repository.', 400) diff --git a/app/services/snippets/update_statistics_service.rb b/app/services/snippets/update_statistics_service.rb index 61fa43e7755..295cb963ccc 100644 --- a/app/services/snippets/update_statistics_service.rb +++ b/app/services/snippets/update_statistics_service.rb @@ -16,11 +16,6 @@ module Snippets snippet.repository.expire_statistics_caches statistics.refresh! - # Update project statistics if the snippet is a Project one - if snippet.project_id - ProjectCacheWorker.perform_async(snippet.project_id, [], [:snippets_size]) - end - ServiceResponse.success(message: 'Snippet statistics successfully updated.') end diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index d65563a6eba..737e4f66dd2 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -24,7 +24,7 @@ .control = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do - = search_field_tag :search, params[:search], { placeholder: _('Search by message'), id: 'commits-search', class: 'form-control search-text-input input-short gl-mt-3 mt-sm-0 gl-min-w-full', spellcheck: false } + = search_field_tag :search, params[:search], { placeholder: _('Search by message'), id: 'commits-search', class: 'form-control search-text-input input-short gl-mt-3 gl-sm-mt-0 gl-min-w-full', spellcheck: false } .control.d-none.d-md-block = link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do = icon("rss") diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index d80248f7e80..3036e918160 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -31,7 +31,7 @@ - revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?" - if can?(current_user, :admin_trigger, trigger) = link_to edit_project_trigger_path(@project, trigger), method: :get, title: "Edit", class: "btn btn-default btn-sm" do - %i.fa.fa-pencil + = sprite_icon('pencil') - if can?(current_user, :manage_trigger, trigger) = link_to project_trigger_path(@project, trigger), data: { confirm: revoke_trigger_confirmation }, method: :delete, title: "Revoke", class: "btn btn-default btn-warning btn-sm btn-trigger-revoke" do %i.fa.fa-trash |