Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue75
-rw-r--r--app/assets/javascripts/linked_resources/components/resource_links_block.vue111
-rw-r--r--app/assets/javascripts/linked_resources/constants.js14
-rw-r--r--app/assets/javascripts/linked_resources/index.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue101
-rw-r--r--app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql13
-rw-r--r--app/controllers/projects/pipelines/tests_controller.rb2
-rw-r--r--app/graphql/resolvers/ci/test_suite_resolver.rb2
-rw-r--r--app/helpers/users/callouts_helper.rb24
-rw-r--r--app/helpers/web_hooks/web_hooks_helper.rb27
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/hooks/project_hook.rb15
-rw-r--r--app/models/hooks/web_hook.rb9
-rw-r--r--app/models/note.rb8
-rw-r--r--app/models/user.rb17
-rw-r--r--app/models/users/namespace_callout.rb33
-rw-r--r--app/policies/project_policy.rb1
-rw-r--r--app/services/ci/build_report_result_service.rb2
-rw-r--r--app/services/ci/test_failure_history_service.rb2
-rw-r--r--app/services/web_hooks/log_execution_service.rb1
-rw-r--r--app/views/errors/omniauth_error.html.haml16
-rw-r--r--app/views/projects/pipelines/_info.html.haml12
25 files changed, 286 insertions, 224 deletions
diff --git a/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue b/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue
deleted file mode 100644
index 6a0deb41fd1..00000000000
--- a/app/assets/javascripts/linked_resources/components/add_issuable_resource_link_form.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-<script>
-import { GlFormGroup, GlButton, GlFormInput } from '@gitlab/ui';
-import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
-import { resourceLinksFormI18n } from '../constants';
-
-export default {
- name: 'AddIssuableResourceLinkForm',
- components: {
- GlFormGroup,
- GlButton,
- GlFormInput,
- },
- i18n: resourceLinksFormI18n,
- directives: {
- autofocusonshow,
- },
- props: {
- isSubmitting: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- linkTextValue: '',
- linkValue: '',
- };
- },
- computed: {
- isSubmitButtonDisabled() {
- return this.linkValue.length === 0 || this.isSubmitting;
- },
- },
- methods: {
- onFormCancel() {
- this.linkValue = '';
- this.linkTextValue = '';
- this.$emit('add-issuable-resource-link-form-cancel');
- },
- },
-};
-</script>
-
-<template>
- <form @submit.prevent>
- <gl-form-group :label="$options.i18n.linkTextLabel">
- <gl-form-input
- v-model="linkTextValue"
- v-autofocusonshow
- data-testid="link-text-input"
- type="text"
- />
- </gl-form-group>
- <gl-form-group :label="$options.i18n.linkValueLabel">
- <gl-form-input v-model="linkValue" data-testid="link-value-input" type="text" />
- </gl-form-group>
- <div class="gl-mt-5 gl-clearfix">
- <gl-button
- category="primary"
- variant="confirm"
- data-testid="add-button"
- :disabled="isSubmitButtonDisabled"
- :loading="isSubmitting"
- type="submit"
- class="gl-float-left"
- >
- {{ $options.i18n.submitButtonText }}
- </gl-button>
- <gl-button class="gl-float-right" @click="onFormCancel">
- {{ $options.i18n.cancelButtonText }}
- </gl-button>
- </div>
- </form>
-</template>
diff --git a/app/assets/javascripts/linked_resources/components/resource_links_block.vue b/app/assets/javascripts/linked_resources/components/resource_links_block.vue
deleted file mode 100644
index 46c4fc7f632..00000000000
--- a/app/assets/javascripts/linked_resources/components/resource_links_block.vue
+++ /dev/null
@@ -1,111 +0,0 @@
-<script>
-import { GlLink, GlIcon, GlButton } from '@gitlab/ui';
-import { resourceLinksI18n } from '../constants';
-import AddIssuableResourceLinkForm from './add_issuable_resource_link_form.vue';
-
-export default {
- name: 'ResourceLinksBlock',
- components: {
- GlLink,
- GlButton,
- GlIcon,
- AddIssuableResourceLinkForm,
- },
- i18n: resourceLinksI18n,
- props: {
- helpPath: {
- type: String,
- required: false,
- default: '',
- },
- canAddResourceLinks: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- isFormVisible: false,
- isSubmitting: false,
- };
- },
- computed: {
- badgeLabel() {
- return 0;
- },
- hasBody() {
- return this.isFormVisible;
- },
- },
- methods: {
- async toggleResourceLinkForm() {
- this.isFormVisible = !this.isFormVisible;
- },
- hideResourceLinkForm() {
- this.isFormVisible = false;
- },
- },
-};
-</script>
-
-<template>
- <div id="resource-links" class="gl-mt-5">
- <div class="card card-slim gl-overflow-hidden">
- <div
- :class="{ 'panel-empty-heading border-bottom-0': !hasBody }"
- class="card-header gl-display-flex gl-justify-content-space-between"
- >
- <h3
- class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7"
- >
- <gl-link
- id="user-content-resource-links"
- class="anchor position-absolute gl-text-decoration-none"
- href="#resource-links"
- aria-hidden="true"
- />
- <slot name="header-text">{{ $options.i18n.headerText }}</slot>
- <gl-link
- :href="helpPath"
- target="_blank"
- class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
- data-testid="help-link"
- :aria-label="$options.i18n.helpText"
- >
- <gl-icon name="question" :size="12" />
- </gl-link>
-
- <div class="gl-display-inline-flex">
- <div class="gl-display-inline-flex gl-mx-5">
- <span class="gl-display-inline-flex gl-align-items-center">
- <gl-icon name="link" class="gl-mr-2 gl-text-gray-500" />
- {{ badgeLabel }}
- </span>
- </div>
- <gl-button
- v-if="canAddResourceLinks"
- icon="plus"
- :aria-label="$options.i18n.addButtonText"
- @click="toggleResourceLinkForm"
- />
- </div>
- </h3>
- </div>
- <div
- class="linked-issues-card-body bg-gray-light"
- :class="{
- 'gl-p-5': isFormVisible,
- }"
- >
- <div v-show="isFormVisible" class="card-body bordered-box gl-bg-white">
- <add-issuable-resource-link-form
- ref="resourceLinkForm"
- :is-submitting="isSubmitting"
- @add-issuable-resource-link-form-cancel="hideResourceLinkForm"
- />
- </div>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/linked_resources/constants.js b/app/assets/javascripts/linked_resources/constants.js
deleted file mode 100644
index 1b11cfc5f88..00000000000
--- a/app/assets/javascripts/linked_resources/constants.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { s__ } from '~/locale';
-
-export const resourceLinksI18n = Object.freeze({
- headerText: s__('LinkedResources|Linked resources'),
- helpText: s__('LinkedResources|Read more about linked resources'),
- addButtonText: s__('LinkedResources|Add a resource link'),
-});
-
-export const resourceLinksFormI18n = Object.freeze({
- linkTextLabel: s__('LinkedResources|Text (Optional)'),
- linkValueLabel: s__('LinkedResources|Link'),
- submitButtonText: s__('LinkedResources|Add'),
- cancelButtonText: s__('LinkedResources|Cancel'),
-});
diff --git a/app/assets/javascripts/linked_resources/index.js b/app/assets/javascripts/linked_resources/index.js
index 4ac9ca31a84..244adca86c9 100644
--- a/app/assets/javascripts/linked_resources/index.js
+++ b/app/assets/javascripts/linked_resources/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
+import ResourceLinksBlock from 'ee_component/linked_resources/components/resource_links_block.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
-import ResourceLinksBlock from './components/resource_links_block.vue';
export default function initLinkedResources() {
const linkedResourcesRootElement = document.querySelector('.js-linked-resources-root');
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
index 56da8e88b7a..bfa99c01c3f 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue
@@ -1,4 +1,5 @@
<script>
+import { uniqueId } from 'lodash';
import { GlIcon, GlPopover, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { timeTilRun } from '../../utils';
@@ -43,6 +44,11 @@ export default {
CLEANUP_STATUS_UNFINISHED,
PARTIAL_CLEANUP_CONTINUE_MESSAGE,
},
+ data() {
+ return {
+ iconId: uniqueId('status-info-'),
+ };
+ },
computed: {
showStatus() {
return this.status !== UNSCHEDULED_STATUS;
@@ -85,14 +91,14 @@ export default {
</span>
<gl-icon
v-if="failedDelete"
- id="status-info"
+ :id="iconId"
:size="14"
class="gl-text-gray-500"
data-testid="extra-info"
name="information-o"
/>
<gl-popover
- target="status-info"
+ :target="iconId"
container="status-popover-container"
v-bind="$options.statusPopoverOptions"
>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
index 7ac9395c725..176f84f6c1a 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/index.js
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -1,9 +1,11 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { GlToast } from '@gitlab/ui';
import createDefaultClient from '~/lib/graphql';
import WorkItemLinks from './work_item_links.vue';
Vue.use(VueApollo);
+Vue.use(GlToast);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
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 60b30a82e9d..89f086cfca5 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
@@ -11,6 +11,7 @@ import {
} from '../../constants';
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
import WorkItemLinksForm from './work_item_links_form.vue';
+import WorkItemLinksMenu from './work_item_links_menu.vue';
export default {
components: {
@@ -19,6 +20,7 @@ export default {
GlIcon,
GlLoadingIcon,
WorkItemLinksForm,
+ WorkItemLinksMenu,
},
props: {
workItemId: {
@@ -156,19 +158,22 @@ export default {
<div
v-for="child in children"
:key="child.id"
- 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"
+ 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>
<gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-3 gl-text-gray-700" />
<span class="gl-word-break-all">{{ child.title }}</span>
</div>
- <div class="gl-ml-0 gl-sm-ml-auto! gl-mt-3 gl-sm-mt-0">
+ <div
+ class="gl-ml-0 gl-sm-ml-auto! gl-mt-3 gl-sm-mt-0 gl-display-inline-flex gl-align-items-center"
+ >
<gl-badge :variant="badgeVariant(child.state)">
<span class="gl-sm-display-block">{{
$options.WORK_ITEM_STATUS_TEXT[child.state]
}}</span>
</gl-badge>
+ <work-item-links-menu :work-item-id="child.id" :parent-work-item-id="issuableGid" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue
new file mode 100644
index 00000000000..6deb87c5dca
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue
@@ -0,0 +1,101 @@
+<script>
+import { GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { produce } from 'immer';
+import { s__ } from '~/locale';
+import changeWorkItemParentMutation from '../../graphql/change_work_item_parent_link.mutation.graphql';
+import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
+import { WIDGET_TYPE_HIERARCHY } from '../../constants';
+
+export default {
+ components: {
+ GlDropdownItem,
+ GlDropdown,
+ GlIcon,
+ },
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ parentWorkItemId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ activeToast: null,
+ };
+ },
+ methods: {
+ toggleChildFromCache(data, store) {
+ const sourceData = store.readQuery({
+ query: getWorkItemLinksQuery,
+ variables: { id: this.parentWorkItemId },
+ });
+
+ const newData = produce(sourceData, (draftState) => {
+ const widgetHierarchy = draftState.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
+ );
+
+ const index = widgetHierarchy.children.nodes.findIndex(
+ (child) => child.id === this.workItemId,
+ );
+
+ if (index >= 0) {
+ widgetHierarchy.children.nodes.splice(index, 1);
+ } else {
+ widgetHierarchy.children.nodes.push(data.workItemUpdate.workItem);
+ }
+ });
+
+ store.writeQuery({
+ query: getWorkItemLinksQuery,
+ variables: { id: this.parentWorkItemId },
+ data: newData,
+ });
+ },
+ async addChild(data) {
+ const { data: resp } = await this.$apollo.mutate({
+ mutation: changeWorkItemParentMutation,
+ variables: { id: this.workItemId, parentId: this.parentWorkItemId },
+ update: this.toggleChildFromCache.bind(this, data),
+ });
+
+ if (resp.workItemUpdate.errors.length === 0) {
+ this.activeToast?.hide();
+ }
+ },
+ async removeChild() {
+ const { data } = await this.$apollo.mutate({
+ mutation: changeWorkItemParentMutation,
+ variables: { id: this.workItemId, parentId: null },
+ update: this.toggleChildFromCache.bind(this, null),
+ });
+
+ if (data.workItemUpdate.errors.length === 0) {
+ this.activeToast = this.$toast.show(s__('WorkItem|Child removed'), {
+ action: {
+ text: s__('WorkItem|Undo'),
+ onClick: this.addChild.bind(this, data),
+ },
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-ml-2">
+ <gl-dropdown category="tertiary" toggle-class="btn-icon" :right="true">
+ <template #button-content>
+ <gl-icon name="ellipsis_v" :size="14" />
+ </template>
+ <gl-dropdown-item @click="removeChild">
+ {{ s__('WorkItem|Remove') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </span>
+</template>
diff --git a/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql b/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql
new file mode 100644
index 00000000000..dc5286174d8
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql
@@ -0,0 +1,13 @@
+mutation changeWorkItemParentLink($id: WorkItemID!, $parentId: WorkItemID) {
+ workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: $parentId } }) {
+ workItem {
+ id
+ workItemType {
+ id
+ }
+ title
+ state
+ }
+ errors
+ }
+}
diff --git a/app/controllers/projects/pipelines/tests_controller.rb b/app/controllers/projects/pipelines/tests_controller.rb
index e42cb9b8422..8ac370b1bd4 100644
--- a/app/controllers/projects/pipelines/tests_controller.rb
+++ b/app/controllers/projects/pipelines/tests_controller.rb
@@ -51,7 +51,7 @@ module Projects
def test_suite
suite = builds.sum do |build|
- build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
+ build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
end
Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, project).load!
diff --git a/app/graphql/resolvers/ci/test_suite_resolver.rb b/app/graphql/resolvers/ci/test_suite_resolver.rb
index 5d61d9e986b..f758e217b47 100644
--- a/app/graphql/resolvers/ci/test_suite_resolver.rb
+++ b/app/graphql/resolvers/ci/test_suite_resolver.rb
@@ -28,7 +28,7 @@ module Resolvers
def load_test_suite_data(builds)
suite = builds.sum do |build|
- build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
+ build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
end
Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, pipeline.project).load!
diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb
index 87c8bf5cb28..3dd6b3f4a80 100644
--- a/app/helpers/users/callouts_helper.rb
+++ b/app/helpers/users/callouts_helper.rb
@@ -11,6 +11,7 @@ module Users
UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
+ WEB_HOOK_DISABLED = 'web_hook_disabled'
def show_gke_cluster_integration_callout?(project)
active_nav_link?(controller: sidebar_operations_paths) &&
@@ -60,12 +61,31 @@ module Users
!user_dismissed?(SECURITY_NEWSLETTER_CALLOUT)
end
+ def web_hook_disabled_dismissed?(project)
+ return false unless project
+
+ last_failure = Gitlab::Redis::SharedState.with do |redis|
+ key = "web_hooks:last_failure:project-#{project.id}"
+ redis.get(key)
+ end
+
+ last_failure = DateTime.parse(last_failure) if last_failure
+
+ user_dismissed?(WEB_HOOK_DISABLED, last_failure, namespace: project.namespace)
+ end
+
private
- def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
+ def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, namespace: nil)
return false unless current_user
- current_user.dismissed_callout?(feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
+ query = { feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than }
+
+ if namespace
+ current_user.dismissed_callout_for_namespace?(namespace: namespace, **query)
+ else
+ current_user.dismissed_callout?(**query)
+ end
end
end
end
diff --git a/app/helpers/web_hooks/web_hooks_helper.rb b/app/helpers/web_hooks/web_hooks_helper.rb
new file mode 100644
index 00000000000..95122750c2f
--- /dev/null
+++ b/app/helpers/web_hooks/web_hooks_helper.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module WebHooks
+ module WebHooksHelper
+ EXPIRY_TTL = 1.hour
+
+ def show_project_hook_failed_callout?(project:)
+ return false unless current_user
+ return false unless Feature.enabled?(:webhooks_failed_callout, project)
+ return false unless Feature.enabled?(:web_hooks_disable_failed, project)
+ return false unless Ability.allowed?(current_user, :read_web_hooks, project)
+
+ # Assumes include of Users::CalloutsHelper
+ return false if web_hook_disabled_dismissed?(project)
+
+ any_project_hook_failed?(project) # Most expensive query last
+ end
+
+ private
+
+ def any_project_hook_failed?(project)
+ Rails.cache.fetch("any_web_hook_failed:#{project.id}", expires_in: EXPIRY_TTL) do
+ ProjectHook.for_projects(project).disabled.exists?
+ end
+ end
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 791bae17271..78b55680b5e 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1090,7 +1090,7 @@ module Ci
end
def test_reports
- Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
+ Gitlab::Ci::Reports::TestReport.new.tap do |test_reports|
latest_test_report_builds.find_each do |build|
build.collect_test_reports!(test_reports)
end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index b7ace34141e..bcbf43ee38b 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -27,6 +27,8 @@ class ProjectHook < WebHook
belongs_to :project
validates :project, presence: true
+ scope :for_projects, ->(project) { where(project: project) }
+
def pluralized_name
_('Webhooks')
end
@@ -41,6 +43,19 @@ class ProjectHook < WebHook
project
end
+ override :update_last_failure
+ def update_last_failure
+ return if executable?
+
+ key = "web_hooks:last_failure:project-#{project_id}"
+ time = Time.current.utc.iso8601
+
+ Gitlab::Redis::SharedState.with do |redis|
+ prev = redis.get(key)
+ redis.set(key, time) if !prev || prev < time
+ end
+ end
+
private
override :web_hooks_disable_failed?
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 71e7206718e..f428d07cd7f 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -48,6 +48,11 @@ class WebHook < ApplicationRecord
where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current)
end
+ # Inverse of executable
+ scope :disabled, -> do
+ where('recent_failures > ? OR disabled_until >= ?', FAILURE_THRESHOLD, Time.current)
+ end
+
def executable?
!temporarily_disabled? && !permanently_disabled?
end
@@ -181,6 +186,10 @@ class WebHook < ApplicationRecord
raise InterpolationError, "Invalid URL template. Missing key #{e.key}"
end
+ def update_last_failure
+ # Overridden in child classes.
+ end
+
private
def web_hooks_disable_failed?
diff --git a/app/models/note.rb b/app/models/note.rb
index f2ddf0efe47..986a85acac6 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -111,6 +111,7 @@ class Note < ApplicationRecord
end
validate :does_not_exceed_notes_limit?, on: :create, unless: [:system?, :importing?]
+ validate :validate_created_after
# @deprecated attachments are handled by the Upload model.
#
@@ -748,6 +749,13 @@ class Note < ApplicationRecord
errors.add(:base, _('Maximum number of comments exceeded')) if noteable.notes.count >= Noteable::MAX_NOTES_LIMIT
end
+ def validate_created_after
+ return unless created_at
+ return if created_at >= '1970-01-01'
+
+ errors.add(:created_at, s_('Note|The created date provided is too far in the past.'))
+ end
+
def noteable_label_url_method
for_merge_request? ? :project_merge_requests_url : :project_issues_url
end
diff --git a/app/models/user.rb b/app/models/user.rb
index dc2f36a9ddb..12f434db631 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -222,6 +222,7 @@ class User < ApplicationRecord
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'Users::Callout'
has_many :group_callouts, class_name: 'Users::GroupCallout'
+ has_many :namespace_callouts, class_name: 'Users::NamespaceCallout'
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
@@ -2085,6 +2086,13 @@ class User < ApplicationRecord
callout_dismissed?(callout, ignore_dismissal_earlier_than)
end
+ def dismissed_callout_for_namespace?(feature_name:, namespace:, ignore_dismissal_earlier_than: nil)
+ source_feature_name = "#{feature_name}_#{namespace.id}"
+ callout = namespace_callouts_by_feature_name[source_feature_name]
+
+ callout_dismissed?(callout, ignore_dismissal_earlier_than)
+ end
+
# Load the current highest access by looking directly at the user's memberships
def current_highest_access_level
members.non_request.maximum(:access_level)
@@ -2111,6 +2119,11 @@ class User < ApplicationRecord
.find_or_initialize_by(feature_name: ::Users::GroupCallout.feature_names[feature_name], group_id: group_id)
end
+ def find_or_initialize_namespace_callout(feature_name, namespace_id)
+ namespace_callouts
+ .find_or_initialize_by(feature_name: ::Users::NamespaceCallout.feature_names[feature_name], namespace_id: namespace_id)
+ end
+
def can_trigger_notifications?
confirmed? && !blocked? && !ghost?
end
@@ -2228,6 +2241,10 @@ class User < ApplicationRecord
@group_callouts_by_feature_name ||= group_callouts.index_by(&:source_feature_name)
end
+ def namespace_callouts_by_feature_name
+ @namespace_callouts_by_feature_name ||= namespace_callouts.index_by(&:source_feature_name)
+ end
+
def authorized_groups_without_shared_membership
Group.from_union([
groups.select(*Namespace.cached_column_list),
diff --git a/app/models/users/namespace_callout.rb b/app/models/users/namespace_callout.rb
new file mode 100644
index 00000000000..a20a196a4ef
--- /dev/null
+++ b/app/models/users/namespace_callout.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Users
+ class NamespaceCallout < ApplicationRecord
+ include Users::Calloutable
+
+ self.table_name = 'user_namespace_callouts'
+
+ belongs_to :namespace
+
+ enum feature_name: {
+ invite_members_banner: 1,
+ approaching_seat_count_threshold: 2, # EE-only
+ storage_enforcement_banner_first_enforcement_threshold: 3,
+ storage_enforcement_banner_second_enforcement_threshold: 4,
+ storage_enforcement_banner_third_enforcement_threshold: 5,
+ storage_enforcement_banner_fourth_enforcement_threshold: 6,
+ preview_user_over_limit_free_plan_alert: 7, # EE-only
+ user_reached_limit_free_plan_alert: 8, # EE-only
+ web_hook_disabled: 9
+ }
+
+ validates :namespace, presence: true
+ validates :feature_name,
+ presence: true,
+ uniqueness: { scope: [:user_id, :namespace_id] },
+ inclusion: { in: NamespaceCallout.feature_names.keys }
+
+ def source_feature_name
+ "#{feature_name}_#{namespace_id}"
+ end
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index c54dbefc1ae..850f25a6089 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -492,6 +492,7 @@ class ProjectPolicy < BasePolicy
enable :update_runners_registration_token
enable :admin_project_google_cloud
enable :admin_secure_files
+ enable :read_web_hooks
end
rule { public_project & metrics_dashboard_allowed }.policy do
diff --git a/app/services/ci/build_report_result_service.rb b/app/services/ci/build_report_result_service.rb
index 8bdb51320f9..f9146b3677a 100644
--- a/app/services/ci/build_report_result_service.rb
+++ b/app/services/ci/build_report_result_service.rb
@@ -22,7 +22,7 @@ module Ci
private
def generate_test_suite_report(build)
- build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
+ build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
end
def tests_params(test_suite)
diff --git a/app/services/ci/test_failure_history_service.rb b/app/services/ci/test_failure_history_service.rb
index 7323ad417ea..2214a6a2729 100644
--- a/app/services/ci/test_failure_history_service.rb
+++ b/app/services/ci/test_failure_history_service.rb
@@ -81,7 +81,7 @@ module Ci
def generate_test_suite!(build)
# Returns an instance of Gitlab::Ci::Reports::TestSuite
- build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
+ build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
end
def ci_unit_test_attrs(batch)
diff --git a/app/services/web_hooks/log_execution_service.rb b/app/services/web_hooks/log_execution_service.rb
index 0ee7c41469f..17dcf615830 100644
--- a/app/services/web_hooks/log_execution_service.rb
+++ b/app/services/web_hooks/log_execution_service.rb
@@ -44,6 +44,7 @@ module WebHooks
end
log_state_change
+ hook.update_last_failure
end
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
raise if raise_lock_error?
diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml
index e114e4609f8..3090c823677 100644
--- a/app/views/errors/omniauth_error.html.haml
+++ b/app/views/errors/omniauth_error.html.haml
@@ -2,14 +2,18 @@
.container
= render partial: "shared/errors/graphic_422", formats: :svg
- %h3 Sign-in using #{@provider} auth failed
+ %h3
+ = _('Sign-in using %{provider} auth failed') % { provider: @provider }
- %p.light.subtitle Sign-in failed because #{@error}.
+ %p.light.subtitle
+ = _('Sign-in failed because %{error}.') % { error: @error }
- %p Try logging in using your username or email. If you have forgotten your password, try recovering it
+ %p
+ = _('Try logging in using your username or email. If you have forgotten your password, try recovering it')
- = link_to "Sign in", new_session_path(:user), class: 'gl-button btn primary'
- = link_to "Recover password", new_password_path(:user), class: 'gl-button btn secondary'
+ = link_to _('Sign in'), new_session_path(:user), class: 'gl-button btn primary'
+ = link_to _('Recover password'), new_password_path(:user), class: 'gl-button btn secondary'
%hr
- %p.light If none of the options work, try contacting a GitLab administrator.
+ %p.light
+ = _('If none of the options work, try contacting a GitLab administrator.')
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 54435f675a7..07e299d71ea 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -6,9 +6,9 @@
= preserve(markdown(commit.description, pipeline: :single_line))
.info-well
- .well-segment.pipeline-info
- .icon-container.gl-vertical-align-text-bottom
- = sprite_icon('clock')
+ .well-segment.pipeline-info{ class: "gl-align-items-baseline!" }
+ .icon-container
+ = sprite_icon('clock', css_class: 'gl-top-0!')
= pluralize @pipeline.total_size, "job"
= @pipeline.ref_text
- if @pipeline.duration
@@ -20,7 +20,7 @@
- if has_pipeline_badges?(@pipeline)
.well-segment.qa-pipeline-badges
.icon-container
- = sprite_icon('flag')
+ = sprite_icon('flag', css_class: 'gl-top-0!')
- if @pipeline.child?
- text = sprintf(s_('Pipelines|Child pipeline (%{link_start}parent%{link_end})'), { link_start: "<a href='#{pipeline_path(@pipeline.triggered_by_pipeline)}' class='text-underline'>", link_end: "</a>"}).html_safe
= gl_badge_tag text, { variant: :info, size: :sm }, { class: 'js-pipeline-child has-tooltip', title: s_("Pipelines|This is a child pipeline within the parent pipeline") }
@@ -44,13 +44,13 @@
.well-segment.branch-info
.icon-container.commit-icon
- = custom_icon("icon_commit")
+ = sprite_icon('commit', css_class: 'gl-top-0!')
= link_to commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha"
= clipboard_button(text: @pipeline.sha, title: _("Copy commit SHA"))
.well-segment.related-merge-request-info
.icon-container
- = sprite_icon("git-merge")
+ = sprite_icon("git-merge", css_class: 'gl-top-0!')
%span.related-merge-requests
%span.js-truncated-mr-list
= @pipeline.all_related_merge_request_text(limit: 1)