diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-18 15:08:08 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-18 15:08:08 +0300 |
commit | 48650fe1bfc1e3d20ec3a5702ef4d64e9fe69912 (patch) | |
tree | 0f73ad6e03989c301b79490ddb30125c233e4eff /app | |
parent | 1b9a2ce27825c02cc14b594ed5ea061fccf1d957 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
18 files changed, 327 insertions, 22 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue index ace48f25716..d042336c361 100644 --- a/app/assets/javascripts/alert_management/components/alert_details.vue +++ b/app/assets/javascripts/alert_management/components/alert_details.vue @@ -151,12 +151,14 @@ export default { <strong>{{ $options.severityLabels[alert.severity] }}</strong> </div> <span class="mx-2">•</span> - <gl-sprintf :message="reportedAtMessage"> - <template #when> - <time-ago-tooltip :time="alert.createdAt" class="gl-ml-3" /> - </template> - <template #tool>{{ alert.monitoringTool }}</template> - </gl-sprintf> + <span> + <gl-sprintf :message="reportedAtMessage"> + <template #when> + <time-ago-tooltip :time="alert.createdAt" /> + </template> + <template #tool>{{ alert.monitoringTool }}</template> + </gl-sprintf> + </span> </div> <gl-button v-if="glFeatures.createIssueFromAlertEnabled" diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue index 7d5d48cfc31..4f433bd8dfd 100644 --- a/app/assets/javascripts/blob/components/blob_content.vue +++ b/app/assets/javascripts/blob/components/blob_content.vue @@ -3,12 +3,19 @@ import { GlLoadingIcon } from '@gitlab/ui'; import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers'; import BlobContentError from './blob_content_error.vue'; +import { BLOB_RENDER_EVENT_LOAD, BLOB_RENDER_EVENT_SHOW_SOURCE } from './constants'; + export default { components: { GlLoadingIcon, BlobContentError, }, props: { + blob: { + type: Object, + required: false, + default: () => ({}), + }, content: { type: String, default: '', @@ -37,6 +44,8 @@ export default { return this.activeViewer.renderError; }, }, + BLOB_RENDER_EVENT_LOAD, + BLOB_RENDER_EVENT_SHOW_SOURCE, }; </script> <template> @@ -44,7 +53,13 @@ export default { <gl-loading-icon v-if="loading" size="md" color="dark" class="my-4 mx-auto" /> <template v-else> - <blob-content-error v-if="viewerError" :viewer-error="viewerError" /> + <blob-content-error + v-if="viewerError" + :viewer-error="viewerError" + :blob="blob" + @[$options.BLOB_RENDER_EVENT_LOAD]="$emit($options.BLOB_RENDER_EVENT_LOAD)" + @[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="$emit($options.BLOB_RENDER_EVENT_SHOW_SOURCE)" + /> <component :is="viewer" v-else diff --git a/app/assets/javascripts/blob/components/blob_content_error.vue b/app/assets/javascripts/blob/components/blob_content_error.vue index 0f1af0a962d..44dc4a6c727 100644 --- a/app/assets/javascripts/blob/components/blob_content_error.vue +++ b/app/assets/javascripts/blob/components/blob_content_error.vue @@ -1,15 +1,84 @@ <script> +import { __ } from '~/locale'; +import { GlSprintf, GlLink } from '@gitlab/ui'; +import { BLOB_RENDER_ERRORS } from './constants'; + export default { + components: { + GlSprintf, + GlLink, + }, props: { viewerError: { type: String, required: true, }, + blob: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + notStoredExternally() { + return this.viewerError !== BLOB_RENDER_ERRORS.REASONS.EXTERNAL.id; + }, + renderErrorReason() { + const defaultReasonPath = Object.keys(BLOB_RENDER_ERRORS.REASONS).find( + reason => BLOB_RENDER_ERRORS.REASONS[reason].id === this.viewerError, + ); + const defaultReason = BLOB_RENDER_ERRORS.REASONS[defaultReasonPath].text; + return this.notStoredExternally + ? defaultReason + : defaultReason[this.blob.externalStorage || 'default']; + }, + renderErrorOptions() { + const load = { + ...BLOB_RENDER_ERRORS.OPTIONS.LOAD, + condition: this.shouldShowLoadBtn, + }; + const showSource = { + ...BLOB_RENDER_ERRORS.OPTIONS.SHOW_SOURCE, + condition: this.shouldShowSourceBtn, + }; + const download = { + ...BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD, + href: this.blob.rawPath, + }; + return [load, showSource, download]; + }, + shouldShowLoadBtn() { + return this.viewerError === BLOB_RENDER_ERRORS.REASONS.COLLAPSED.id; + }, + shouldShowSourceBtn() { + return this.blob.richViewer && this.blob.renderedAsText && this.notStoredExternally; + }, }, + errorMessage: __( + 'This content could not be displayed because %{reason}. You can %{options} instead.', + ), }; </script> <template> <div class="file-content code"> - <div class="text-center py-4" v-html="viewerError"></div> + <div class="text-center py-4"> + <gl-sprintf :message="$options.errorMessage"> + <template #reason>{{ renderErrorReason }}</template> + <template #options> + <template v-for="option in renderErrorOptions"> + <span v-if="option.condition" :key="option.text"> + <gl-link + :href="option.href" + :target="option.target" + :data-test-id="`option-${option.id}`" + @click="option.event && $emit(option.event)" + >{{ option.text }}</gl-link + > + {{ option.conjunction }} + </span> + </template> + </template> + </gl-sprintf> + </div> </div> </template> diff --git a/app/assets/javascripts/blob/components/constants.js b/app/assets/javascripts/blob/components/constants.js index d3fed9e51e9..93dceacabdd 100644 --- a/app/assets/javascripts/blob/components/constants.js +++ b/app/assets/javascripts/blob/components/constants.js @@ -1,4 +1,5 @@ -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; export const BTN_COPY_CONTENTS_TITLE = __('Copy file contents'); export const BTN_RAW_TITLE = __('Open raw'); @@ -9,3 +10,56 @@ export const SIMPLE_BLOB_VIEWER_TITLE = __('Display source'); export const RICH_BLOB_VIEWER = 'rich'; export const RICH_BLOB_VIEWER_TITLE = __('Display rendered file'); + +export const BLOB_RENDER_EVENT_LOAD = 'force-content-fetch'; +export const BLOB_RENDER_EVENT_SHOW_SOURCE = 'force-switch-viewer'; + +export const BLOB_RENDER_ERRORS = { + REASONS: { + COLLAPSED: { + id: 'collapsed', + text: sprintf(__('it is larger than %{limit}'), { + limit: numberToHumanSize(1048576), // 1MB in bytes + }), + }, + TOO_LARGE: { + id: 'too_large', + text: sprintf(__('it is larger than %{limit}'), { + limit: numberToHumanSize(104857600), // 100MB in bytes + }), + }, + EXTERNAL: { + id: 'server_side_but_stored_externally', + text: { + lfs: __('it is stored in LFS'), + build_artifact: __('it is stored as a job artifact'), + default: __('it is stored externally'), + }, + }, + }, + OPTIONS: { + LOAD: { + id: 'load', + text: __('load it anyway'), + conjunction: __('or'), + href: '#', + target: '', + event: BLOB_RENDER_EVENT_LOAD, + }, + SHOW_SOURCE: { + id: 'show_source', + text: __('view the source'), + conjunction: __('or'), + href: '#', + target: '', + event: BLOB_RENDER_EVENT_SHOW_SOURCE, + }, + DOWNLOAD: { + id: 'download', + text: __('download it'), + conjunction: '', + target: '_blank', + condition: true, + }, + }, +}; diff --git a/app/assets/javascripts/helpers/avatar_helper.js b/app/assets/javascripts/helpers/avatar_helper.js index 7891b44dd27..4f04a1b8c16 100644 --- a/app/assets/javascripts/helpers/avatar_helper.js +++ b/app/assets/javascripts/helpers/avatar_helper.js @@ -1,11 +1,14 @@ import { escape } from 'lodash'; import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; export const DEFAULT_SIZE_CLASS = 's40'; export const IDENTICON_BG_COUNT = 7; export function getIdenticonBackgroundClass(entityId) { - const type = (entityId % IDENTICON_BG_COUNT) + 1; + // If a GraphQL string id is passed in, convert it to the entity number + const id = typeof entityId === 'string' ? getIdFromGraphQLId(entityId) : entityId; + const type = (id % IDENTICON_BG_COUNT) + 1; return `bg${type}`; } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 05869b483c8..713f57a2b27 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -111,6 +111,9 @@ function deferredInitialisation() { const recoverySettingsCallout = document.querySelector('.js-recovery-settings-callout'); PersistentUserCallout.factory(recoverySettingsCallout); + const usersOverLicenseCallout = document.querySelector('.js-users-over-license-callout'); + PersistentUserCallout.factory(usersOverLicenseCallout); + if (document.querySelector('.search')) initSearchAutocomplete(); addSelectOnFocusBehaviour('.js-select-on-focus'); diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js index 4598626718c..b3068c46bcb 100644 --- a/app/assets/javascripts/persistent_user_callout.js +++ b/app/assets/javascripts/persistent_user_callout.js @@ -18,6 +18,11 @@ export default class PersistentUserCallout { init() { const closeButton = this.container.querySelector('.js-close'); + + if (!closeButton) { + return; + } + closeButton.addEventListener('click', event => this.dismiss(event)); if (this.deferLinks) { diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue index d615eaadb78..6af1c161c5e 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue @@ -7,7 +7,12 @@ import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; import GetBlobContent from '../queries/snippet.blob.content.query.graphql'; -import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants'; +import { + SIMPLE_BLOB_VIEWER, + RICH_BLOB_VIEWER, + BLOB_RENDER_EVENT_LOAD, + BLOB_RENDER_EVENT_SHOW_SOURCE, +} from '~/blob/components/constants'; export default { components: { @@ -27,6 +32,16 @@ export default { }, update: data => data.snippets.edges[0].node.blob.richData || data.snippets.edges[0].node.blob.plainData, + result() { + if (this.activeViewerType === RICH_BLOB_VIEWER) { + this.blob.richViewer.renderError = null; + } else { + this.blob.simpleViewer.renderError = null; + } + }, + skip() { + return this.viewer.renderError; + }, }, }, props: { @@ -62,9 +77,15 @@ export default { }, methods: { switchViewer(newViewer) { - this.activeViewerType = newViewer; + this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER; + }, + forceQuery() { + this.$apollo.queries.blobContent.skip = false; + this.$apollo.queries.blobContent.refetch(); }, }, + BLOB_RENDER_EVENT_LOAD, + BLOB_RENDER_EVENT_SHOW_SOURCE, }; </script> <template> @@ -81,7 +102,14 @@ export default { /> </template> </blob-header> - <blob-content :loading="isContentLoading" :content="blobContent" :active-viewer="viewer" /> + <blob-content + :loading="isContentLoading" + :content="blobContent" + :active-viewer="viewer" + :blob="blob" + @[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery" + @[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer" + /> </article> </div> </template> diff --git a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql index d793d0b6bb4..e7765dfd8ba 100644 --- a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql +++ b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql @@ -17,6 +17,8 @@ fragment SnippetBase on Snippet { path rawPath size + externalStorage + renderedAsText simpleViewer { ...BlobViewer } diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 9dd61c8eada..87a995464fa 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -4,7 +4,7 @@ import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar export default { props: { entityId: { - type: Number, + type: [Number, String], required: true, }, entityName: { diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb index ed0995e7ffd..5723ccc14a7 100644 --- a/app/controllers/google_api/authorizations_controller.rb +++ b/app/controllers/google_api/authorizations_controller.rb @@ -15,6 +15,9 @@ module GoogleApi session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = expires_at.to_s + rescue ::Faraday::TimeoutError, ::Faraday::ConnectionFailed + flash[:alert] = _('Timeout connecting to the Google API. Please try again.') + ensure redirect_to redirect_uri_from_session end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a815b378f8b..2df33073a89 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -54,6 +54,10 @@ module ApplicationHelper args.any? { |v| v.to_s.downcase == action_name } end + def admin_section? + controller.class.ancestors.include?(Admin::ApplicationController) + end + def last_commit(project) if project.repo_exists? time_ago_with_tooltip(project.repository.commit.committed_date) diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 1ed97ada412..83f558af1a1 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -223,11 +223,19 @@ module Clusters end def applications - APPLICATIONS_ASSOCIATIONS.map do |association_name| - public_send(association_name) || public_send("build_#{association_name}") # rubocop:disable GitlabSecurity/PublicSend + APPLICATIONS.each_value.map do |application_class| + find_or_build_application(application_class) end end + def find_or_build_application(application_class) + raise ArgumentError, "#{application_class} is not in APPLICATIONS" unless APPLICATIONS.value?(application_class) + + association_name = application_class.association_name + + public_send(association_name) || public_send("build_#{association_name}") # rubocop:disable GitlabSecurity/PublicSend + end + def provider if gcp? provider_gcp diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 14237439a8d..0b915126f8a 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -27,6 +27,7 @@ module Clusters state :update_errored, value: 6 state :uninstalling, value: 7 state :uninstall_errored, value: 8 + state :uninstalled, value: 10 # Used for applications that are pre-installed by the cluster, # e.g. Knative in GCP Cloud Run enabled clusters @@ -35,6 +36,14 @@ module Clusters # and no exit transitions. state :pre_installed, value: 9 + event :make_externally_installed do + transition any => :installed + end + + event :make_externally_uninstalled do + transition any => :uninstalled + end + event :make_scheduled do transition [:installable, :errored, :installed, :updated, :update_errored, :uninstall_errored] => :scheduled end diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb index c2b7632971a..f0ffe67510b 100644 --- a/app/services/ci/create_job_artifacts_service.rb +++ b/app/services/ci/create_job_artifacts_service.rb @@ -61,6 +61,7 @@ module Ci case artifact.file_type when 'dotenv' then parse_dotenv_artifact(job, artifact) + when 'cluster_applications' then parse_cluster_applications_artifact(job, artifact) else success end end @@ -111,5 +112,9 @@ module Ci def parse_dotenv_artifact(job, artifact) Ci::ParseDotenvArtifactService.new(job.project, current_user).execute(artifact) end + + def parse_cluster_applications_artifact(job, artifact) + Clusters::ParseClusterApplicationsArtifactService.new(job, job.user).execute(artifact) + end end end diff --git a/app/services/clusters/parse_cluster_applications_artifact_service.rb b/app/services/clusters/parse_cluster_applications_artifact_service.rb new file mode 100644 index 00000000000..b8e1c80cfe7 --- /dev/null +++ b/app/services/clusters/parse_cluster_applications_artifact_service.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Clusters + class ParseClusterApplicationsArtifactService < ::BaseService + include Gitlab::Utils::StrongMemoize + + MAX_ACCEPTABLE_ARTIFACT_SIZE = 5.kilobytes + RELEASE_NAMES = %w[prometheus].freeze + + def initialize(job, current_user) + @job = job + + super(job.project, current_user) + end + + def execute(artifact) + return success unless Feature.enabled?(:cluster_applications_artifact, project) + + raise ArgumentError, 'Artifact is not cluster_applications file type' unless artifact&.cluster_applications? + + unless artifact.file.size < MAX_ACCEPTABLE_ARTIFACT_SIZE + return error(too_big_error_message, :bad_request) + end + + unless cluster + return error(s_('ClusterIntegration|No deployment cluster found for this job')) + end + + parse!(artifact) + + success + rescue Gitlab::Kubernetes::Helm::Parsers::ListV2::ParserError, ActiveRecord::RecordInvalid => error + Gitlab::ErrorTracking.track_exception(error, job_id: artifact.job_id) + error(error.message, :bad_request) + end + + private + + attr_reader :job + + def cluster + strong_memoize(:cluster) do + deployment_cluster = job.deployment&.cluster + + deployment_cluster if Ability.allowed?(current_user, :admin_cluster, deployment_cluster) + end + end + + def parse!(artifact) + releases = [] + + artifact.each_blob do |blob| + releases.concat(Gitlab::Kubernetes::Helm::Parsers::ListV2.new(blob).releases) + end + + update_cluster_application_statuses!(releases) + end + + def update_cluster_application_statuses!(releases) + release_by_name = releases.index_by { |release| release['Name'] } + + Clusters::Cluster.transaction do + RELEASE_NAMES.each do |release_name| + application = find_or_build_application(release_name) + + release = release_by_name[release_name] + + if release + case release['Status'] + when 'DEPLOYED' + application.make_externally_installed! + when 'FAILED' + application.make_errored!(s_('ClusterIntegration|Helm release failed to install')) + end + else + # missing, so by definition, we consider this uninstalled + application.make_externally_uninstalled! if application.persisted? + end + end + end + end + + def find_or_build_application(application_name) + application_class = Clusters::Cluster::APPLICATIONS[application_name] + + cluster.find_or_build_application(application_class) + end + + def too_big_error_message + human_size = ActiveSupport::NumberHelper.number_to_human_size(MAX_ACCEPTABLE_ARTIFACT_SIZE) + + s_('ClusterIntegration|Cluster_applications artifact too big. Maximum allowable size: %{human_size}') % { human_size: human_size } + end + end +end diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 49345b7b215..3885fa311ba 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -5,6 +5,7 @@ .mobile-overlay .alert-wrapper = render 'shared/outdated_browser' + = render_if_exists 'layouts/header/users_over_license_banner' - if Feature.enabled?(:subscribable_banner_license, default_enabled: true) = render_if_exists "layouts/header/ee_subscribable_banner" = render "layouts/broadcast" diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml index c6629cd33a5..25c841d2344 100644 --- a/app/views/shared/_delete_label_modal.html.haml +++ b/app/views/shared/_delete_label_modal.html.haml @@ -2,20 +2,19 @@ .modal-dialog .modal-content .modal-header - %h3.page-title Delete label: #{label.name} ? + %h3.page-title= _('Delete label: %{label_name} ?') % { label_name: label.name } %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } %span{ "aria-hidden": true } × .modal-body %p - %strong= label.name - %span will be permanently deleted from #{label.subject_name}. This cannot be undone. + = _('<strong>%{label_name}</strong> <span>will be permanently deleted from %{subject_name}. This cannot be undone.</span>').html_safe % { label_name: label.name, subject_name: label.subject_name } .modal-footer - %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel + %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }= _('Cancel') - = link_to 'Delete label', + = link_to _('Delete label'), label.destroy_path, - title: 'Delete', + title: _('Delete'), method: :delete, class: 'btn btn-remove' |