summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-17 18:08:05 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-17 18:08:05 +0000
commit184c2ced0761bd8dd7032619d16d3983fed7944a (patch)
treecc82b32ee7c1797509da3cf384617e4ffa2e1733 /app
parent238d22c07218adf2b8f3db630ee8b74ca6f29df5 (diff)
downloadgitlab-ce-184c2ced0761bd8dd7032619d16d3983fed7944a.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue11
-rw-r--r--app/assets/javascripts/lib/utils/set.js9
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue26
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue29
-rw-r--r--app/assets/stylesheets/framework/blank.scss49
-rw-r--r--app/assets/stylesheets/framework/typography.scss4
-rw-r--r--app/controllers/groups/registry/repositories_controller.rb5
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/mailers/emails/releases.rb28
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/notification_setting.rb1
-rw-r--r--app/models/release.rb6
-rw-r--r--app/serializers/build_details_entity.rb24
-rw-r--r--app/services/notification_recipient_service.rb24
-rw-r--r--app/services/notification_service.rb9
-rw-r--r--app/views/admin/runners/index.html.haml5
-rw-r--r--app/views/dashboard/projects/_blank_state_admin_welcome.html.haml61
-rw-r--r--app/views/dashboard/projects/_blank_state_welcome.html.haml72
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml15
-rw-r--r--app/views/notify/new_release_email.html.haml18
-rw-r--r--app/views/notify/new_release_email.text.erb12
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml4
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/new_release_worker.rb14
26 files changed, 299 insertions, 143 deletions
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index b4b124d5db1..859f839741f 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -130,6 +130,10 @@ export default {
return title;
},
+
+ shouldRenderHeaderCallout() {
+ return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure;
+ },
},
watch: {
// Once the job log is loaded,
@@ -239,10 +243,9 @@ export default {
/>
</div>
- <callout
- v-if="shouldRenderCalloutMessage && !hasUnmetPrerequisitesFailure"
- :message="job.callout_message"
- />
+ <callout v-if="shouldRenderHeaderCallout">
+ <div v-html="job.callout_message"></div>
+ </callout>
</header>
<!-- EO Header Section -->
diff --git a/app/assets/javascripts/lib/utils/set.js b/app/assets/javascripts/lib/utils/set.js
new file mode 100644
index 00000000000..3845d648b61
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/set.js
@@ -0,0 +1,9 @@
+/**
+ * Checks if the first argument is a subset of the second argument.
+ * @param {Set} subset The set to be considered as the subset.
+ * @param {Set} superset The set to be considered as the superset.
+ * @returns {boolean}
+ */
+// eslint-disable-next-line import/prefer-default-export
+export const isSubset = (subset, superset) =>
+ Array.from(subset).every(value => superset.has(value));
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index ed48331f459..95f8270b5d0 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -1,6 +1,13 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import { GlLoadingIcon, GlButton, GlTooltipDirective, GlModal, GlModalDirective } from '@gitlab/ui';
+import {
+ GlLoadingIcon,
+ GlButton,
+ GlTooltipDirective,
+ GlModal,
+ GlModalDirective,
+ GlEmptyState,
+} from '@gitlab/ui';
import createFlash from '../../flash';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import Icon from '../../vue_shared/components/icon.vue';
@@ -17,6 +24,7 @@ export default {
GlButton,
Icon,
GlModal,
+ GlEmptyState,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -103,10 +111,18 @@ export default {
<div v-else-if="!repo.isLoading && isOpen" class="container-image-tags">
<table-registry v-if="repo.list.length" :repo="repo" :can-delete-repo="canDeleteRepo" />
-
- <div v-else class="nothing-here-block">
- {{ s__('ContainerRegistry|No tags in Container Registry for this container image.') }}
- </div>
+ <gl-empty-state
+ v-else
+ :title="s__('ContainerRegistry|This image has no active tags')"
+ :description="
+ s__(
+ `ContainerRegistry|The last tag related to this image was recently removed.
+ This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
+ If you have any questions, contact your administrator.`,
+ )
+ "
+ class="mx-auto my-0"
+ />
</div>
<gl-modal :modal-id="modalId" ok-variant="danger" @ok="handleDeleteRepository">
<template v-slot:modal-title>{{ s__('ContainerRegistry|Remove repository') }}</template>
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index ac7272c4d29..8470fbc2b59 100644
--- a/app/assets/javascripts/registry/components/table_registry.vue
+++ b/app/assets/javascripts/registry/components/table_registry.vue
@@ -43,6 +43,7 @@ export default {
},
data() {
return {
+ selectedItems: [],
itemsToBeDeleted: [],
modalId: `confirm-image-deletion-modal-${this.repo.id}`,
selectAllChecked: false,
@@ -96,6 +97,7 @@ export default {
},
deleteSingleItem(index) {
this.setModalDescription(index);
+ this.itemsToBeDeleted = [index];
this.$refs.deleteModal.$refs.modal.$once('ok', () => {
this.removeModalEvents();
@@ -103,9 +105,10 @@ export default {
});
},
deleteMultipleItems() {
- if (this.itemsToBeDeleted.length === 1) {
+ this.itemsToBeDeleted = [...this.selectedItems];
+ if (this.selectedItems.length === 1) {
this.setModalDescription(this.itemsToBeDeleted[0]);
- } else if (this.itemsToBeDeleted.length > 1) {
+ } else if (this.selectedItems.length > 1) {
this.setModalDescription();
}
@@ -115,6 +118,7 @@ export default {
});
},
handleSingleDelete(itemToDelete) {
+ this.itemsToBeDeleted = [];
this.deleteItem(itemToDelete)
.then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
@@ -122,6 +126,7 @@ export default {
handleMultipleDelete() {
const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = [];
+ this.selectedItems = [];
if (this.bulkDeletePath) {
this.multiDeleteItems({
@@ -150,23 +155,23 @@ export default {
}
},
selectAll() {
- this.itemsToBeDeleted = this.repo.list.map((x, index) => index);
+ this.selectedItems = this.repo.list.map((x, index) => index);
this.selectAllChecked = true;
},
deselectAll() {
- this.itemsToBeDeleted = [];
+ this.selectedItems = [];
this.selectAllChecked = false;
},
- updateItemsToBeDeleted(index) {
- const delIndex = this.itemsToBeDeleted.findIndex(x => x === index);
+ updateselectedItems(index) {
+ const delIndex = this.selectedItems.findIndex(x => x === index);
if (delIndex > -1) {
- this.itemsToBeDeleted.splice(delIndex, 1);
+ this.selectedItems.splice(delIndex, 1);
this.selectAllChecked = false;
} else {
- this.itemsToBeDeleted.push(index);
+ this.selectedItems.push(index);
- if (this.itemsToBeDeleted.length === this.repo.list.length) {
+ if (this.selectedItems.length === this.repo.list.length) {
this.selectAllChecked = true;
}
}
@@ -199,7 +204,7 @@ export default {
v-if="canDeleteRepo"
v-gl-tooltip
v-gl-modal="modalId"
- :disabled="!itemsToBeDeleted || itemsToBeDeleted.length === 0"
+ :disabled="!selectedItems || selectedItems.length === 0"
class="js-delete-registry float-right"
data-track-event="click_button"
data-track-label="bulk_registry_tag_delete"
@@ -219,8 +224,8 @@ export default {
<gl-form-checkbox
v-if="canDeleteRow(item)"
class="js-select-checkbox"
- :checked="itemsToBeDeleted && itemsToBeDeleted.includes(index)"
- @change="updateItemsToBeDeleted(index)"
+ :checked="selectedItems && selectedItems.includes(index)"
+ @change="updateselectedItems(index)"
/>
</td>
<td class="monospace">
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index cbd390e7145..7dd7ab339dd 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -14,13 +14,12 @@
.blank-state-row {
display: flex;
flex-wrap: wrap;
- justify-content: space-around;
- height: 100%;
+ justify-content: space-between;
}
.blank-state-welcome {
text-align: center;
- padding: 20px 0 40px;
+ padding: $gl-padding 0 ($gl-padding * 2);
.blank-state-welcome-title {
font-size: 24px;
@@ -32,23 +31,9 @@
}
.blank-state-link {
- display: block;
color: $gl-text-color;
- flex: 0 0 100%;
margin-bottom: 15px;
- @include media-breakpoint-up(sm) {
- flex: 0 0 49%;
-
- &:nth-child(odd) {
- margin-right: 5px;
- }
-
- &:nth-child(even) {
- margin-left: 5px;
- }
- }
-
&:hover {
background-color: $gray-light;
text-decoration: none;
@@ -63,15 +48,25 @@
}
.blank-state {
- padding: 20px;
+ display: flex;
+ align-items: center;
+ padding: 20px 50px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
+ min-height: 240px;
+ margin-bottom: $gl-padding;
+ width: calc(50% - #{$gl-padding-8});
+
+ @include media-breakpoint-down(sm) {
+ width: 100%;
+ flex-direction: column;
+ justify-content: center;
+ padding: 50px 20px;
+
+ .column-small & {
+ width: 100%;
+ }
- @include media-breakpoint-up(sm) {
- display: flex;
- height: 100%;
- align-items: center;
- padding: 50px 30px;
}
}
@@ -90,7 +85,7 @@
}
.blank-state-body {
- @include media-breakpoint-down(xs) {
+ @include media-breakpoint-down(sm) {
text-align: center;
margin-top: 20px;
}
@@ -121,9 +116,3 @@
}
}
}
-
-@include media-breakpoint-down(xs) {
- .blank-state-icon svg {
- width: 315px;
- }
-}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 9028bfa8ec9..3876d1c10d4 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -69,10 +69,6 @@
details {
margin-bottom: $gl-padding;
-
- summary {
- margin-bottom: $gl-padding;
- }
}
// Single code lines should wrap
diff --git a/app/controllers/groups/registry/repositories_controller.rb b/app/controllers/groups/registry/repositories_controller.rb
index 39f6963ee0a..e09a9e6eb21 100644
--- a/app/controllers/groups/registry/repositories_controller.rb
+++ b/app/controllers/groups/registry/repositories_controller.rb
@@ -4,6 +4,7 @@ module Groups
class RepositoriesController < Groups::ApplicationController
before_action :verify_container_registry_enabled!
before_action :authorize_read_container_image!
+ before_action :feature_flag_group_container_registry_browser!
def index
track_event(:list_repositories)
@@ -22,6 +23,10 @@ module Groups
private
+ def feature_flag_group_container_registry_browser!
+ render_404 unless Feature.enabled?(:group_container_registry_browser, group)
+ end
+
def verify_container_registry_enabled!
render_404 unless Gitlab.config.registry.enabled
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 811467ca03a..6ddcbf61090 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -22,7 +22,9 @@ module GroupsHelper
end
def group_container_registry_nav?
- Gitlab.config.registry.enabled && can?(current_user, :read_container_image, @group)
+ Gitlab.config.registry.enabled &&
+ can?(current_user, :read_container_image, @group) &&
+ Feature.enabled?(:group_container_registry_browser, @group)
end
def group_sidebar_links
diff --git a/app/mailers/emails/releases.rb b/app/mailers/emails/releases.rb
new file mode 100644
index 00000000000..137858d31e8
--- /dev/null
+++ b/app/mailers/emails/releases.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Emails
+ module Releases
+ def new_release_email(user_id, release, reason = nil)
+ @release = release
+ @project = @release.project
+ @target_url = namespace_project_releases_url(
+ namespace_id: @project.namespace,
+ project_id: @project
+ )
+
+ user = User.find(user_id)
+
+ mail(
+ to: user.notification_email_for(@project.group),
+ subject: subject(release_email_subject)
+ )
+ end
+
+ private
+
+ def release_email_subject
+ release_info = [@release.name, @release.tag].select(&:presence).join(' - ')
+ "New release: #{release_info}"
+ end
+ end
+end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index d0b43b4397f..c7cfefeec9b 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -16,6 +16,7 @@ class Notify < BaseMailer
include Emails::Members
include Emails::AutoDevops
include Emails::RemoteMirrors
+ include Emails::Releases
helper MilestonesHelper
helper MergeRequestsHelper
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b016aa8e477..c48ab28ce73 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -754,6 +754,10 @@ module Ci
true
end
+ def invalid_dependencies
+ dependencies.reject(&:valid_dependency?)
+ end
+
def runner_required_feature_names
strong_memoize(:runner_required_feature_names) do
RUNNER_FEATURES.select do |feature, method|
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 981590b688f..20160da62d2 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -25,6 +25,7 @@ class NotificationSetting < ApplicationRecord
end
EMAIL_EVENTS = [
+ :new_release,
:new_note,
:new_issue,
:reopen_issue,
diff --git a/app/models/release.rb b/app/models/release.rb
index add57367f61..5a7bfe2d495 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -26,10 +26,12 @@ class Release < ApplicationRecord
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
scope :sorted, -> { order(released_at: :desc) }
+ scope :with_project_and_namespace, -> { includes(project: :namespace) }
delegate :repository, to: :project
after_commit :create_evidence!, on: :create
+ after_commit :notify_new_release, on: :create
def commit
strong_memoize(:commit) do
@@ -73,6 +75,10 @@ class Release < ApplicationRecord
def create_evidence!
CreateEvidenceWorker.perform_async(self.id)
end
+
+ def notify_new_release
+ NewReleaseWorker.perform_async(id)
+ end
end
Release.prepend_if_ee('EE::Release')
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 0c754157267..480a8cab6ff 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -121,4 +121,28 @@ class BuildDetailsEntity < JobEntity
def can_admin_build?
can?(request.current_user, :admin_build, project)
end
+
+ def callout_message
+ return super unless build.failure_reason.to_sym == :missing_dependency_failure
+
+ docs_url = "https://docs.gitlab.com/ce/ci/yaml/README.html#dependencies"
+
+ [
+ failure_message.html_safe,
+ help_message(docs_url).html_safe
+ ].join("<br />")
+ end
+
+ def invalid_dependencies
+ build.invalid_dependencies.map(&:name).join(', ')
+ end
+
+ def failure_message
+ _("This job depends on other jobs with expired/erased artifacts: %{invalid_dependencies}") %
+ { invalid_dependencies: invalid_dependencies }
+ end
+
+ def help_message(docs_url)
+ _("Please refer to <a href=\"%{docs_url}\">%{docs_url}</a>") % { docs_url: docs_url }
+ end
end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index fca64270cae..9afbb678f5d 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -28,6 +28,10 @@ module NotificationRecipientService
Builder::ProjectMaintainers.new(*args).notification_recipients
end
+ def self.build_new_release_recipients(*args)
+ Builder::NewRelease.new(*args).notification_recipients
+ end
+
module Builder
class Base
def initialize(*)
@@ -359,6 +363,26 @@ module NotificationRecipientService
end
end
+ class NewRelease < Base
+ attr_reader :target
+
+ def initialize(target)
+ @target = target
+ end
+
+ def build!
+ add_recipients(target.project.authorized_users, :custom, nil)
+ end
+
+ def custom_action
+ :new_release
+ end
+
+ def acting_user
+ target.author
+ end
+ end
+
class MergeRequestUnmergeable < Base
attr_reader :target
def initialize(merge_request)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ed357aa0392..b56b2cf14e3 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -289,6 +289,15 @@ class NotificationService
end
end
+ # Notify users when a new release is created
+ def send_new_release_notifications(release)
+ recipients = NotificationRecipientService.build_new_release_recipients(release)
+
+ recipients.each do |recipient|
+ mailer.new_release_email(recipient.user.id, release, recipient.reason).deliver_later
+ end
+ end
+
# Members
def new_access_request(member)
return true unless member.notifiable?(:subscription)
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 545e53e6b09..2bf2b5fce8d 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -45,12 +45,11 @@
= form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do
.filtered-search-wrapper.d-flex
.filtered-search-box
- = dropdown_tag(custom_icon('icon_history'),
+ = dropdown_tag(_('Recent searches'),
options: { wrapper_class: 'filtered-search-history-dropdown-wrapper',
toggle_class: 'filtered-search-history-dropdown-toggle-button',
dropdown_class: 'filtered-search-history-dropdown',
- content_class: 'filtered-search-history-dropdown-content',
- title: _('Recent searches') }) do
+ content_class: 'filtered-search-history-dropdown-content' }) do
.js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index c50b20a83dc..6e7ec1264ea 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -1,41 +1,40 @@
.blank-state-row
- = link_to new_project_path, class: "blank-state-link" do
- .blank-state
+ - if has_start_trial?
+ = render_if_exists "dashboard/projects/blank_state_ee_trial"
+
+ = link_to new_project_path, class: "blank-state blank-state-link" do
+ .blank-state-icon
+ = image_tag("illustrations/welcome/add_new_project")
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ Projects are where you store your code, access issues, wiki and other features of GitLab.
+
+ - if current_user.can_create_group?
+ = link_to new_group_path, class: "blank-state blank-state-link" do
.blank-state-icon
- = custom_icon("add_new_project", size: 50)
+ = image_tag("illustrations/welcome/add_new_group")
.blank-state-body
%h3.blank-state-title
- Create a project
+ Create a group
%p.blank-state-text
- Projects are where you store your code, access issues, wiki and other features of GitLab.
+ Groups are a great way to organize projects and people.
- - if current_user.can_create_group?
- = link_to new_group_path, class: "blank-state-link" do
- .blank-state
- .blank-state-icon
- = custom_icon("add_new_group", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a group
- %p.blank-state-text
- Groups are a great way to organize projects and people.
-
- = link_to new_admin_user_path, class: "blank-state-link" do
- .blank-state
- .blank-state-icon
- = custom_icon("add_new_user", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Add people
- %p.blank-state-text
- Add your team members and others to GitLab.
-
- = link_to admin_root_path, class: "blank-state-link" do
- .blank-state
+ = link_to new_admin_user_path, class: "blank-state blank-state-link" do
.blank-state-icon
- = custom_icon("configure_server", size: 50)
+ = image_tag("illustrations/welcome/add_new_user")
.blank-state-body
%h3.blank-state-title
- Configure GitLab
+ Add people
%p.blank-state-text
- Make adjustments to how your GitLab instance is set up.
+ Add your team members and others to GitLab.
+
+ = link_to admin_root_path, class: "blank-state blank-state-link" do
+ .blank-state-icon
+ = image_tag("illustrations/welcome/configure_server")
+ .blank-state-body
+ %h3.blank-state-title
+ Configure GitLab
+ %p.blank-state-text
+ Make adjustments to how your GitLab instance is set up.
diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml
index 8d5bddbb288..e3af3405b76 100644
--- a/app/views/dashboard/projects/_blank_state_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml
@@ -2,19 +2,18 @@
.blank-state-row
- if current_user.can_create_project?
- = link_to new_project_path, class: "blank-state-link" do
- .blank-state
- .blank-state-icon
- = custom_icon("add_new_project", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a project
- %p.blank-state-text
- Projects are where you store your code, access issues, wiki and other features of GitLab.
+ = link_to new_project_path, class: "blank-state blank-state-link" do
+ .blank-state-icon
+ = image_tag("illustrations/welcome/add_new_project")
+ .blank-state-body
+ %h3.blank-state-title
+ Create a project
+ %p.blank-state-text
+ Projects are where you store your code, access issues, wiki and other features of GitLab.
- else
.blank-state
.blank-state-icon
- = custom_icon("add_new_project", size: 50)
+ = image_tag("illustrations/welcome/add_new_project")
.blank-state-body
%h3.blank-state-title
Create a project
@@ -22,37 +21,34 @@
If you are added to a project, it will be displayed here.
- if current_user.can_create_group?
- = link_to new_group_path, class: "blank-state-link" do
- .blank-state
- .blank-state-icon
- = custom_icon("add_new_group", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Create a group
- %p.blank-state-text
- Groups are the best way to manage projects and members.
+ = link_to new_group_path, class: "blank-state blank-state-link" do
+ .blank-state-icon
+ = image_tag("illustrations/welcome/add_new_group")
+ .blank-state-body
+ %h3.blank-state-title
+ Create a group
+ %p.blank-state-text
+ Groups are the best way to manage projects and members.
- if public_project_count > 0
- = link_to trending_explore_projects_path, class: "blank-state-link" do
- .blank-state
- .blank-state-icon
- = custom_icon("globe", size: 50)
- .blank-state-body
- %h3.blank-state-title
- Explore public projects
- %p.blank-state-text
- There are
- = number_with_delimiter(public_project_count)
- public projects on this server.
- Public projects are an easy way to allow
- everyone to have read-only access.
-
- = link_to "https://docs.gitlab.com/", class: "blank-state-link" do
- .blank-state
+ = link_to trending_explore_projects_path, class: "blank-state blank-state-link" do
.blank-state-icon
- = custom_icon("lightbulb", size: 50)
+ = image_tag("illustrations/welcome/globe")
.blank-state-body
%h3.blank-state-title
- Learn more about GitLab
+ Explore public projects
%p.blank-state-text
- Take a look at the documentation to discover all of GitLab's capabilities.
+ There are
+ = number_with_delimiter(public_project_count)
+ public projects on this server.
+ Public projects are an easy way to allow
+ everyone to have read-only access.
+
+ = link_to "https://docs.gitlab.com/", class: "blank-state blank-state-link" do
+ .blank-state-icon
+ = image_tag("illustrations/welcome/lightbulb")
+ .blank-state-body
+ %h3.blank-state-title
+ Learn more about GitLab
+ %p.blank-state-text
+ Take a look at the documentation to discover all of GitLab's capabilities.
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index eff68f817bb..a2b1f0d9298 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,4 @@
-.blank-state-parent-container{ class: ('has-start-trial-container' if has_start_trial?) }
+.blank-state-parent-container
.section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
.container.section-body
.row
@@ -7,12 +7,7 @@
= _('Welcome to GitLab')
%p.blank-state-text
= _('Faster releases. Better code. Less pain.')
- .blank-state-row
- %div{ class: ('column-large' if has_start_trial?) }
- - if current_user.admin?
- = render "blank_state_admin_welcome"
- - else
- = render "blank_state_welcome"
- - if has_start_trial?
- .column-small
- = render_if_exists "blank_state_ee_trial"
+ - if current_user.admin?
+ = render "blank_state_admin_welcome"
+ - else
+ = render "blank_state_welcome"
diff --git a/app/views/notify/new_release_email.html.haml b/app/views/notify/new_release_email.html.haml
new file mode 100644
index 00000000000..45e99f3c07a
--- /dev/null
+++ b/app/views/notify/new_release_email.html.haml
@@ -0,0 +1,18 @@
+- release_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
+- description_details = { tag: @release.tag, name: @project.name, release_link_start: release_link_start, release_link_end: '</a>'.html_safe }
+
+%div{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif;" }
+ %p
+ = _("A new Release %{tag} for %{name} was published. Visit the %{release_link_start}Releases page%{release_link_end} to read more about it.").html_safe % description_details
+
+ %p
+ %h4= _("Assets:")
+ %ul
+ - @release.links.each do |link|
+ %li= link_to(link.name, link.url)
+ - @release.sources.each do |source|
+ %li= link_to(_("Download %{format}") % { format: source.format }, source.url)
+
+ %p
+ %h4= _("Release notes:")
+ = markdown_field(@release, :description)
diff --git a/app/views/notify/new_release_email.text.erb b/app/views/notify/new_release_email.text.erb
new file mode 100644
index 00000000000..e03cf2d5fd1
--- /dev/null
+++ b/app/views/notify/new_release_email.text.erb
@@ -0,0 +1,12 @@
+<%= _("A new Release %{tag} for %{name} was published. Visit the Releases page to read more about it:").html_safe % { tag: @release.tag, name: @project.name } %> <%= @target_url %>
+
+<%= _("Assets:") %>
+<% @release.links.each do |link| -%>
+ - <%= link.name %>: <%= link.url %>
+<% end -%>
+<% @release.sources.each do |source| -%>
+ - <%= _("Download %{format}:") % { format: source.format } %> <%= source.url %>
+<% end -%>
+
+<%= _("Release notes:") %>
+<%= @release.description %>
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index a674136e791..66ed1cadf6a 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -51,10 +51,10 @@
%hr
.form-group
- = f.label :ci_config_path, _('Custom CI config path'), class: 'label-bold'
+ = f.label :ci_config_path, _('Custom CI configuration path'), class: 'label-bold'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.form-text.text-muted
- = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>").html_safe
+ = _("The path to the CI configuration file. Defaults to <code>.gitlab-ci.yml</code>").html_safe
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
%hr
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 9165147ef2a..9d580930fb8 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -17,12 +17,11 @@
.issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
.filtered-search-box
- if type != :boards_modal && type != :boards
- = dropdown_tag(custom_icon('icon_history'),
+ = dropdown_tag(_('Recent searches'),
options: { wrapper_class: "filtered-search-history-dropdown-wrapper",
toggle_class: "filtered-search-history-dropdown-toggle-button",
dropdown_class: "filtered-search-history-dropdown",
- content_class: "filtered-search-history-dropdown-content",
- title: "Recent searches" }) do
+ content_class: "filtered-search-history-dropdown-content" }) do
.js-filtered-search-history-dropdown{ data: { full_path: search_history_storage_prefix } }
.filtered-search-box-input-container.droplab-dropdown
.scroll-container
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index cd8d1d05d8b..b161cc65602 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -119,6 +119,8 @@
- container_repository:delete_container_repository
- container_repository:cleanup_container_repository
+- notifications:new_release
+
- default
- mailers # ActionMailer::DeliveryJob.queue_name
diff --git a/app/workers/new_release_worker.rb b/app/workers/new_release_worker.rb
new file mode 100644
index 00000000000..b80553a60db
--- /dev/null
+++ b/app/workers/new_release_worker.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class NewReleaseWorker
+ include ApplicationWorker
+
+ queue_namespace :notifications
+
+ def perform(release_id)
+ release = Release.with_project_and_namespace.find_by_id(release_id)
+ return unless release
+
+ NotificationService.new.send_new_release_notifications(release)
+ end
+end