diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-13 18:08:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-13 18:08:52 +0300 |
commit | 0ab47b994caa80c5587f33dc818626b66cfdafe2 (patch) | |
tree | 5ef3976d2f84e3368903a67ba2dbd87a74b9a43c /app | |
parent | 1308dc5eb484ab0f8064989fc551ebdb4b1a7976 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
34 files changed, 435 insertions, 262 deletions
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 26456fb28db..939c396e1b9 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -257,6 +257,7 @@ export default class ClusterStore { name: environment.name, project: environment.project, environmentPath: environment.environment_path, + logsPath: environment.logs_path, lastDeployment: environment.last_deployment, rolloutStatus: { status: environment.rollout_status ? environment.rollout_status.status : null, diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue deleted file mode 100644 index 9eaceb8893c..00000000000 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ /dev/null @@ -1,150 +0,0 @@ -<script> -import { mapState, mapGetters, mapActions } from 'vuex'; -import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; -import Icon from '~/vue_shared/components/icon.vue'; -import DiffGutterAvatars from './diff_gutter_avatars.vue'; -import { LINE_POSITION_RIGHT } from '../constants'; - -export default { - components: { - DiffGutterAvatars, - Icon, - }, - props: { - line: { - type: Object, - required: true, - }, - fileHash: { - type: String, - required: true, - }, - contextLinesPath: { - type: String, - required: true, - }, - lineNumber: { - type: Number, - required: false, - default: 0, - }, - linePosition: { - type: String, - required: false, - default: '', - }, - showCommentButton: { - type: Boolean, - required: false, - default: false, - }, - isBottom: { - type: Boolean, - required: false, - default: false, - }, - isMatchLine: { - type: Boolean, - required: false, - default: false, - }, - isMetaLine: { - type: Boolean, - required: false, - default: false, - }, - isContextLine: { - type: Boolean, - required: false, - default: false, - }, - isHover: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - ...mapState({ - diffViewType: state => state.diffs.diffViewType, - diffFiles: state => state.diffs.diffFiles, - }), - ...mapGetters(['isLoggedIn']), - lineCode() { - return ( - this.line.line_code || - (this.line.left && this.line.left.line_code) || - (this.line.right && this.line.right.line_code) - ); - }, - lineHref() { - return `#${this.line.line_code || ''}`; - }, - shouldShowCommentButton() { - return ( - this.isHover && - !this.isMatchLine && - !this.isContextLine && - !this.isMetaLine && - !this.hasDiscussions - ); - }, - hasDiscussions() { - return this.line.discussions && this.line.discussions.length > 0; - }, - shouldShowAvatarsOnGutter() { - if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) { - return false; - } - return this.showCommentButton && this.hasDiscussions; - }, - shouldRenderCommentButton() { - const isDiffHead = parseBoolean(getParameterByName('diff_head')); - return !isDiffHead && this.isLoggedIn && this.showCommentButton; - }, - }, - methods: { - ...mapActions('diffs', [ - 'loadMoreLines', - 'showCommentForm', - 'setHighlightedRow', - 'toggleLineDiscussions', - 'toggleLineDiscussionWrappers', - ]), - handleCommentButton() { - this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash }); - }, - }, -}; -</script> - -<template> - <div> - <button - v-if="shouldRenderCommentButton" - v-show="shouldShowCommentButton" - type="button" - class="add-diff-note js-add-diff-note-button qa-diff-comment" - title="Add a comment to this line" - @click="handleCommentButton" - > - <icon :size="12" name="comment" /> - </button> - <a - v-if="lineNumber" - ref="lineNumberRef" - :data-linenumber="lineNumber" - :href="lineHref" - @click="setHighlightedRow(lineCode)" - > - </a> - <diff-gutter-avatars - v-if="shouldShowAvatarsOnGutter" - :discussions="line.discussions" - :discussions-expanded="line.discussionsExpanded" - @toggleLineDiscussions=" - toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded }) - " - /> - </div> -</template> diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue index 0f3e9208d21..9544fbe9fc5 100644 --- a/app/assets/javascripts/diffs/components/diff_table_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue @@ -1,21 +1,24 @@ <script> import { mapGetters, mapActions } from 'vuex'; -import DiffLineGutterContent from './diff_line_gutter_content.vue'; +import { GlIcon } from '@gitlab/ui'; +import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils'; +import DiffGutterAvatars from './diff_gutter_avatars.vue'; import { MATCH_LINE_TYPE, CONTEXT_LINE_TYPE, + LINE_POSITION_RIGHT, EMPTY_CELL_TYPE, OLD_LINE_TYPE, OLD_NO_NEW_LINE_TYPE, NEW_NO_NEW_LINE_TYPE, LINE_HOVER_CLASS_NAME, LINE_UNFOLD_CLASS_NAME, - INLINE_DIFF_VIEW_TYPE, } from '../constants'; export default { components: { - DiffLineGutterContent, + DiffGutterAvatars, + GlIcon, }, props: { line: { @@ -33,12 +36,6 @@ export default { isHighlighted: { type: Boolean, required: true, - default: false, - }, - diffViewType: { - type: String, - required: false, - default: INLINE_DIFF_VIEW_TYPE, }, showCommentButton: { type: Boolean, @@ -73,6 +70,38 @@ export default { }, computed: { ...mapGetters(['isLoggedIn']), + lineCode() { + return ( + this.line.line_code || + (this.line.left && this.line.left.line_code) || + (this.line.right && this.line.right.line_code) + ); + }, + lineHref() { + return `#${this.line.line_code || ''}`; + }, + shouldShowCommentButton() { + return ( + this.isHover && + !this.isMatchLine && + !this.isContextLine && + !this.isMetaLine && + !this.hasDiscussions + ); + }, + hasDiscussions() { + return this.line.discussions && this.line.discussions.length > 0; + }, + shouldShowAvatarsOnGutter() { + if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) { + return false; + } + return this.showCommentButton && this.hasDiscussions; + }, + shouldRenderCommentButton() { + const isDiffHead = parseBoolean(getParameterByName('diff_head')); + return !isDiffHead && this.isLoggedIn && this.showCommentButton; + }, isMatchLine() { return this.line.type === MATCH_LINE_TYPE; }, @@ -107,24 +136,45 @@ export default { return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line; }, }, - methods: mapActions('diffs', ['setHighlightedRow']), + methods: { + ...mapActions('diffs', ['showCommentForm', 'setHighlightedRow', 'toggleLineDiscussions']), + handleCommentButton() { + this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash }); + }, + }, }; </script> <template> - <td :class="classNameMap"> - <diff-line-gutter-content - :line="line" - :file-hash="fileHash" - :context-lines-path="contextLinesPath" - :line-position="linePosition" - :line-number="lineNumber" - :show-comment-button="showCommentButton" - :is-hover="isHover" - :is-bottom="isBottom" - :is-match-line="isMatchLine" - :is-context-line="isContentLine" - :is-meta-line="isMetaLine" - /> + <td ref="td" :class="classNameMap"> + <div> + <button + v-if="shouldRenderCommentButton" + v-show="shouldShowCommentButton" + ref="addDiffNoteButton" + type="button" + class="add-diff-note js-add-diff-note-button qa-diff-comment" + title="Add a comment to this line" + @click="handleCommentButton" + > + <gl-icon :size="12" name="comment" /> + </button> + <a + v-if="lineNumber" + ref="lineNumberRef" + :data-linenumber="lineNumber" + :href="lineHref" + @click="setHighlightedRow(lineCode)" + > + </a> + <diff-gutter-avatars + v-if="shouldShowAvatarsOnGutter" + :discussions="line.discussions" + :discussions-expanded="line.discussionsExpanded" + @toggleLineDiscussions=" + toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded }) + " + /> + </div> </td> </template> diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 8abc927c500..3f316643784 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -162,8 +162,7 @@ export default { :is-loading="model.isLoadingDeployBoard" :is-empty="model.isEmptyDeployBoard" :has-legacy-app-label="model.hasLegacyAppLabel" - :project-path="model.project_path" - :environment-name="model.name" + :logs-path="model.logs_path" /> </div> </div> diff --git a/app/assets/javascripts/error_tracking/details.js b/app/assets/javascripts/error_tracking/details.js index 1a92681374b..55ab362f805 100644 --- a/app/assets/javascripts/error_tracking/details.js +++ b/app/assets/javascripts/error_tracking/details.js @@ -8,28 +8,30 @@ import csrf from '~/lib/utils/csrf'; Vue.use(VueApollo); export default () => { + const selector = '#js-error_details'; + + const domEl = document.querySelector(selector); + const { + issueId, + projectPath, + issueUpdatePath, + issueStackTracePath, + projectIssuesPath, + } = domEl.dataset; + const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(), }); // eslint-disable-next-line no-new new Vue({ - el: '#js-error_details', + el: selector, apolloProvider, components: { ErrorDetails, }, store, render(createElement) { - const domEl = document.querySelector(this.$options.el); - const { - issueId, - projectPath, - issueUpdatePath, - issueStackTracePath, - projectIssuesPath, - } = domEl.dataset; - return createElement('error-details', { props: { issueId, diff --git a/app/assets/javascripts/error_tracking/list.js b/app/assets/javascripts/error_tracking/list.js index 8f3700249da..cb656a9ef13 100644 --- a/app/assets/javascripts/error_tracking/list.js +++ b/app/assets/javascripts/error_tracking/list.js @@ -4,27 +4,29 @@ import store from './store'; import ErrorTrackingList from './components/error_tracking_list.vue'; export default () => { + const selector = '#js-error_tracking'; + + const domEl = document.querySelector(selector); + const { + indexPath, + enableErrorTrackingLink, + illustrationPath, + projectPath, + listPath, + } = domEl.dataset; + let { errorTrackingEnabled, userCanEnableErrorTracking } = domEl.dataset; + + errorTrackingEnabled = parseBoolean(errorTrackingEnabled); + userCanEnableErrorTracking = parseBoolean(userCanEnableErrorTracking); + // eslint-disable-next-line no-new new Vue({ - el: '#js-error_tracking', + el: selector, components: { ErrorTrackingList, }, store, render(createElement) { - const domEl = document.querySelector(this.$options.el); - const { - indexPath, - enableErrorTrackingLink, - illustrationPath, - projectPath, - listPath, - } = domEl.dataset; - let { errorTrackingEnabled, userCanEnableErrorTracking } = domEl.dataset; - - errorTrackingEnabled = parseBoolean(errorTrackingEnabled); - userCanEnableErrorTracking = parseBoolean(userCanEnableErrorTracking); - return createElement('error-tracking-list', { props: { indexPath, diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue index 8b2c5e44bb5..eaf0780d9e1 100644 --- a/app/assets/javascripts/monitoring/components/charts/time_series.vue +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -18,6 +18,17 @@ import { import { makeDataSeries } from '~/helpers/monitor_helper'; import { graphDataValidatorForValues } from '../../utils'; +/** + * A "virtual" coordinates system for the deployment icons. + * Deployment icons are displayed along the [min, max] + * range at height `pos`. + */ +const deploymentYAxisCoords = { + min: 0, + pos: 3, // 3% height of chart's grid + max: 100, +}; + const THROTTLED_DATAZOOM_WAIT = 1000; // miliseconds const timestampToISODate = timestamp => new Date(timestamp).toISOString(); @@ -145,10 +156,33 @@ export default { }, []); }, chartOptionSeries() { - return (this.option.series || []).concat(this.scatterSeries ? [this.scatterSeries] : []); + return (this.option.series || []).concat( + this.deploymentSeries ? [this.deploymentSeries] : [], + ); }, chartOptions() { const option = omit(this.option, 'series'); + + const dataYAxis = { + name: this.yAxisLabel, + nameGap: 50, // same as gitlab-ui's default + nameLocation: 'center', // same as gitlab-ui's default + boundaryGap: [0.1, 0.1], + scale: true, + axisLabel: { + formatter: num => roundOffFloat(num, 3).toString(), + }, + }; + const deploymentsYAxis = { + show: false, + min: deploymentYAxisCoords.min, + max: deploymentYAxisCoords.max, + axisLabel: { + // formatter fn required to trigger tooltip re-positioning + formatter: () => {}, + }, + }; + return { series: this.chartOptionSeries, xAxis: { @@ -161,12 +195,7 @@ export default { snap: true, }, }, - yAxis: { - name: this.yAxisLabel, - axisLabel: { - formatter: num => roundOffFloat(num, 3).toString(), - }, - }, + yAxis: [dataYAxis, deploymentsYAxis], dataZoom: [this.dataZoomConfig], ...option, }; @@ -228,10 +257,16 @@ export default { return acc; }, []); }, - scatterSeries() { + deploymentSeries() { return { type: graphTypes.deploymentData, - data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]), + + yAxisIndex: 1, // deploymentsYAxis index + data: this.recentDeployments.map(deployment => [ + deployment.createdAt, + deploymentYAxisCoords.pos, + ]), + symbol: this.svgs.rocket, symbolSize: symbolSizes.default, itemStyle: { @@ -265,6 +300,7 @@ export default { formatTooltipText(params) { this.tooltip.title = dateFormat(params.value, dateFormats.default); this.tooltip.content = []; + params.seriesData.forEach(dataPoint => { if (dataPoint.value) { const [xVal, yVal] = dataPoint.value; diff --git a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue index 84d1c5ccc6a..a15b854cb9b 100644 --- a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue +++ b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue @@ -82,7 +82,7 @@ export default { regexHelpText() { return sprintf( s__( - 'ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}', + 'ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}', ), { codeStart: '<code>', diff --git a/app/assets/javascripts/snippet/collapsible_input.js b/app/assets/javascripts/snippet/collapsible_input.js new file mode 100644 index 00000000000..e7225162f86 --- /dev/null +++ b/app/assets/javascripts/snippet/collapsible_input.js @@ -0,0 +1,45 @@ +const hide = el => el.classList.add('d-none'); +const show = el => el.classList.remove('d-none'); + +const setupCollapsibleInput = el => { + const collapsedEl = el.querySelector('.js-collapsed'); + const expandedEl = el.querySelector('.js-expanded'); + const collapsedInputEl = collapsedEl.querySelector('textarea,input,select'); + const expandedInputEl = expandedEl.querySelector('textarea,input,select'); + const formEl = el.closest('form'); + + const collapse = () => { + hide(expandedEl); + show(collapsedEl); + }; + + const expand = () => { + hide(collapsedEl); + show(expandedEl); + }; + + // NOTE: + // We add focus listener to all form inputs so that we can collapse + // when something is focused that's not the expanded input. + formEl.addEventListener('focusin', e => { + if (e.target === collapsedInputEl) { + expand(); + expandedInputEl.focus(); + } else if (!el.contains(e.target) && !expandedInputEl.value) { + collapse(); + } + }); +}; + +/** + * Usage in HAML + * + * .js-collapsible-input + * .js-collapsed{ class: ('d-none' if is_expanded) } + * = input + * .js-expanded{ class: ('d-none' if !is_expanded) } + * = big_input + */ +export default () => { + Array.from(document.querySelectorAll('.js-collapsible-input')).forEach(setupCollapsibleInput); +}; diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index dcee17453b8..652531a1289 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,6 +1,7 @@ /* global ace */ import $ from 'jquery'; +import setupCollapsibleInputs from './collapsible_input'; export default () => { const editor = ace.edit('editor'); @@ -8,4 +9,6 @@ export default () => { $('.snippet-form-holder form').on('submit', () => { $('.snippet-file-content').val(editor.getValue()); }); + + setupCollapsibleInputs(); }; diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 9fbfc59f630..8414095d454 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -75,7 +75,9 @@ class Admin::UsersController < Admin::ApplicationController end def block - if update_user { |user| user.block } + result = Users::BlockService.new(current_user).execute(user) + + if result[:status] = :success redirect_back_or_admin_user(notice: _("Successfully blocked")) else redirect_back_or_admin_user(alert: _("Error occurred. User was not blocked")) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index d3b0304f2c7..1ce76fd57b1 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -717,6 +717,6 @@ module ProjectsHelper def settings_container_registry_expiration_policy_available?(project) Feature.enabled?(:registry_retention_policies_settings, project) && Gitlab.config.registry.enabled && - can?(current_user, :read_container_image, project) + can?(current_user, :destroy_container_image, project) end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 46222bbc4cd..d8a3bbfeeb2 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -244,6 +244,8 @@ class Commit # Discover issues should be closed when this commit is pushed to a project's # default branch. def closes_issues(current_user = self.committer) + return unless repository.repo_type.project? + Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message) end @@ -297,7 +299,11 @@ class Commit end def merge_requests - @merge_requests ||= project&.merge_requests&.by_commit_sha(sha) + strong_memoize(:merge_requests) do + next MergeRequest.none unless repository.repo_type.project? && project + + project.merge_requests.by_commit_sha(sha) + end end def method_missing(method, *args, &block) @@ -507,7 +513,7 @@ class Commit end def commit_reference(from, referable_commit_id, full: false) - base = project&.to_reference_base(from, full: full) + base = container.to_reference_base(from, full: full) if base.present? "#{base}#{self.class.reference_prefix}#{referable_commit_id}" diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb index 66c2f57bedd..d04a6408a21 100644 --- a/app/models/concerns/has_repository.rb +++ b/app/models/concerns/has_repository.rb @@ -1,9 +1,17 @@ # frozen_string_literal: true +# This concern is created to handle repository actions. +# It should be include inside any object capable +# of directly having a repository, like project or snippet. +# +# It also includes `Referable`, therefore the method +# `to_reference` should be overriden in case the object +# needs any special behavior. module HasRepository extend ActiveSupport::Concern include Gitlab::ShellAdapter include AfterCommitQueue + include Referable include Gitlab::Utils::StrongMemoize delegate :base_dir, :disk_path, to: :storage diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index 1b5be8698b1..5940265b17a 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -2,4 +2,8 @@ class PersonalSnippet < Snippet include WithUploads + + def web_url(only_path: nil) + Gitlab::Routing.url_helpers.snippet_url(self, only_path: only_path) + end end diff --git a/app/models/project.rb b/app/models/project.rb index 0ed2510dbf4..bc652a19986 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -9,7 +9,6 @@ class Project < ApplicationRecord include AccessRequestable include Avatarable include CacheMarkdownField - include Referable include Sortable include AfterCommitQueue include CaseSensitivity @@ -2336,6 +2335,10 @@ class Project < ApplicationRecord false end + def self_monitoring? + Gitlab::CurrentSettings.self_monitoring_project_id == id + end + private def closest_namespace_setting(name) diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index ffb08e10f1f..6045ec71c6e 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -5,4 +5,8 @@ class ProjectSnippet < Snippet validates :project, presence: true validates :secret, inclusion: { in: [false] } + + def web_url(only_path: nil) + Gitlab::Routing.url_helpers.project_snippet_url(project, self, only_path: only_path) + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 37a20404ae7..c439d0700f1 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1131,7 +1131,11 @@ class Repository end def project - container + if repo_type.snippet? + container.project + else + container + end end private @@ -1145,7 +1149,7 @@ class Repository Gitlab::Git::Commit.find(raw_repository, oid_or_ref) end - ::Commit.new(commit, project) if commit + ::Commit.new(commit, container) if commit end def cache diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 77ec683f584..e2b72dfde7a 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -6,7 +6,6 @@ class Snippet < ApplicationRecord include CacheMarkdownField include Noteable include Participable - include Referable include Sortable include Awardable include Mentionable @@ -15,10 +14,11 @@ class Snippet < ApplicationRecord include Gitlab::SQL::Pattern include FromUnion include IgnorableColumns - + include HasRepository extend ::Gitlab::Utils::Override ignore_column :storage_version, remove_with: '12.9', remove_after: '2020-03-22' + ignore_column :repository_storage, remove_with: '12.10', remove_after: '2020-04-22' cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description @@ -42,6 +42,7 @@ class Snippet < ApplicationRecord has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :user_mentions, class_name: "SnippetUserMention" + has_one :snippet_repository, inverse_of: :snippet delegate :name, :email, to: :author, prefix: true, allow_nil: true @@ -254,6 +255,47 @@ class Snippet < ApplicationRecord super end + def repository + @repository ||= Repository.new(full_path, self, disk_path: disk_path, repo_type: Gitlab::GlRepository::SNIPPET) + end + + def storage + @storage ||= Storage::Hashed.new(self, prefix: Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX) + end + + # This is the full_path used to identify the + # the snippet repository. It will be used mostly + # for logging purposes. + def full_path + return unless persisted? + + @full_path ||= begin + components = [] + components << project.full_path if project_id? + components << '@snippets' + components << self.id + components.join('/') + end + end + + def repository_storage + snippet_repository&.shard_name || + Gitlab::CurrentSettings.pick_repository_storage + end + + def create_repository + return if repository_exists? + + repository.create_if_not_exists + + track_snippet_repository if repository_exists? + end + + def track_snippet_repository + repository = snippet_repository || build_snippet_repository + repository.update!(shard_name: repository_storage, disk_path: disk_path) + end + class << self # Searches for snippets with a matching title or file name. # diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb new file mode 100644 index 00000000000..ba2a061a5f4 --- /dev/null +++ b/app/models/snippet_repository.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class SnippetRepository < ApplicationRecord + include Shardable + + belongs_to :snippet, inverse_of: :snippet_repository + + class << self + def find_snippet(disk_path) + find_by(disk_path: disk_path)&.snippet + end + end +end diff --git a/app/models/storage/hashed.rb b/app/models/storage/hashed.rb index 898e75194db..3dea50ab98b 100644 --- a/app/models/storage/hashed.rb +++ b/app/models/storage/hashed.rb @@ -2,14 +2,15 @@ module Storage class Hashed - attr_accessor :project - delegate :gitlab_shell, :repository_storage, to: :project + attr_accessor :container + delegate :gitlab_shell, :repository_storage, to: :container REPOSITORY_PATH_PREFIX = '@hashed' + SNIPPET_REPOSITORY_PATH_PREFIX = '@snippets' POOL_PATH_PREFIX = '@pools' - def initialize(project, prefix: REPOSITORY_PATH_PREFIX) - @project = project + def initialize(container, prefix: REPOSITORY_PATH_PREFIX) + @container = container @prefix = prefix end @@ -20,9 +21,10 @@ module Storage "#{@prefix}/#{disk_hash[0..1]}/#{disk_hash[2..3]}" if disk_hash end - # Disk path is used to build repository and project's wiki path on disk + # Disk path is used to build repository path on disk # - # @return [String] combination of base_dir and the repository own name without `.git` or `.wiki.git` extensions + # @return [String] combination of base_dir and the repository own name + # without `.git`, `.wiki.git`, or any other extension def disk_path "#{base_dir}/#{disk_hash}" if disk_hash end @@ -33,10 +35,10 @@ module Storage private - # Generates the hash for the project path and name on disk + # Generates the hash for the repository path and name on disk # If you need to refer to the repository on disk, use the `#disk_path` def disk_hash - @disk_hash ||= Digest::SHA2.hexdigest(project.id.to_s) if project.id + @disk_hash ||= Digest::SHA2.hexdigest(container.id.to_s) if container.id end end end diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb index c6dfd62804f..729bca6580e 100644 --- a/app/services/boards/list_service.rb +++ b/app/services/boards/list_service.rb @@ -5,13 +5,7 @@ module Boards def execute(create_default_board: true) create_board! if create_default_board && parent.boards.empty? - if parent.multiple_issue_boards_available? - boards - else - # When multiple issue boards are not available - # a user is only allowed to view the default shown board - first_board - end + find_boards end private @@ -27,5 +21,18 @@ module Boards def create_board! Boards::CreateService.new(parent, current_user).execute end + + def find_boards + found = + if parent.multiple_issue_boards_available? + boards + else + # When multiple issue boards are not available + # a user is only allowed to view the default shown board + first_board + end + + params[:board_id].present? ? [found.find(params[:board_id])] : found + end end end diff --git a/app/services/container_expiration_policy_service.rb b/app/services/container_expiration_policy_service.rb index 5d141d4d64d..82274fd8668 100644 --- a/app/services/container_expiration_policy_service.rb +++ b/app/services/container_expiration_policy_service.rb @@ -6,9 +6,11 @@ class ContainerExpirationPolicyService < BaseService container_expiration_policy.container_repositories.find_each do |container_repository| CleanupContainerRepositoryWorker.perform_async( - current_user.id, + nil, container_repository.id, - container_expiration_policy.attributes.except("created_at", "updated_at") + container_expiration_policy.attributes + .except('created_at', 'updated_at') + .merge(container_expiration_policy: true) ) end end diff --git a/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb b/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb new file mode 100644 index 00000000000..d705c3f3ce5 --- /dev/null +++ b/app/services/metrics/dashboard/self_monitoring_dashboard_service.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Fetches the self monitoring metrics dashboard and formats the output. +# Use Gitlab::Metrics::Dashboard::Finder to retrieve dashboards. +module Metrics + module Dashboard + class SelfMonitoringDashboardService < ::Metrics::Dashboard::PredefinedDashboardService + DASHBOARD_PATH = 'config/prometheus/self_monitoring_default.yml' + DASHBOARD_NAME = 'Default' + + SEQUENCE = [ + STAGES::ProjectMetricsInserter, + STAGES::EndpointInserter, + STAGES::Sorter + ].freeze + + class << self + def valid_params?(params) + matching_dashboard?(params[:dashboard_path]) || self_monitoring_project?(params) + end + + def all_dashboard_paths(_project) + [{ + path: DASHBOARD_PATH, + display_name: DASHBOARD_NAME, + default: true, + system_dashboard: false + }] + end + + def self_monitoring_project?(params) + params[:dashboard_path].nil? && params[:environment]&.project&.self_monitoring? + end + end + end + end +end diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb index b995df12e56..046745d725e 100644 --- a/app/services/projects/container_repository/cleanup_tags_service.rb +++ b/app/services/projects/container_repository/cleanup_tags_service.rb @@ -5,7 +5,7 @@ module Projects class CleanupTagsService < BaseService def execute(container_repository) return error('feature disabled') unless can_use? - return error('access denied') unless can_admin? + return error('access denied') unless can_destroy? tags = container_repository.tags tags_by_digest = group_by_digest(tags) @@ -82,8 +82,10 @@ module Projects end end - def can_admin? - can?(current_user, :admin_container_image, project) + def can_destroy? + return true if params['container_expiration_policy'] + + can?(current_user, :destroy_container_image, project) end def can_use? diff --git a/app/services/projects/container_repository/delete_tags_service.rb b/app/services/projects/container_repository/delete_tags_service.rb index d19f275e928..21081bd077f 100644 --- a/app/services/projects/container_repository/delete_tags_service.rb +++ b/app/services/projects/container_repository/delete_tags_service.rb @@ -42,7 +42,7 @@ module Projects # Deletes the dummy image # All created tag digests are the same since they all have the same dummy image. # a single delete is sufficient to remove all tags with it - if deleted_tags.any? && container_repository.delete_tag_by_digest(deleted_tags.values.first) + if deleted_tags.any? && container_repository.delete_tag_by_digest(deleted_tags.each_value.first) success(deleted: deleted_tags.keys) else error('could not delete tags') diff --git a/app/services/users/block_service.rb b/app/services/users/block_service.rb new file mode 100644 index 00000000000..9c393832d8f --- /dev/null +++ b/app/services/users/block_service.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Users + class BlockService < BaseService + def initialize(current_user) + @current_user = current_user + end + + def execute(user) + if user.block + after_block_hook(user) + success + else + messages = user.errors.full_messages + error(messages.uniq.join('. ')) + end + end + + private + + def after_block_hook(user) + # overriden by EE module + end + end +end + +Users::BlockService.prepend_if_ee('EE::Users::BlockService') diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index 4359a2c3c2b..2db3e35250f 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -5,4 +5,5 @@ %i.fa.fa-rss .content_list -= spinner +.loading + .spinner.spinner-md diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index 6dbd67df886..9f5af1cfe1e 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -1,6 +1,7 @@ - add_to_breadcrumbs _("Snippets"), project_snippets_path(@project) - breadcrumb_title @snippet.to_reference - page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets") +- @content_class = "limit-container-width" unless fluid_layout %h3.page-title = _("Edit Snippet") diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index d64e3a49a81..d55a1160d48 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -1,6 +1,7 @@ - add_to_breadcrumbs _("Snippets"), project_snippets_path(@project) - breadcrumb_title _("New") - page_title _("New Snippet") +- @content_class = "limit-container-width" unless fluid_layout %h3.page-title = _("New Snippet") diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 73401029da4..f867fb2b6f7 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -6,27 +6,37 @@ html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" } do |f| = form_errors(@snippet) - .form-group.row - .col-sm-2.col-form-label - = f.label :title - .col-sm-10 - = f.text_field :title, class: 'form-control qa-snippet-title', required: true, autofocus: true - - = render 'shared/form_elements/description', model: @snippet, project: @project, form: f - - = render 'shared/old_visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet, with_label: false - - .file-editor - .form-group.row - .col-sm-2.col-form-label - = f.label :file_name, "File" - .col-sm-10 - .file-holder.snippet - .js-file-title.file-title-flex-parent - = f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name qa-snippet-file-name' - .file-content.code - %pre#editor= @snippet.content - = f.hidden_field :content, class: 'snippet-file-content' + .form-group + = f.label :title, class: 'label-bold' + = f.text_field :title, class: 'form-control qa-snippet-title', required: true, autofocus: true + + .form-group.js-description-input + - description_placeholder = s_('Snippets|Optionally add a description about what your snippet does or how to use it...') + - is_expanded = @snippet.description && !@snippet.description.empty? + = f.label :description, s_("Snippets|Description (optional)"), class: 'label-bold' + .js-collapsible-input + .js-collapsed{ class: ('d-none' if is_expanded) } + = text_field_tag nil, nil, class: 'form-control', placeholder: description_placeholder + .js-expanded{ class: ('d-none' if !is_expanded) } + = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: description_placeholder + = render 'shared/notes/hints' + + .form-group.file-editor + = f.label :file_name, s_('Snippets|File') + .file-holder.snippet + .js-file-title.file-title-flex-parent + = f.text_field :file_name, placeholder: s_("Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby"), class: 'form-control snippet-file-name qa-snippet-file-name' + .file-content.code + %pre#editor= @snippet.content + = f.hidden_field :content, class: 'snippet-file-content' + + .form-group + .font-weight-bold + = _('Visibility level') + = link_to icon('question-circle'), help_page_path("public_access/public_access"), target: '_blank' + = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet, with_label: false + - if params[:files] - params[:files].each_with_index do |file, index| = hidden_field_tag "files[]", file, id: "files_#{index}" diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index f5ffb037152..66f5e8148e1 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -1,4 +1,5 @@ - page_title _("Edit"), "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets") +- @content_class = "limit-container-width" unless fluid_layout %h3.page-title = _("Edit Snippet") diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index 9d462865471..acc0ce0fff3 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -1,6 +1,7 @@ - @hide_top_links = true - @hide_breadcrumbs = true - page_title _("New Snippet") +- @content_class = "limit-container-width" unless fluid_layout .page-title-holder.d-flex.align-items-center %h1.page-title= _('New Snippet') diff --git a/app/workers/cleanup_container_repository_worker.rb b/app/workers/cleanup_container_repository_worker.rb index 83fb3e58d29..83397a1dda2 100644 --- a/app/workers/cleanup_container_repository_worker.rb +++ b/app/workers/cleanup_container_repository_worker.rb @@ -11,6 +11,7 @@ class CleanupContainerRepositoryWorker def perform(current_user_id, container_repository_id, params) @current_user = User.find_by_id(current_user_id) @container_repository = ContainerRepository.find_by_id(container_repository_id) + @params = params return unless valid? @@ -22,9 +23,15 @@ class CleanupContainerRepositoryWorker private def valid? + return true if run_by_container_expiration_policy? + current_user && container_repository && project end + def run_by_container_expiration_policy? + @params['container_expiration_policy'] && container_repository && project + end + def project container_repository&.project end |