diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-10 10:53:40 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-10 10:53:40 +0300 |
commit | cfc792b9ca064990e6540cb742e80529ea669a81 (patch) | |
tree | 147cd4256319990cebbc02fe8e4fbbbe06f5720a /app | |
parent | 93c6764dacd4c605027ef1cd367d3aebe420b223 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 |