diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-13 12:09:23 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-13 12:09:23 +0300 |
commit | 4cb5e5011abfe8d50ac3a7ebd0018c563c6d7af4 (patch) | |
tree | 82591df15758864325897043f855b4e4dfcb6a56 /app | |
parent | 0301a0cad0063d76b1607358dc6c711ea043fdda (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
29 files changed, 272 insertions, 90 deletions
diff --git a/app/assets/images/cluster_app_logos/modsecurity.png b/app/assets/images/cluster_app_logos/modsecurity.png Binary files differnew file mode 100644 index 00000000000..fd58275e1d7 --- /dev/null +++ b/app/assets/images/cluster_app_logos/modsecurity.png diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index dc1328a2236..e20c87ed8a0 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -256,6 +256,7 @@ export default class Clusters { eventHub.$on('uninstallApplication', data => this.uninstallApplication(data)); eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data)); eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data)); + eventHub.$on('resetIngressModSecurityEnabled', id => this.resetIngressModSecurityEnabled(id)); // Add event listener to all the banner close buttons this.addBannerCloseHandler(this.unreachableContainer, 'unreachable'); this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure'); @@ -270,6 +271,7 @@ export default class Clusters { eventHub.$off('setCrossplaneProviderStack'); eventHub.$off('uninstallApplication'); eventHub.$off('setIngressModSecurityEnabled'); + eventHub.$off('resetIngressModSecurityEnabled'); } initPolling(method, successCallback, errorCallback) { @@ -523,6 +525,10 @@ export default class Clusters { this.store.updateAppProperty(id, 'modsecurity_enabled', modSecurityEnabled); } + resetIngressModSecurityEnabled(id) { + this.store.updateAppProperty(id, 'isEditingModSecurityEnabled', false); + } + destroy() { this.destroyed = true; diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 9058f1c0141..442c52110f2 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -119,9 +119,6 @@ export default { ingressInstalled() { return this.applications.ingress.status === APPLICATION_STATUS.INSTALLED; }, - ingressEnableModsecurity() { - return this.applications.ingress.modsecurity_enabled; - }, ingressExternalEndpoint() { return this.applications.ingress.externalIp || this.applications.ingress.externalHostname; }, diff --git a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue index c30015f31de..98a783aab6e 100644 --- a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue +++ b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue @@ -1,19 +1,22 @@ <script> import _ from 'lodash'; import { __ } from '../../locale'; -import LoadingButton from '~/vue_shared/components/loading_button.vue'; import { APPLICATION_STATUS, INGRESS } from '~/clusters/constants'; -import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import { GlAlert, GlSprintf, GlLink, GlToggle, GlButton } from '@gitlab/ui'; import eventHub from '~/clusters/event_hub'; +import modSecurityLogo from 'images/cluster_app_logos/modsecurity.png'; -const { UPDATING, UNINSTALLING } = APPLICATION_STATUS; +const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS; export default { + title: 'ModSecurity Web Application Firewall', + modsecurityUrl: 'https://modsecurity.org/about.html', components: { - LoadingButton, GlAlert, GlSprintf, GlLink, + GlToggle, + GlButton, }, props: { ingress: { @@ -26,6 +29,10 @@ export default { default: '', }, }, + data: () => ({ + modSecurityLogo, + hasValueChanged: false, + }), computed: { modSecurityEnabled: { get() { @@ -36,6 +43,11 @@ export default { id: INGRESS, modSecurityEnabled: isEnabled, }); + if (this.hasValueChanged) { + this.resetStatus(); + } else { + this.hasValueChanged = true; + } }, }, ingressModSecurityDescription() { @@ -45,13 +57,21 @@ export default { return [UPDATING].includes(this.ingress.status); }, saveButtonDisabled() { - return [UNINSTALLING, UPDATING].includes(this.ingress.status); + return [UNINSTALLING, UPDATING, INSTALLING].includes(this.ingress.status); }, saveButtonLabel() { return this.saving ? __('Saving') : __('Save changes'); }, - ingressInstalled() { - return this.ingress.installed; + /** + * Returns true either when: + * - The application is getting updated. + * - The user has changed some of the settings for an application which is + * neither getting installed nor updated. + */ + showButtons() { + return ( + this.saving || (this.hasValueChanged && [INSTALLED, UPDATED].includes(this.ingress.status)) + ); }, }, methods: { @@ -60,6 +80,11 @@ export default { id: INGRESS, params: { modsecurity_enabled: this.ingress.modsecurity_enabled }, }); + this.resetStatus(); + }, + resetStatus() { + eventHub.$emit('resetIngressModSecurityEnabled', INGRESS); + this.hasValueChanged = false; }, }, }; @@ -75,42 +100,65 @@ export default { @dismiss="alert = null" > {{ - s__('ClusterIntegration|Something went wrong while updating the Web Application Firewall.') + s__( + 'ClusterIntegration|Something went wrong while trying to save your settings. Please try again.', + ) }} </gl-alert> - <div class="form-group"> - <div class="form-check form-check-inline"> - <input - v-model="modSecurityEnabled" - type="checkbox" - autocomplete="off" - class="form-check-input" + <div class="gl-responsive-table-row-layout" role="row"> + <div class="table-section append-right-8 section-align-top" role="gridcell"> + <img + :src="modSecurityLogo" + :alt="`${$options.title} logo`" + class="cluster-application-logo avatar s40" /> - <label class="form-check-label label-bold" for="ingress-enable-modsecurity"> - {{ s__('ClusterIntegration|Enable Web Application Firewall') }} - </label> </div> - <p class="form-text text-muted"> + <div class="table-section section-wrap" role="gridcell"> <strong> - <gl-sprintf - :message="s__('ClusterIntegration|Learn more about %{linkStart}ModSecurity%{linkEnd}')" - > - <template #link="{ content }"> - <gl-link :href="ingressModSecurityDescription" target="_blank" - >{{ content }} - </gl-link> - </template> - </gl-sprintf> + <gl-link :href="$options.modsecurityUrl" target="_blank">{{ $options.title }} </gl-link> </strong> - </p> - <loading-button - v-if="ingressInstalled" - class="btn-success mt-1" - :loading="saving" - :disabled="saveButtonDisabled" - :label="saveButtonLabel" - @click="updateApplication" - /> + <div class="form-group"> + <p class="form-text text-muted"> + <strong> + <gl-sprintf + :message=" + s__( + 'ClusterIntegration|Real-time web application monitoring, logging and access control. %{linkStart}More information%{linkEnd}', + ) + " + > + <template #link="{ content }"> + <gl-link :href="ingressModSecurityDescription" target="_blank" + >{{ content }} + </gl-link> + </template> + </gl-sprintf> + </strong> + </p> + <div class="form-check form-check-inline mt-3"> + <gl-toggle + v-model="modSecurityEnabled" + :label-on="__('Enabled')" + :label-off="__('Disabled')" + :disabled="saveButtonDisabled" + label-position="right" + /> + </div> + <div v-if="showButtons"> + <gl-button + class="btn-success inline mr-1" + :loading="saving" + :disabled="saveButtonDisabled" + @click="updateApplication" + > + {{ saveButtonLabel }} + </gl-button> + <gl-button :disabled="saveButtonDisabled" @click="resetStatus"> + {{ __('Cancel') }} + </gl-button> + </div> + </div> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index ffe71455b2d..1d17170cea1 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -211,9 +211,7 @@ export default class ClusterStore { this.state.applications.ingress.externalIp = serverAppEntry.external_ip; this.state.applications.ingress.externalHostname = serverAppEntry.external_hostname; if (!this.state.applications.ingress.isEditingModSecurityEnabled) { - this.state.applications.ingress.modsecurity_enabled = - serverAppEntry.modsecurity_enabled || - this.state.applications.ingress.modsecurity_enabled; + this.state.applications.ingress.modsecurity_enabled = serverAppEntry.modsecurity_enabled; } } else if (appId === CERT_MANAGER) { this.state.applications.cert_manager.email = diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 515d513e3ee..577612de06a 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -77,6 +77,9 @@ export default { v-gl-tooltip title="Jump to next unresolved thread" class="btn btn-default discussion-next-btn" + data-track-event="click_button" + data-track-label="mr_next_unresolved_thread" + data-track-property="click_next_unresolved_thread_top" @click="jumpToNextDiscussion" > <icon name="comment-next" /> diff --git a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue index e66abcfddbb..b71ce1b6a0a 100644 --- a/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue +++ b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue @@ -28,6 +28,9 @@ export default { v-gl-tooltip class="btn btn-default discussion-next-btn" :title="s__('MergeRequests|Jump to next unresolved thread')" + data-track-event="click_button" + data-track-label="mr_next_unresolved_thread" + data-track-property="click_next_unresolved_thread" @click="jumpToNextRelativeDiscussion(fromDiscussionId)" > <icon name="comment-next" /> diff --git a/app/assets/javascripts/pages/admin/integrations/edit/index.js b/app/assets/javascripts/pages/admin/integrations/edit/index.js new file mode 100644 index 00000000000..2d77f2686f7 --- /dev/null +++ b/app/assets/javascripts/pages/admin/integrations/edit/index.js @@ -0,0 +1,16 @@ +import IntegrationSettingsForm from '~/integrations/integration_settings_form'; +import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; +import initAlertsSettings from '~/alerts_service_settings'; + +document.addEventListener('DOMContentLoaded', () => { + const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring'); + const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); + integrationSettingsForm.init(); + + if (prometheusSettingsWrapper) { + const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + prometheusMetrics.loadActiveMetrics(); + } + + initAlertsSettings(document.querySelector('.js-alerts-service-settings')); +}); diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js index 738bf08f1bf..d8fbb851ffb 100644 --- a/app/assets/javascripts/pages/projects/snippets/show/index.js +++ b/app/assets/javascripts/pages/projects/snippets/show/index.js @@ -14,5 +14,6 @@ document.addEventListener('DOMContentLoaded', () => { snippetEmbed(); } else { initSnippetsApp(); + initNotes(); } }); diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js index 6e00c14f43e..3bc9d4f957f 100644 --- a/app/assets/javascripts/pages/snippets/show/index.js +++ b/app/assets/javascripts/pages/snippets/show/index.js @@ -14,5 +14,6 @@ document.addEventListener('DOMContentLoaded', () => { snippetEmbed(); } else { initSnippetsApp(); + initNotes(); } }); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue index 6f77d2fa779..9c476d5b2e0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue @@ -64,7 +64,7 @@ export default { :deployment="deployment" :computed-deployment-status="computedDeploymentStatus" :show-visual-review-app="showVisualReviewApp" - :visual-review-app-metadata="visualReviewAppMeta" + :visual-review-app-meta="visualReviewAppMeta" /> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue index a11e62b048a..573fc388cca 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue @@ -173,7 +173,7 @@ export default { :app-button-text="appButtonText" :deployment="deployment" :show-visual-review-app="showVisualReviewApp" - :visual-review-app-metadata="visualReviewAppMeta" + :visual-review-app-meta="visualReviewAppMeta" /> <deployment-action-button v-if="stopUrl" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue index e1a1b1022e3..5dabd9fe5fe 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue @@ -93,8 +93,10 @@ export default { /> <visual-review-app-link v-if="showVisualReviewApp" + :view-app-display="appButtonText" :link="deploymentExternalUrl" :app-metadata="visualReviewAppMeta" + :changes="deployment.changes" /> </span> </template> diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 08796742f08..43cf0d4bd70 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -118,6 +118,14 @@ } } +.ssh-keys-list { + .last-used-at, + .expires, + .key-created-at { + line-height: 32px; + } +} + .key-created-at { line-height: 42px; } diff --git a/app/controllers/admin/integrations_controller.rb b/app/controllers/admin/integrations_controller.rb new file mode 100644 index 00000000000..715aa882bda --- /dev/null +++ b/app/controllers/admin/integrations_controller.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +class Admin::IntegrationsController < Admin::ApplicationController + include ServiceParams + + before_action :not_found, unless: :instance_level_integrations_enabled? + before_action :service, only: [:edit, :update, :test] + + def edit + end + + def update + @service.attributes = service_params[:service] + + if @service.save(context: :manual_change) + redirect_to edit_admin_application_settings_integration_path(@service), notice: success_message + else + render :edit + end + end + + def test + if @service.can_test? + render json: service_test_response, status: :ok + else + render json: {}, status: :not_found + end + end + + private + + def instance_level_integrations_enabled? + Feature.enabled?(:instance_level_integrations) + end + + def project + # TODO: Change to something more meaningful + Project.first + end + + def service + @service ||= project.find_or_initialize_service(params[:id]) + end + + def success_message + message = @service.active? ? _('activated') : _('settings saved, but not activated') + + _('%{service_title} %{message}.') % { service_title: @service.title, message: message } + end + + def service_test_response + unless @service.update(service_params[:service]) + return { error: true, message: _('Validations failed.'), service_response: @service.errors.full_messages.join(','), test_failed: false } + end + + data = @service.test_data(project, current_user) + outcome = @service.test(data) + + unless outcome[:success] + return { error: true, message: _('Test failed.'), service_response: outcome[:result].to_s, test_failed: true } + end + + {} + rescue Gitlab::HTTP::BlockedUrlError => e + { error: true, message: _('Test failed.'), service_response: e.message, test_failed: true } + end +end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 055d900eece..b9cb71ae89a 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -55,6 +55,6 @@ class Profiles::KeysController < Profiles::ApplicationController private def key_params - params.require(:key).permit(:title, :key) + params.require(:key).permit(:title, :key, :expires_at) end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index c916140211e..92c6ce324f7 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -52,28 +52,26 @@ class Projects::ServicesController < Projects::ApplicationController private def service_test_response - if @service.update(service_params[:service]) - data = @service.test_data(project, current_user) - outcome = @service.test(data) - - if outcome[:success] - {} - else - { error: true, message: _('Test failed.'), service_response: outcome[:result].to_s, test_failed: true } - end - else - { error: true, message: _('Validations failed.'), service_response: @service.errors.full_messages.join(','), test_failed: false } + unless @service.update(service_params[:service]) + return { error: true, message: _('Validations failed.'), service_response: @service.errors.full_messages.join(','), test_failed: false } + end + + data = @service.test_data(project, current_user) + outcome = @service.test(data) + + unless outcome[:success] + return { error: true, message: _('Test failed.'), service_response: outcome[:result].to_s, test_failed: true } end + + {} rescue Gitlab::HTTP::BlockedUrlError => e { error: true, message: _('Test failed.'), service_response: e.message, test_failed: true } end def success_message - if @service.active? - _("%{service_title} activated.") % { service_title: @service.title } - else - _("%{service_title} settings saved, but not activated.") % { service_title: @service.title } - end + message = @service.active? ? _('activated') : _('settings saved, but not activated') + + _('%{service_title} %{message}.') % { service_title: @service.title, message: message } end def service diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 0158d909468..64659208315 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -16,7 +16,7 @@ module Clusters include AfterCommitQueue default_value_for :ingress_type, :nginx - default_value_for :modsecurity_enabled, false + default_value_for :modsecurity_enabled, true default_value_for :version, VERSION enum ingress_type: { diff --git a/app/models/key.rb b/app/models/key.rb index afa0d489ef6..18fa8aaaa16 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -6,6 +6,7 @@ class Key < ApplicationRecord include AfterCommitQueue include Sortable include Sha256Attribute + include Expirable sha256_attribute :fingerprint_sha256 diff --git a/app/views/admin/integrations/_form.html.haml b/app/views/admin/integrations/_form.html.haml new file mode 100644 index 00000000000..aa865c3b052 --- /dev/null +++ b/app/views/admin/integrations/_form.html.haml @@ -0,0 +1,12 @@ +%h3.page-title + = @service.title + +%p= @service.description + += form_for @service, as: :service, url: admin_application_settings_integration_path, method: :put, html: { class: 'gl-show-field-errors fieldset-form integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_admin_application_settings_integration_path(@service) } } do |form| + = render 'shared/service_settings', form: form, service: @service + + - if @service.editable? + .footer-block.row-content-block + = service_save_button(@service) + = link_to _('Cancel'), admin_application_settings_integration_path, class: 'btn btn-cancel' diff --git a/app/views/admin/integrations/edit.html.haml b/app/views/admin/integrations/edit.html.haml new file mode 100644 index 00000000000..dea0f524f03 --- /dev/null +++ b/app/views/admin/integrations/edit.html.haml @@ -0,0 +1,5 @@ +- add_to_breadcrumbs _('Integrations'), admin_application_settings_integration_path +- breadcrumb_title @service.title +- page_title @service.title, _('Integrations') + += render 'form' diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 495ee6a04ea..d18e91c0b14 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -4,7 +4,7 @@ %p #{@service.description} template. = form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form| - = render 'shared/service_settings', form: form, subject: @service + = render 'shared/service_settings', form: form, service: @service .footer-block.row-content-block = form.submit 'Save', class: 'btn btn-success' diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index 63ef5eaa172..34e81285328 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -6,10 +6,15 @@ = f.label :key, s_('Profiles|Key'), class: 'label-bold' %p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Don't use your private SSH key.") = f.text_area :key, class: "form-control js-add-ssh-key-validation-input qa-key-public-key-field", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-ed25519 …" or "ssh-rsa …"') - .form-group - = f.label :title, _('Title'), class: 'label-bold' - = f.text_field :title, class: "form-control input-lg qa-key-title-field", required: true, placeholder: s_('Profiles|e.g. My MacBook key') - %p.form-text.text-muted= _('Name your individual key via a title') + .form-row + .col.form-group + = f.label :title, _('Title'), class: 'label-bold' + = f.text_field :title, class: "form-control input-lg qa-key-title-field", required: true, placeholder: s_('Profiles|e.g. My MacBook key') + %p.form-text.text-muted= s_('Profiles|Give your individual key a title') + + .col.form-group + = f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold' + = f.date_field :expires_at, class: "form-control input-lg qa-key-expiry-field", min: Date.tomorrow .js-add-ssh-key-validation-warning.hide .bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' } diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index 0e94e6563fd..b227041c9de 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -1,24 +1,31 @@ -%li.key-list-item - .float-left.append-right-10 +%li.d-flex.align-items-center.key-list-item + .append-right-10 - if key.valid? - = icon 'key', class: 'settings-list-icon d-none d-sm-block' + - if key.expired? + %span.d-inline-block.has-tooltip{ title: s_('Profiles|Your key has expired') } + = sprite_icon('warning-solid', size: 16, css_class: 'settings-list-icon d-none d-sm-block') + - else + = sprite_icon('key', size: 16, css_class: 'settings-list-icon d-none d-sm-block ') - else - = icon 'exclamation-triangle', class: 'settings-list-icon d-none d-sm-block has-tooltip', - title: key.errors.full_messages.join(', ') + %span.d-inline-block.has-tooltip{ title: key.errors.full_messages.join(', ') } + = sprite_icon('warning-solid', size: 16, css_class: 'settings-list-icon d-none d-sm-block') - - .key-list-item-info + .key-list-item-info.w-100.float-none = link_to path_to_key(key, is_admin), class: "title" do = key.title %span.text-truncate = key.fingerprint - .last-used-at - last used: - = key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a' - .float-right - %span.key-created-at - = s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)} - - if key.can_delete? - = link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent prepend-left-10" do - %span.sr-only= _('Remove') - = icon('trash') + + .key-list-item-dates.d-flex.align-items-start.justify-content-between + %span.last-used-at.append-right-10 + = s_('Profiles|Last used:') + = key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : _('Never') + %span.expires.append-right-10 + = s_('Profiles|Expires:') + = key.expires_at ? key.expires_at.to_date : _('Never') + %span.key-created-at + = s_('Profiles|Created %{time_ago}'.html_safe) % { time_ago:time_ago_with_tooltip(key.created_at)} + - if key.can_delete? + = link_to path_to_key(key, is_admin), data: { confirm: _('Are you sure?')}, method: :delete, class: "btn btn-transparent prepend-left-10 align-baseline" do + %span.sr-only= _('Remove') + = sprite_icon('remove', size: 16) diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 02f1a267044..88deb0f11cb 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -12,8 +12,11 @@ %span.light= _('Created on:') %strong= @key.created_at.to_s(:medium) %li + %span.light= _('Expires:') + %strong= @key.expires_at.try(:to_s, :medium) || _('Never') + %li %span.light= _('Last used on:') - %strong= @key.last_used_at.try(:to_s, :medium) || 'N/A' + %strong= @key.last_used_at.try(:to_s, :medium) || _('Never') .col-md-8 = form_errors(@key, type: 'key') unless @key.valid? diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml index 8b862522645..176d7a42002 100644 --- a/app/views/profiles/keys/_key_table.html.haml +++ b/app/views/profiles/keys/_key_table.html.haml @@ -1,7 +1,7 @@ - is_admin = local_assigns.fetch(:admin, false) - if @keys.any? - %ul.content-list{ data: { qa_selector: 'ssh_keys_list' } } + %ul.content-list.ssh-keys-list{ data: { qa_selector: 'ssh_keys_list' } } = render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin } - else %p.settings-message.text-center diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index caaf164a763..971107675ab 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -23,4 +23,5 @@ = _('There are no matching files') %p.text-secondary = _('Try using a different search term to find the file you are looking for.') - = spinner nil, true + .text-center.prepend-top-default.loading + .spinner.spinner-md diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 582f3d6fce4..a0d9d29a7ae 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -11,7 +11,7 @@ %p= @service.detailed_description .col-lg-9 = form_for(@service, as: :service, url: project_service_path(@project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form| - = render 'shared/service_settings', form: form, subject: @service + = render 'shared/service_settings', form: form, service: @service - if @service.editable? .footer-block.row-content-block = service_save_button(@service) diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 4415c654ab9..aeda7ea9909 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -1,7 +1,7 @@ = form_errors(@service) - if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true) - = render "projects/services/#{@service.to_param}/help", subject: subject + = render "projects/services/#{@service.to_param}/help", subject: @service - elsif @service.help.present? .info-well .well-segment |