summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-10 07:53:40 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-10 07:53:40 +0000
commitcfc792b9ca064990e6540cb742e80529ea669a81 (patch)
tree147cd4256319990cebbc02fe8e4fbbbe06f5720a /app
parent93c6764dacd4c605027ef1cd367d3aebe420b223 (diff)
downloadgitlab-ce-cfc792b9ca064990e6540cb742e80529ea669a81.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue6
-rw-r--r--app/assets/javascripts/pages/projects/pages_domains/show/index.js (renamed from app/assets/javascripts/pages/projects/pages_domains/edit/index.js)0
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/framework/files.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss1
-rw-r--r--app/assets/stylesheets/utilities.scss2
-rw-r--r--app/controllers/projects/environments_controller.rb34
-rw-r--r--app/controllers/projects/pages_domains_controller.rb10
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb10
-rw-r--r--app/graphql/resolvers/concerns/resolves_snippets.rb57
-rw-r--r--app/graphql/resolvers/projects/snippets_resolver.rb23
-rw-r--r--app/graphql/resolvers/snippets_resolver.rb45
-rw-r--r--app/graphql/resolvers/users/snippets_resolver.rb21
-rw-r--r--app/graphql/types/notes/noteable_type.rb2
-rw-r--r--app/graphql/types/permission_types/project.rb8
-rw-r--r--app/graphql/types/permission_types/snippet.rb15
-rw-r--r--app/graphql/types/permission_types/user.rb15
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/graphql/types/query_type.rb6
-rw-r--r--app/graphql/types/snippet_type.rb69
-rw-r--r--app/graphql/types/snippets/type_enum.rb10
-rw-r--r--app/graphql/types/snippets/visibility_scopes_enum.rb11
-rw-r--r--app/graphql/types/user_type.rb8
-rw-r--r--app/mailers/emails/profile.rb12
-rw-r--r--app/models/clusters/applications/prometheus.rb17
-rw-r--r--app/models/clusters/cluster.rb8
-rw-r--r--app/models/concerns/ci/metadatable.rb8
-rw-r--r--app/models/concerns/expirable.rb4
-rw-r--r--app/models/environment.rb15
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/personal_access_token.rb1
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/project_services/prometheus_service.rb2
-rw-r--r--app/models/user.rb7
-rw-r--r--app/policies/personal_snippet_policy.rb3
-rw-r--r--app/policies/project_policy.rb3
-rw-r--r--app/policies/project_snippet_policy.rb3
-rw-r--r--app/presenters/snippet_presenter.rb35
-rw-r--r--app/serializers/environment_entity.rb9
-rw-r--r--app/services/deployments/after_create_service.rb7
-rw-r--r--app/services/environments/reset_auto_stop_service.rb22
-rw-r--r--app/services/notification_service.rb8
-rw-r--r--app/views/clusters/clusters/_banner.html.haml18
-rw-r--r--app/views/layouts/_flash.html.haml7
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml10
-rw-r--r--app/views/notify/access_token_about_to_expire_email.html.haml7
-rw-r--r--app/views/notify/access_token_about_to_expire_email.text.erb5
-rw-r--r--app/views/projects/environments/show.html.haml4
-rw-r--r--app/views/projects/pages/_list.html.haml4
-rw-r--r--app/views/projects/pages_domains/edit.html.haml21
-rw-r--r--app/views/projects/pages_domains/show.html.haml60
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/workers/all_queues.yml3
-rw-r--r--app/workers/clusters/applications/activate_service_worker.rb19
-rw-r--r--app/workers/clusters/applications/deactivate_service_worker.rb24
-rw-r--r--app/workers/personal_access_tokens/expiring_worker.rb23
56 files changed, 617 insertions, 127 deletions
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 36a5334bd31..91d374eafc0 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -2,7 +2,7 @@
import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
import { GlButton, GlTooltipDirective, GlTooltip, GlLoadingIcon } from '@gitlab/ui';
-import { polyfillSticky, stickyMonitor } from '~/lib/utils/sticky';
+import { polyfillSticky } from '~/lib/utils/sticky';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -11,7 +11,7 @@ import { __, s__, sprintf } from '~/locale';
import { diffViewerModes } from '~/ide/constants';
import EditButton from './edit_button.vue';
import DiffStats from './diff_stats.vue';
-import { scrollToElement, contentTop } from '~/lib/utils/common_utils';
+import { scrollToElement } from '~/lib/utils/common_utils';
export default {
components: {
@@ -127,8 +127,6 @@ export default {
},
mounted() {
polyfillSticky(this.$refs.header);
- const fileHeaderHeight = this.$refs.header.clientHeight;
- stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
},
methods: {
...mapActions('diffs', [
diff --git a/app/assets/javascripts/pages/projects/pages_domains/edit/index.js b/app/assets/javascripts/pages/projects/pages_domains/show/index.js
index 27e4433ad4d..27e4433ad4d 100644
--- a/app/assets/javascripts/pages/projects/pages_domains/edit/index.js
+++ b/app/assets/javascripts/pages/projects/pages_domains/show/index.js
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index fc99f3ab5af..21253e004ef 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -288,7 +288,7 @@
list-style: none;
padding: 0 1px;
- a:not(.help-link),
+ > a,
button,
.menu-item {
@include dropdown-link;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 4938215b2e7..e7b5629b999 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -334,10 +334,6 @@ span.idiff {
padding: $gl-padding-8 $gl-padding;
margin: 0;
border-radius: $border-radius-default $border-radius-default 0 0;
-
- &.is-stuck {
- border-radius: 0;
- }
}
.file-header-content {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index defa1a6c0d5..a1afcf5077e 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -10,6 +10,7 @@
.file-title-flex-parent {
border-top-left-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
+ box-shadow: 0 -2px 0 0 var(--white);
cursor: pointer;
@media (min-width: map-get($grid-breakpoints, md)) {
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 5377a8cdcba..26c563675a8 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -29,6 +29,8 @@
.border-color-default { border-color: $border-color; }
.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; }
+.mh-50vh { max-height: 50vh; }
+
.gl-w-64 { width: px-to-rem($grid-size * 8); }
.gl-h-64 { height: px-to-rem($grid-size * 8); }
.gl-bg-blue-500 { @include gl-bg-blue-500; }
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 4562296cea0..1179782036d 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -7,14 +7,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]
- before_action :authorize_update_environment!, only: [:edit, :update]
+ before_action :authorize_update_environment!, only: [:edit, :update, :cancel_auto_stop]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
- before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
+ before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics, :cancel_auto_stop]
before_action :verify_api_request!, only: :terminal_websocket_authorize
- before_action :expire_etag_cache, only: [:index]
+ before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:prometheus_computed_alerts)
end
+ after_action :expire_etag_cache, only: [:cancel_auto_stop]
def index
@environments = project.environments
@@ -104,6 +105,27 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ def cancel_auto_stop
+ result = Environments::ResetAutoStopService.new(project, current_user)
+ .execute(environment)
+
+ if result[:status] == :success
+ respond_to do |format|
+ message = _('Auto stop successfully canceled.')
+
+ format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
+ format.json { render json: { message: message }, status: :ok }
+ end
+ else
+ respond_to do |format|
+ message = result[:message]
+
+ format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: message }) }
+ format.json { render json: { message: message }, status: :unprocessable_entity }
+ end
+ end
+ end
+
def terminal
# Currently, this acts as a hint to load the terminal details into the cache
# if they aren't there already. In the future, users will need these details
@@ -175,8 +197,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def expire_etag_cache
- return if request.format.json?
-
# this forces to reload json content
Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(project_environments_path(project, format: :json))
@@ -222,6 +242,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def authorize_stop_environment!
access_denied! unless can?(current_user, :stop_environment, environment)
end
+
+ def authorize_update_environment!
+ access_denied! unless can?(current_user, :update_environment, environment)
+ end
end
Projects::EnvironmentsController.prepend_if_ee('EE::Projects::EnvironmentsController')
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
index b693642981e..5a81a064048 100644
--- a/app/controllers/projects/pages_domains_controller.rb
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -8,7 +8,6 @@ class Projects::PagesDomainsController < Projects::ApplicationController
before_action :domain, except: [:new, :create]
def show
- redirect_to edit_project_pages_domain_path(@project, @domain)
end
def new
@@ -24,17 +23,18 @@ class Projects::PagesDomainsController < Projects::ApplicationController
flash[:alert] = 'Failed to verify domain ownership'
end
- redirect_to edit_project_pages_domain_path(@project, @domain)
+ redirect_to project_pages_domain_path(@project, @domain)
end
def edit
+ redirect_to project_pages_domain_path(@project, @domain)
end
def create
@domain = @project.pages_domains.create(create_params)
if @domain.valid?
- redirect_to edit_project_pages_domain_path(@project, @domain)
+ redirect_to project_pages_domain_path(@project, @domain)
else
render 'new'
end
@@ -46,7 +46,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
status: :found,
notice: 'Domain was updated'
else
- render 'edit'
+ render 'show'
end
end
@@ -68,7 +68,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
flash[:alert] = @domain.errors.full_messages.join(', ')
end
- redirect_to edit_project_pages_domain_path(@project, @domain)
+ redirect_to project_pages_domain_path(@project, @domain)
end
private
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index cfed8727450..6af815b8daa 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -13,7 +13,7 @@ module Projects
Projects::UpdateService.new(project, current_user, update_params).tap do |service|
result = service.execute
if result[:status] == :success
- flash[:notice] = _("Pipelines settings for '%{project_name}' were successfully updated.") % { project_name: @project.name }
+ flash[:toast] = _("Pipelines settings for '%{project_name}' were successfully updated.") % { project_name: @project.name }
run_autodevops_pipeline(service)
@@ -39,7 +39,7 @@ module Projects
def reset_registration_token
@project.reset_runners_token!
- flash[:notice] = _('New runners registration token has been generated!')
+ flash[:toast] = _("New runners registration token has been generated!")
redirect_to namespace_project_settings_ci_cd_path
end
@@ -65,12 +65,14 @@ module Projects
return unless service.run_auto_devops_pipeline?
if @project.empty_repo?
- flash[:warning] = _("This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch.")
+ flash[:notice] = _("This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch.")
return
end
CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
- flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
+
+ pipelines_link_start = '<a href="%{url}">'.html_safe % { url: project_pipelines_path(@project) }
+ flash[:toast] = _("A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details") % { pipelines_link_start: pipelines_link_start, pipelines_link_end: "</a>".html_safe }
end
def define_variables
diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb
new file mode 100644
index 00000000000..483372bbf63
--- /dev/null
+++ b/app/graphql/resolvers/concerns/resolves_snippets.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module ResolvesSnippets
+ extend ActiveSupport::Concern
+
+ included do
+ type Types::SnippetType, null: false
+
+ argument :ids, [GraphQL::ID_TYPE],
+ required: false,
+ description: 'Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"'
+
+ argument :visibility, Types::Snippets::VisibilityScopesEnum,
+ required: false,
+ description: 'The visibility of the snippet'
+ end
+
+ def resolve(**args)
+ resolve_snippets(args)
+ end
+
+ private
+
+ def resolve_snippets(args)
+ SnippetsFinder.new(context[:current_user], snippet_finder_params(args)).execute
+ end
+
+ def snippet_finder_params(args)
+ {
+ ids: resolve_ids(args[:ids]),
+ scope: args[:visibility]
+ }.merge(options_by_type(args[:type]))
+ end
+
+ def resolve_ids(ids)
+ Array.wrap(ids).map { |id| resolve_gid(id, :id) }
+ end
+
+ def resolve_gid(gid, argument)
+ return unless gid.present?
+
+ GlobalID.parse(gid)&.model_id.tap do |id|
+ raise Gitlab::Graphql::Errors::ArgumentError, "Invalid global id format for param #{argument}" if id.nil?
+ end
+ end
+
+ def options_by_type(type)
+ case type
+ when 'personal'
+ { only_personal: true }
+ when 'project'
+ { only_project: true }
+ else
+ {}
+ end
+ end
+end
diff --git a/app/graphql/resolvers/projects/snippets_resolver.rb b/app/graphql/resolvers/projects/snippets_resolver.rb
new file mode 100644
index 00000000000..bf9aa45349f
--- /dev/null
+++ b/app/graphql/resolvers/projects/snippets_resolver.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Projects
+ class SnippetsResolver < BaseResolver
+ include ResolvesSnippets
+
+ alias_method :project, :object
+
+ def resolve(**args)
+ return Snippet.none if project.nil?
+
+ super
+ end
+
+ private
+
+ def snippet_finder_params(args)
+ super.merge(project: project)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb
new file mode 100644
index 00000000000..530a288a25b
--- /dev/null
+++ b/app/graphql/resolvers/snippets_resolver.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class SnippetsResolver < BaseResolver
+ include ResolvesSnippets
+
+ ERROR_MESSAGE = 'Filtering by both an author and a project is not supported'
+
+ alias_method :user, :object
+
+ argument :author_id, GraphQL::ID_TYPE,
+ required: false,
+ description: 'The ID of an author'
+
+ argument :project_id, GraphQL::ID_TYPE,
+ required: false,
+ description: 'The ID of a project'
+
+ argument :type, Types::Snippets::TypeEnum,
+ required: false,
+ description: 'The type of snippet'
+
+ argument :explore,
+ GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'Explore personal snippets'
+
+ def resolve(**args)
+ if args[:author_id].present? && args[:project_id].present?
+ raise Gitlab::Graphql::Errors::ArgumentError, ERROR_MESSAGE
+ end
+
+ super
+ end
+
+ private
+
+ def snippet_finder_params(args)
+ super
+ .merge(author: resolve_gid(args[:author_id], :author),
+ project: resolve_gid(args[:project_id], :project),
+ explore: args[:explore])
+ end
+ end
+end
diff --git a/app/graphql/resolvers/users/snippets_resolver.rb b/app/graphql/resolvers/users/snippets_resolver.rb
new file mode 100644
index 00000000000..d757640b5ff
--- /dev/null
+++ b/app/graphql/resolvers/users/snippets_resolver.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Users
+ class SnippetsResolver < BaseResolver
+ include ResolvesSnippets
+
+ alias_method :user, :object
+
+ argument :type, Types::Snippets::TypeEnum,
+ required: false,
+ description: 'The type of snippet'
+
+ private
+
+ def snippet_finder_params(args)
+ super.merge(author: user)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb
index ab4a170b123..2ac66452841 100644
--- a/app/graphql/types/notes/noteable_type.rb
+++ b/app/graphql/types/notes/noteable_type.rb
@@ -15,6 +15,8 @@ module Types
Types::IssueType
when MergeRequest
Types::MergeRequestType
+ when Snippet
+ Types::SnippetType
else
raise "Unknown GraphQL type for #{object}"
end
diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb
index 3a6ba371154..2879dbd2b5c 100644
--- a/app/graphql/types/permission_types/project.rb
+++ b/app/graphql/types/permission_types/project.rb
@@ -10,13 +10,19 @@ module Types
:remove_pages, :read_project, :create_merge_request_in,
:read_wiki, :read_project_member, :create_issue, :upload_file,
:read_cycle_analytics, :download_code, :download_wiki_code,
- :fork_project, :create_project_snippet, :read_commit_status,
+ :fork_project, :read_commit_status,
:request_access, :create_pipeline, :create_pipeline_schedule,
:create_merge_request_from, :create_wiki, :push_code,
:create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages,
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
:create_pages, :destroy_pages, :read_pages_content, :admin_operations
+
+ permission_field :create_snippet
+
+ def create_snippet
+ Ability.allowed?(context[:current_user], :create_project_snippet, object)
+ end
end
end
end
diff --git a/app/graphql/types/permission_types/snippet.rb b/app/graphql/types/permission_types/snippet.rb
new file mode 100644
index 00000000000..1e21efe790a
--- /dev/null
+++ b/app/graphql/types/permission_types/snippet.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class Snippet < BasePermissionType
+ graphql_name 'SnippetPermissions'
+
+ abilities :create_note, :award_emoji
+
+ permission_field :read_snippet, method: :can_read_snippet?
+ permission_field :update_snippet, method: :can_update_snippet?
+ permission_field :admin_snippet, method: :can_admin_snippet?
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/user.rb b/app/graphql/types/permission_types/user.rb
new file mode 100644
index 00000000000..dba4de2dacc
--- /dev/null
+++ b/app/graphql/types/permission_types/user.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class User < BasePermissionType
+ graphql_name 'UserPermissions'
+
+ permission_field :create_snippet
+
+ def create_snippet
+ Ability.allowed?(context[:current_user], :create_personal_snippet)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index d2a163b70db..a11676770a9 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -151,5 +151,11 @@ module Types
null: true,
description: 'Detailed version of a Sentry error on the project',
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver
+
+ field :snippets,
+ Types::SnippetType.connection_type,
+ null: true,
+ description: 'Snippets of the project',
+ resolver: Resolvers::Projects::SnippetsResolver
end
end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 996bf225976..06188d99490 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -29,6 +29,12 @@ module Types
resolver: Resolvers::MetadataResolver,
description: 'Metadata about GitLab'
+ field :snippets,
+ Types::SnippetType.connection_type,
+ null: true,
+ resolver: Resolvers::SnippetsResolver,
+ description: 'Find Snippets visible to the current user'
+
field :echo, GraphQL::STRING_TYPE, null: false, resolver: Resolvers::EchoResolver # rubocop:disable Graphql/Descriptions
end
end
diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb
new file mode 100644
index 00000000000..3b4dce1d486
--- /dev/null
+++ b/app/graphql/types/snippet_type.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Types
+ class SnippetType < BaseObject
+ graphql_name 'Snippet'
+ description 'Represents a snippet entry'
+
+ implements(Types::Notes::NoteableType)
+
+ present_using SnippetPresenter
+
+ authorize :read_snippet
+
+ expose_permissions Types::PermissionTypes::Snippet
+
+ field :id, GraphQL::ID_TYPE,
+ description: 'Id of the snippet',
+ null: false
+
+ field :title, GraphQL::STRING_TYPE,
+ description: 'Title of the snippet',
+ null: false
+
+ field :project, Types::ProjectType,
+ description: 'The project the snippet is associated with',
+ null: true,
+ authorize: :read_project,
+ resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, snippet.project_id).find }
+
+ field :author, Types::UserType,
+ description: 'The owner of the snippet',
+ null: false,
+ resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, snippet.author_id).find }
+
+ field :file_name, GraphQL::STRING_TYPE,
+ description: 'File Name of the snippet',
+ null: true
+
+ field :content, GraphQL::STRING_TYPE,
+ description: 'Content of the snippet',
+ null: false
+
+ field :description, GraphQL::STRING_TYPE,
+ description: 'Description of the snippet',
+ null: true
+
+ field :visibility, GraphQL::STRING_TYPE,
+ description: 'Visibility of the snippet',
+ null: false
+
+ field :created_at, Types::TimeType,
+ description: 'Timestamp this snippet was created',
+ null: false
+
+ field :updated_at, Types::TimeType,
+ description: 'Timestamp this snippet was updated',
+ null: false
+
+ field :web_url, type: GraphQL::STRING_TYPE,
+ description: 'Web URL of the snippet',
+ null: false
+
+ field :raw_url, type: GraphQL::STRING_TYPE,
+ description: 'Raw URL of the snippet',
+ null: false
+
+ markdown_field :description_html, null: true, method: :description
+ end
+end
diff --git a/app/graphql/types/snippets/type_enum.rb b/app/graphql/types/snippets/type_enum.rb
new file mode 100644
index 00000000000..243f05359db
--- /dev/null
+++ b/app/graphql/types/snippets/type_enum.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Types
+ module Snippets
+ class TypeEnum < BaseEnum
+ value 'personal', value: 'personal'
+ value 'project', value: 'project'
+ end
+ end
+end
diff --git a/app/graphql/types/snippets/visibility_scopes_enum.rb b/app/graphql/types/snippets/visibility_scopes_enum.rb
new file mode 100644
index 00000000000..5488e05b95d
--- /dev/null
+++ b/app/graphql/types/snippets/visibility_scopes_enum.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module Snippets
+ class VisibilityScopesEnum < BaseEnum
+ value 'private', value: 'are_private'
+ value 'internal', value: 'are_internal'
+ value 'public', value: 'are_public'
+ end
+ end
+end
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index b45c7893e75..3943c891335 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -8,6 +8,8 @@ module Types
present_using UserPresenter
+ expose_permissions Types::PermissionTypes::User
+
field :name, GraphQL::STRING_TYPE, null: false,
description: 'Human-readable name of the user'
field :username, GraphQL::STRING_TYPE, null: false,
@@ -19,5 +21,11 @@ module Types
field :todos, Types::TodoType.connection_type, null: false,
resolver: Resolvers::TodoResolver,
description: 'Todos of the user'
+
+ field :snippets,
+ Types::SnippetType.connection_type,
+ null: true,
+ description: 'Snippets authored by the user',
+ resolver: Resolvers::Users::SnippetsResolver
end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index c368e6c8ac7..441439444d5 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -32,6 +32,18 @@ module Emails
mail(to: @user.notification_email, subject: subject("GPG key was added to your account"))
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def access_token_about_to_expire_email(user)
+ return unless user
+
+ @user = user
+ @target_url = profile_personal_access_tokens_url
+ @days_to_expire = PersonalAccessToken::DAYS_TO_EXPIRE
+
+ Gitlab::I18n.with_locale(@user.preferred_language) do
+ mail(to: @user.notification_email, subject: subject(_("Your Personal Access Tokens will expire in %{days_to_expire} days or less") % { days_to_expire: @days_to_expire }))
+ end
+ end
end
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index 91ffdac3273..2b9285e33d0 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -13,15 +13,21 @@ module Clusters
include ::Clusters::Concerns::ApplicationStatus
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
+ include AfterCommitQueue
default_value_for :version, VERSION
- after_destroy :disable_prometheus_integration
+ after_destroy do
+ run_after_commit do
+ disable_prometheus_integration
+ end
+ end
state_machine :status do
after_transition any => [:installed] do |application|
- application.cluster.projects.each do |project|
- project.find_or_initialize_service('prometheus').update!(active: true)
+ application.run_after_commit do
+ Clusters::Applications::ActivateServiceWorker
+ .perform_async(application.cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
end
end
end
@@ -98,9 +104,8 @@ module Clusters
private
def disable_prometheus_integration
- cluster.projects.each do |project|
- project.prometheus_service&.update!(active: false)
- end
+ ::Clusters::Applications::DeactivateServiceWorker
+ .perform_async(cluster_id, ::PrometheusService.to_param) # rubocop:disable CodeReuse/ServiceClass
end
def kube_client
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 315cdcffb42..0cb18ba90c0 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -34,6 +34,7 @@ module Clusters
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :groups, through: :cluster_groups, class_name: '::Group'
+ has_many :groups_projects, through: :groups, source: :projects, class_name: '::Project'
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
@@ -177,6 +178,13 @@ module Clusters
end
end
+ def all_projects
+ return projects if project_type?
+ return groups_projects if group_type?
+
+ ::Project.all
+ end
+
def status_name
return cleanup_status_name if cleanup_errored?
return :cleanup_ongoing unless cleanup_not_started?
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 17d431bacf2..9bfe76728e4 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -17,6 +17,7 @@ module Ci
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :interruptible, to: :metadata, prefix: false, allow_nil: true
delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true
+ delegate :environment_auto_stop_in, to: :metadata, prefix: false, allow_nil: true
before_create :ensure_metadata
end
@@ -47,8 +48,11 @@ module Ci
def options=(value)
write_metadata_attribute(:options, :config_options, value)
- # Store presence of exposed artifacts in build metadata to make it easier to query
- ensure_metadata.has_exposed_artifacts = value&.dig(:artifacts, :expose_as).present?
+ ensure_metadata.tap do |metadata|
+ # Store presence of exposed artifacts in build metadata to make it easier to query
+ metadata.has_exposed_artifacts = value&.dig(:artifacts, :expose_as).present?
+ metadata.environment_auto_stop_in = value&.dig(:environment, :auto_stop_in)
+ end
end
def yaml_variables=(value)
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
index 1f274487935..512822089ba 100644
--- a/app/models/concerns/expirable.rb
+++ b/app/models/concerns/expirable.rb
@@ -3,6 +3,8 @@
module Expirable
extend ActiveSupport::Concern
+ DAYS_TO_EXPIRE = 7
+
included do
scope :expired, -> { where('expires_at <= ?', Time.current) }
end
@@ -16,6 +18,6 @@ module Expirable
end
def expires_soon?
- expires? && expires_at < 7.days.from_now
+ expires? && expires_at < DAYS_TO_EXPIRE.days.from_now
end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index fec1f034d63..b928dcb21a6 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -162,6 +162,10 @@ class Environment < ApplicationRecord
stop_action&.play(current_user)
end
+ def reset_auto_stop
+ update_column(:auto_stop_at, nil)
+ end
+
def actions_for(environment)
return [] unless manual_actions
@@ -261,6 +265,17 @@ class Environment < ApplicationRecord
end
end
+ def auto_stop_in
+ auto_stop_at - Time.now if auto_stop_at
+ end
+
+ def auto_stop_in=(value)
+ return unless value
+ return unless parsed_result = ChronicDuration.parse(value)
+
+ self.auto_stop_at = parsed_result.seconds.from_now
+ end
+
private
def generate_slug
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d29eb62af7a..987373aaf1b 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -341,6 +341,6 @@ class Milestone < ApplicationRecord
end
def issues_finder_params
- { project_id: project_id, group_id: group_id }.compact
+ { project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact
end
end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 770fc2b5c8f..9ccc90fb74d 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -16,6 +16,7 @@ class PersonalAccessToken < ApplicationRecord
before_save :ensure_token
scope :active, -> { where("revoked = false AND (expires_at >= NOW() OR expires_at IS NULL)") }
+ scope :expiring_and_not_notified, ->(date) { where(["revoked = false AND expire_notification_delivered = false AND expires_at >= NOW() AND expires_at <= ?", date]) }
scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
diff --git a/app/models/project.rb b/app/models/project.rb
index 6ee300dc7f5..88b66423e59 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -404,6 +404,7 @@ class Project < ApplicationRecord
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
+ scope :with_service, ->(service) { joins(service).eager_load(service) }
scope :with_shared_runners, -> { where(shared_runners_enabled: true) }
scope :with_container_registry, -> { where(container_registry_enabled: true) }
scope :inside_path, ->(path) do
@@ -1256,8 +1257,9 @@ class Project < ApplicationRecord
def all_clusters
group_clusters = Clusters::Cluster.joins(:groups).where(cluster_groups: { group_id: ancestors_upto } )
+ instance_clusters = Clusters::Cluster.instance_type
- Clusters::Cluster.from_union([clusters, group_clusters])
+ Clusters::Cluster.from_union([clusters, group_clusters, instance_clusters])
end
def items_for(entity)
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 2ca74b081aa..3d5967de41e 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -88,7 +88,7 @@ class PrometheusService < MonitoringService
return false if template?
return false unless project
- project.clusters.enabled.any? { |cluster| cluster.application_prometheus_available? }
+ project.all_clusters.enabled.any? { |cluster| cluster.application_prometheus_available? }
end
def allow_local_api_url?
diff --git a/app/models/user.rb b/app/models/user.rb
index d0e758b0055..fd02db86582 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -310,6 +310,13 @@ class User < ApplicationRecord
scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) }
scope :with_public_profile, -> { where(private_profile: false) }
+ scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do
+ where('EXISTS (?)',
+ ::PersonalAccessToken
+ .where('personal_access_tokens.user_id = users.id')
+ .expiring_and_not_notified(at).select(1))
+ end
+
def self.with_visible_profile(user)
return with_public_profile if user.nil?
diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb
index 5c62bdd0d95..c2fcf1a1010 100644
--- a/app/policies/personal_snippet_policy.rb
+++ b/app/policies/personal_snippet_policy.rb
@@ -27,4 +27,7 @@ class PersonalSnippetPolicy < BasePolicy
rule { can?(:create_note) }.enable :award_emoji
rule { can?(:read_all_resources) }.enable :read_personal_snippet
+
+ # Aliasing the ability to ease GraphQL permissions check
+ rule { can?(:read_personal_snippet) }.enable :read_snippet
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index ff70c6e6aeb..7b0297ea81b 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -262,6 +262,7 @@ class ProjectPolicy < BasePolicy
enable :update_container_image
enable :destroy_container_image
enable :create_environment
+ enable :update_environment
enable :create_deployment
enable :update_deployment
enable :create_release
@@ -278,8 +279,6 @@ class ProjectPolicy < BasePolicy
enable :admin_board
enable :push_to_delete_protected_branch
enable :update_project_snippet
- enable :update_environment
- enable :update_deployment
enable :admin_project_snippet
enable :admin_project_member
enable :admin_note
diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb
index d9d09eb04cd..076492c6823 100644
--- a/app/policies/project_snippet_policy.rb
+++ b/app/policies/project_snippet_policy.rb
@@ -45,6 +45,9 @@ class ProjectSnippetPolicy < BasePolicy
end
rule { ~can?(:read_project_snippet) }.prevent :create_note
+
+ # Aliasing the ability to ease GraphQL permissions check
+ rule { can?(:read_project_snippet) }.enable :read_snippet
end
ProjectSnippetPolicy.prepend_if_ee('EE::ProjectSnippetPolicy')
diff --git a/app/presenters/snippet_presenter.rb b/app/presenters/snippet_presenter.rb
new file mode 100644
index 00000000000..ca8ae8d60c4
--- /dev/null
+++ b/app/presenters/snippet_presenter.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class SnippetPresenter < Gitlab::View::Presenter::Delegated
+ presents :snippet
+
+ def web_url
+ Gitlab::UrlBuilder.build(snippet)
+ end
+
+ def raw_url
+ Gitlab::UrlBuilder.build(snippet, raw: true)
+ end
+
+ def can_read_snippet?
+ can_access_resource?("read")
+ end
+
+ def can_update_snippet?
+ can_access_resource?("update")
+ end
+
+ def can_admin_snippet?
+ can_access_resource?("admin")
+ end
+
+ private
+
+ def can_access_resource?(ability_prefix)
+ can?(current_user, ability_name(ability_prefix), snippet)
+ end
+
+ def ability_name(ability_prefix)
+ "#{ability_prefix}_#{snippet.class.underscore}".to_sym
+ end
+end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index bffd9de4978..74d6806e83f 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -24,6 +24,10 @@ class EnvironmentEntity < Grape::Entity
stop_project_environment_path(environment.project, environment)
end
+ expose :cancel_auto_stop_path, if: -> (*) { can_update_environment? } do |environment|
+ cancel_auto_stop_project_environment_path(environment.project, environment)
+ end
+
expose :cluster_type, if: ->(environment, _) { cluster_platform_kubernetes? } do |environment|
cluster.cluster_type
end
@@ -37,6 +41,7 @@ class EnvironmentEntity < Grape::Entity
end
expose :created_at, :updated_at
+ expose :auto_stop_at, expose_nil: false
expose :can_stop do |environment|
environment.available? && can?(current_user, :stop_environment, environment)
@@ -54,6 +59,10 @@ class EnvironmentEntity < Grape::Entity
can?(request.current_user, :create_environment_terminal, environment)
end
+ def can_update_environment?
+ can?(current_user, :update_environment, environment)
+ end
+
def cluster_platform_kubernetes?
deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
end
diff --git a/app/services/deployments/after_create_service.rb b/app/services/deployments/after_create_service.rb
index e0a4e5419cc..1d9cb666cff 100644
--- a/app/services/deployments/after_create_service.rb
+++ b/app/services/deployments/after_create_service.rb
@@ -29,6 +29,7 @@ module Deployments
environment.external_url = url
end
+ renew_auto_stop_in
environment.fire_state_event(action)
if environment.save && !environment.stopped?
@@ -63,6 +64,12 @@ module Deployments
def action
environment_options[:action] || 'start'
end
+
+ def renew_auto_stop_in
+ return unless deployable
+
+ environment.auto_stop_in = deployable.environment_auto_stop_in
+ end
end
end
diff --git a/app/services/environments/reset_auto_stop_service.rb b/app/services/environments/reset_auto_stop_service.rb
new file mode 100644
index 00000000000..237629fda79
--- /dev/null
+++ b/app/services/environments/reset_auto_stop_service.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Environments
+ class ResetAutoStopService < ::BaseService
+ def execute(environment)
+ return error(_('Failed to cancel auto stop because you do not have permission to update the environment.')) unless can_update_environment?(environment)
+ return error(_('Failed to cancel auto stop because the environment is not set as auto stop.')) unless environment.auto_stop_at?
+
+ if environment.reset_auto_stop
+ success
+ else
+ error(_('Failed to cancel auto stop because failed to update the environment.'))
+ end
+ end
+
+ private
+
+ def can_update_environment?(environment)
+ can?(current_user, :update_environment, environment)
+ end
+ end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 1709474a6c7..a75eaa99c23 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -58,6 +58,14 @@ class NotificationService
end
end
+ # Notify the owner of the personal access token, when it is about to expire
+ # And mark the token with about_to_expire_delivered
+ def access_token_about_to_expire(user)
+ return unless user.can?(:receive_notifications)
+
+ mailer.access_token_about_to_expire_email(user).deliver_later
+ end
+
# When create an issue we should send an email to:
#
# * issue assignee if their notification level is not Disabled
diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml
index 7d97aaccbcf..82057fd0463 100644
--- a/app/views/clusters/clusters/_banner.html.haml
+++ b/app/views/clusters/clusters/_banner.html.haml
@@ -6,17 +6,19 @@
%span.spinner.spinner-dark.spinner-sm{ 'aria-label': 'Loading' }
%span.prepend-left-4= s_('ClusterIntegration|Kubernetes cluster is being created...')
-.hidden.row.js-cluster-api-unreachable.bs-callout.bs-callout-warning{ role: 'alert' }
- .col-11
+.hidden.row.js-cluster-api-unreachable.gl-alert.gl-alert-warning{ role: 'alert' }
+ = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ %button.js-close-banner.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
+ = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ .gl-alert-body
= s_('ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct.')
- .col-1.p-0
- %button.js-close-banner.close.cluster-application-banner-close.h-100.m-0= "×"
-.hidden.js-cluster-authentication-failure.row.js-cluster-api-unreachable.bs-callout.bs-callout-warning{ role: 'alert' }
- .col-11
+.hidden.js-cluster-authentication-failure.js-cluster-api-unreachable.gl-alert.gl-alert-warning{ role: 'alert' }
+ = sprite_icon('warning', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ %button.js-close-banner.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
+ = sprite_icon('close', size: 16, css_class: 'gl-icon')
+ .gl-alert-body
= s_('ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid.')
- .col-1.p-0
- %button.js-close-banner.close.cluster-application-banner-close.h-100.m-0= "×"
.hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' }
= s_("ClusterIntegration|Kubernetes cluster was successfully created.")
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index a0b030fa3b2..de1caeaa50f 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,8 +1,9 @@
+-# We currently only support `alert`, `notice`, `success`, 'toast'
.flash-container.flash-container-page.sticky
- -# We currently only support `alert`, `notice`, `success`
- flash.each do |key, value|
- -# Don't show a flash message if the message is nil
- - if value
+ - if key == 'toast' && value
+ .js-toast-message{ data: { message: value } }
+ - elsif value
%div{ class: "flash-#{key} mb-2" }
%span= value
%div{ class: "close-icon-wrapper js-close-icon" }
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 9b3ad05d0c0..1e2556aecc1 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -144,8 +144,16 @@
%strong.fly-out-top-item-name
= issue_tracker.title
+ - if (project_nav_tab? :labels) && !@project.issues_enabled?
+ = nav_link(controller: [:labels]) do
+ = link_to project_labels_path(@project), title: _('Labels'), class: 'shortcuts-labels qa-labels-items' do
+ .nav-icon-container
+ = sprite_icon('label')
+ %span.nav-item-name#js-onboarding-labels-link
+ = _('Labels')
+
- if project_nav_tab? :merge_requests
- = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
+ = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :milestones]) do
= link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests', data: { qa_selector: 'merge_requests_link' } do
.nav-icon-container
= sprite_icon('git-merge')
diff --git a/app/views/notify/access_token_about_to_expire_email.html.haml b/app/views/notify/access_token_about_to_expire_email.html.haml
new file mode 100644
index 00000000000..d1923e324f7
--- /dev/null
+++ b/app/views/notify/access_token_about_to_expire_email.html.haml
@@ -0,0 +1,7 @@
+%p
+ = _('Hi %{username}!') % { username: sanitize_name(@user.name) }
+%p
+ = _('One or more of your personal access tokens will expire in %{days_to_expire} days or less.') % { days_to_expire: @days_to_expire }
+%p
+ - pat_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
+ = _('You can create a new one or check them in your %{pat_link_start}Personal Access Tokens%{pat_link_end} settings').html_safe % { pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe }
diff --git a/app/views/notify/access_token_about_to_expire_email.text.erb b/app/views/notify/access_token_about_to_expire_email.text.erb
new file mode 100644
index 00000000000..5e6bd68d33f
--- /dev/null
+++ b/app/views/notify/access_token_about_to_expire_email.text.erb
@@ -0,0 +1,5 @@
+<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
+
+<%= _('One or more of your personal access tokens will expire in %{days_to_expire} days or less.') % { days_to_expire: @days_to_expire} %>
+
+<%= _('You can create a new one or check them in your Personal Access Tokens settings %{pat_link}') % { pat_link: @target_url } %>
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index c4c39c227c6..62b1c140794 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -5,7 +5,7 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag 'page_bundles/xterm'
-- if can?(current_user, :stop_environment, @environment)
+- if @environment.available? && can?(current_user, :stop_environment, @environment)
#stop-environment-modal.modal.fade{ tabindex: -1 }
.modal-dialog
.modal-content
@@ -40,7 +40,7 @@
= render 'projects/environments/metrics_button', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to _('Edit'), edit_project_environment_path(@project, @environment), class: 'btn'
- - if can?(current_user, :stop_environment, @environment)
+ - if @environment.available? && can?(current_user, :stop_environment, @environment)
= button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal',
target: '#stop-environment-modal' } do
= sprite_icon('stop')
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 4676c7399f1..6d196b06135 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -21,11 +21,11 @@
%span.badge.badge-danger
= s_('GitLabPages|Expired')
%div
- = link_to s_('GitLabPages|Edit'), edit_project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped btn-success btn-inverted"
+ = link_to s_('GitLabPages|Edit'), project_pages_domain_path(@project, domain), class: "btn btn-sm btn-grouped btn-success btn-inverted"
= link_to s_('GitLabPages|Remove'), project_pages_domain_path(@project, domain), data: { confirm: s_('GitLabPages|Are you sure?')}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
- if verification_enabled && domain.unverified?
%li.list-group-item.bs-callout-warning
- - details_link_start = "<a href='#{edit_project_pages_domain_path(@project, domain)}'>".html_safe
+ - details_link_start = "<a href='#{project_pages_domain_path(@project, domain)}'>".html_safe
- details_link_end = '</a>'.html_safe
= s_('GitLabPages|%{domain} is not verified. To learn how to verify ownership, visit your %{link_start}domain details%{link_end}.').html_safe % { domain: domain.domain,
link_start: details_link_start,
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
deleted file mode 100644
index a08be65d7e4..00000000000
--- a/app/views/projects/pages_domains/edit.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-- add_to_breadcrumbs _("Pages"), project_pages_path(@project)
-- breadcrumb_title @domain.domain
-- page_title @domain.domain
-
-- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
-
-- if verification_enabled && @domain.unverified?
- = content_for :flash_message do
- .alert.alert-warning
- .container-fluid.container-limited
- = _("This domain is not verified. You will need to verify ownership before access is enabled.")
-
-%h3.page-title
- = _('Pages Domain')
-= render 'projects/pages_domains/helper_text'
-%div
- = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
- = render 'form', { f: f }
- .form-actions.d-flex.justify-content-between
- = f.submit _('Save Changes'), class: "btn btn-success"
- = link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-default btn-inverse'
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index 8eec3d51835..a08be65d7e4 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -1,7 +1,6 @@
- add_to_breadcrumbs _("Pages"), project_pages_path(@project)
- breadcrumb_title @domain.domain
-- page_title "#{@domain.domain}", _('Pages Domains')
-- dns_record = "#{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}."
+- page_title @domain.domain
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
@@ -11,51 +10,12 @@
.container-fluid.container-limited
= _("This domain is not verified. You will need to verify ownership before access is enabled.")
-%h3.page-title.with-button
- = link_to _('Edit'), edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success float-right'
- = _("Pages Domain")
-
-.table-holder
- %table.table
- %tr
- %td
- = _("Domain")
- %td
- = external_link(@domain.url, @domain.url)
- %tr
- %td
- = _("DNS")
- %td
- .input-group
- = text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-append
- = clipboard_button(target: '#domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
- %p.form-text.text-muted
- = _("To access this domain create a new DNS record")
-
- - if verification_enabled
- - verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}"
- %tr
- %td
- = _("Verification status")
- %td
- = form_tag verify_project_pages_domain_path(@project, @domain) do
- .status-badge
- - text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success']
- .badge{ class: status }
- = text
- %button.btn.has-tooltip{ type: "submit", data: { container: 'body' }, title: _("Retry verification") }
- = sprite_icon('redo')
- .input-group
- = text_field_tag :domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-append
- = clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block')
- %p.form-text.text-muted
- - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership'))
- = _("To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration.").html_safe % { link_to_help: link_to_help }
-
- %tr
- %td
- = _("Certificate")
- %td
- = render 'lets_encrypt_callout', auto_ssl_available_and_enabled: false
+%h3.page-title
+ = _('Pages Domain')
+= render 'projects/pages_domains/helper_text'
+%div
+ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f|
+ = render 'form', { f: f }
+ .form-actions.d-flex.justify-content-between
+ = f.submit _('Save Changes'), class: "btn btn-success"
+ = link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-default btn-inverse'
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 04993f3bc82..2a853de12a4 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -33,7 +33,7 @@
.block.milestone{ data: { qa_selector: 'milestone_block' } }
.sidebar-collapsed-icon.has-tooltip{ title: sidebar_milestone_tooltip_label(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
= icon('clock-o', 'aria-hidden': 'true')
- %span.milestone-title.collapse-truncated-title{ data: { qa_selector: 'milestone_title' } }
+ %span.milestone-title.collapse-truncated-title
- if milestone.present?
= milestone[:title]
- else
@@ -45,7 +45,7 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
- if milestone.present?
- = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link' }
+ = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport', qa_selector: 'milestone_link', qa_title: milestone[:title] }
- else
%span.no-value
= _('None')
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 710998dcd1a..02acf360afc 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -16,6 +16,7 @@
- cronjob:pages_domain_verification_cron
- cronjob:pages_domain_removal_cron
- cronjob:pages_domain_ssl_renewal_cron
+- cronjob:personal_access_tokens_expiring
- cronjob:pipeline_schedule
- cronjob:prune_old_events
- cronjob:remove_expired_group_links
@@ -51,6 +52,8 @@
- gcp_cluster:clusters_cleanup_app
- gcp_cluster:clusters_cleanup_project_namespace
- gcp_cluster:clusters_cleanup_service_account
+- gcp_cluster:clusters_applications_activate_service
+- gcp_cluster:clusters_applications_deactivate_service
- github_import_advance_stage
- github_importer:github_import_import_diff_note
diff --git a/app/workers/clusters/applications/activate_service_worker.rb b/app/workers/clusters/applications/activate_service_worker.rb
new file mode 100644
index 00000000000..4f285d55162
--- /dev/null
+++ b/app/workers/clusters/applications/activate_service_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class ActivateServiceWorker
+ include ApplicationWorker
+ include ClusterQueue
+
+ def perform(cluster_id, service_name)
+ cluster = Clusters::Cluster.find_by_id(cluster_id)
+ return unless cluster
+
+ cluster.all_projects.find_each do |project|
+ project.find_or_initialize_service(service_name).update!(active: true)
+ end
+ end
+ end
+ end
+end
diff --git a/app/workers/clusters/applications/deactivate_service_worker.rb b/app/workers/clusters/applications/deactivate_service_worker.rb
new file mode 100644
index 00000000000..2c560cc998c
--- /dev/null
+++ b/app/workers/clusters/applications/deactivate_service_worker.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class DeactivateServiceWorker
+ include ApplicationWorker
+ include ClusterQueue
+
+ def perform(cluster_id, service_name)
+ cluster = Clusters::Cluster.find_by_id(cluster_id)
+ raise cluster_missing_error(service_name) unless cluster
+
+ service = "#{service_name}_service".to_sym
+ cluster.all_projects.with_service(service).find_each do |project|
+ project.public_send(service).update!(active: false) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def cluster_missing_error(service)
+ ActiveRecord::RecordNotFound.new("Can't deactivate #{service} services, host cluster not found! Some inconsistent records may be left in database.")
+ end
+ end
+ end
+end
diff --git a/app/workers/personal_access_tokens/expiring_worker.rb b/app/workers/personal_access_tokens/expiring_worker.rb
new file mode 100644
index 00000000000..f28109c4583
--- /dev/null
+++ b/app/workers/personal_access_tokens/expiring_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module PersonalAccessTokens
+ class ExpiringWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ feature_category :authentication_and_authorization
+
+ def perform(*args)
+ notification_service = NotificationService.new
+ limit_date = PersonalAccessToken::DAYS_TO_EXPIRE.days.from_now.to_date
+
+ User.with_expiring_and_not_notified_personal_access_tokens(limit_date).find_each do |user|
+ notification_service.access_token_about_to_expire(user)
+
+ Rails.logger.info "#{self.class}: Notifying User #{user.id} about expiring tokens" # rubocop:disable Gitlab/RailsLogger
+
+ user.personal_access_tokens.expiring_and_not_notified(limit_date).update_all(expire_notification_delivered: true)
+ end
+ end
+ end
+end