summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-13 09:09:23 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-13 09:09:23 +0000
commit4cb5e5011abfe8d50ac3a7ebd0018c563c6d7af4 (patch)
tree82591df15758864325897043f855b4e4dfcb6a56 /app
parent0301a0cad0063d76b1607358dc6c711ea043fdda (diff)
downloadgitlab-ce-4cb5e5011abfe8d50ac3a7ebd0018c563c6d7af4.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/cluster_app_logos/modsecurity.pngbin0 -> 6235 bytes
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js6
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue3
-rw-r--r--app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue122
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js4
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue3
-rw-r--r--app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue3
-rw-r--r--app/assets/javascripts/pages/admin/integrations/edit/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/snippets/show/index.js1
-rw-r--r--app/assets/javascripts/pages/snippets/show/index.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue2
-rw-r--r--app/assets/stylesheets/pages/profile.scss8
-rw-r--r--app/controllers/admin/integrations_controller.rb67
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb30
-rw-r--r--app/models/clusters/applications/ingress.rb2
-rw-r--r--app/models/key.rb1
-rw-r--r--app/views/admin/integrations/_form.html.haml12
-rw-r--r--app/views/admin/integrations/edit.html.haml5
-rw-r--r--app/views/admin/services/_form.html.haml2
-rw-r--r--app/views/profiles/keys/_form.html.haml13
-rw-r--r--app/views/profiles/keys/_key.html.haml41
-rw-r--r--app/views/profiles/keys/_key_details.html.haml5
-rw-r--r--app/views/profiles/keys/_key_table.html.haml2
-rw-r--r--app/views/projects/find_file/show.html.haml3
-rw-r--r--app/views/projects/services/_form.html.haml2
-rw-r--r--app/views/shared/_service_settings.html.haml2
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
new file mode 100644
index 00000000000..fd58275e1d7
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/modsecurity.png
Binary files differ
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