Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-18 15:08:08 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-18 15:08:08 +0300
commit48650fe1bfc1e3d20ec3a5702ef4d64e9fe69912 (patch)
tree0f73ad6e03989c301b79490ddb30125c233e4eff /app
parent1b9a2ce27825c02cc14b594ed5ea061fccf1d957 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue14
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue17
-rw-r--r--app/assets/javascripts/blob/components/blob_content_error.vue71
-rw-r--r--app/assets/javascripts/blob/components/constants.js56
-rw-r--r--app/assets/javascripts/helpers/avatar_helper.js5
-rw-r--r--app/assets/javascripts/main.js3
-rw-r--r--app/assets/javascripts/persistent_user_callout.js5
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue34
-rw-r--r--app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/identicon.vue2
-rw-r--r--app/controllers/google_api/authorizations_controller.rb3
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/models/clusters/cluster.rb12
-rw-r--r--app/models/clusters/concerns/application_status.rb9
-rw-r--r--app/services/ci/create_job_artifacts_service.rb5
-rw-r--r--app/services/clusters/parse_cluster_applications_artifact_service.rb95
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/shared/_delete_label_modal.html.haml11
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">&bull;</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 } &times;
.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'