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
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/grafana_integration/components/grafana_integration.vue82
-rw-r--r--app/assets/javascripts/grafana_integration/index.js15
-rw-r--r--app/assets/javascripts/grafana_integration/store/actions.js38
-rw-r--r--app/assets/javascripts/grafana_integration/store/index.js16
-rw-r--r--app/assets/javascripts/grafana_integration/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/grafana_integration/store/mutations.js10
-rw-r--r--app/assets/javascripts/grafana_integration/store/state.js5
-rw-r--r--app/assets/javascripts/pages/projects/settings/operations/show/index.js4
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js7
-rw-r--r--app/models/analytics/cycle_analytics/project_stage.rb8
-rw-r--r--app/models/concerns/analytics/cycle_analytics/stage.rb6
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml10
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml4
-rw-r--r--app/views/projects/deployments/_confirm_rollback_modal.html.haml2
-rw-r--r--app/views/projects/settings/operations/_grafana_integration.html.haml2
-rw-r--r--app/views/projects/settings/operations/show.html.haml1
-rw-r--r--changelogs/unreleased/31658-add-rollback-dialog-environment.yml5
-rw-r--r--doc/administration/packages/container_registry.md2
-rw-r--r--locale/gitlab.pot31
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb3
-rw-r--r--spec/features/groups/group_page_with_external_authorization_service_spec.rb4
-rw-r--r--spec/features/groups_spec.rb18
-rw-r--r--spec/features/projects/settings/operations_settings_spec.rb35
-rw-r--r--spec/frontend/fixtures/static/signin_tabs.html3
-rw-r--r--spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap92
-rw-r--r--spec/frontend/grafana_integration/components/grafana_integration_spec.js124
-rw-r--r--spec/frontend/grafana_integration/store/mutations_spec.js28
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js46
-rw-r--r--spec/javascripts/test_bundle.js33
-rw-r--r--spec/models/analytics/cycle_analytics/project_stage_spec.rb10
-rw-r--r--spec/views/projects/deployments/_confirm_rollback_modal_spec.html_spec.rb2
31 files changed, 600 insertions, 48 deletions
diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
new file mode 100644
index 00000000000..2d3212429db
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { mapState, mapActions } from 'vuex';
+
+export default {
+ components: {
+ GlButton,
+ GlFormGroup,
+ GlFormInput,
+ GlLink,
+ Icon,
+ },
+ data() {
+ return { placeholderUrl: 'https://my-url.grafana.net/my-dashboard' };
+ },
+ computed: {
+ ...mapState(['operationsSettingsEndpoint', 'grafanaToken', 'grafanaUrl']),
+ localGrafanaToken: {
+ get() {
+ return this.grafanaToken;
+ },
+ set(token) {
+ this.setGrafanaToken(token);
+ },
+ },
+ localGrafanaUrl: {
+ get() {
+ return this.grafanaUrl;
+ },
+ set(url) {
+ this.setGrafanaUrl(url);
+ },
+ },
+ },
+ methods: {
+ ...mapActions(['setGrafanaUrl', 'setGrafanaToken', 'updateGrafanaIntegration']),
+ },
+};
+</script>
+
+<template>
+ <section id="grafana" class="settings no-animate js-grafana-integration">
+ <div class="settings-header">
+ <h4 class="js-section-header">
+ {{ s__('GrafanaIntegration|Grafana Authentication') }}
+ </h4>
+ <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
+ <p class="js-section-sub-header">
+ {{ s__('GrafanaIntegration|Embed Grafana charts in GitLab issues.') }}
+ </p>
+ </div>
+ <div class="settings-content">
+ <form>
+ <gl-form-group
+ :label="s__('GrafanaIntegration|Grafana URL')"
+ label-for="grafana-url"
+ :description="s__('GrafanaIntegration|Enter the base URL of the Grafana instance.')"
+ >
+ <gl-form-input id="grafana-url" v-model="localGrafanaUrl" :placeholder="placeholderUrl" />
+ </gl-form-group>
+ <gl-form-group :label="s__('GrafanaIntegration|API Token')" label-for="grafana-token">
+ <gl-form-input id="grafana-token" v-model="localGrafanaToken" />
+ <p class="form-text text-muted">
+ {{ s__('GrafanaIntegration|Enter the Grafana API Token.') }}
+ <a
+ href="https://grafana.com/docs/http_api/auth/#create-api-token"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ <icon name="external-link" class="vertical-align-middle" />
+ </a>
+ </p>
+ </gl-form-group>
+ <gl-button variant="success" @click="updateGrafanaIntegration">
+ {{ __('Save Changes') }}
+ </gl-button>
+ </form>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/grafana_integration/index.js b/app/assets/javascripts/grafana_integration/index.js
new file mode 100644
index 00000000000..58c28e09f80
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import store from './store';
+import GrafanaIntegration from './components/grafana_integration.vue';
+
+export default () => {
+ const el = document.querySelector('.js-grafana-integration');
+
+ return new Vue({
+ el,
+ store: store(el.dataset),
+ render(createElement) {
+ return createElement(GrafanaIntegration);
+ },
+ });
+};
diff --git a/app/assets/javascripts/grafana_integration/store/actions.js b/app/assets/javascripts/grafana_integration/store/actions.js
new file mode 100644
index 00000000000..98085fdcb2d
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/store/actions.js
@@ -0,0 +1,38 @@
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import * as mutationTypes from './mutation_types';
+
+export const setGrafanaUrl = ({ commit }, url) => commit(mutationTypes.SET_GRAFANA_URL, url);
+
+export const setGrafanaToken = ({ commit }, token) =>
+ commit(mutationTypes.SET_GRAFANA_TOKEN, token);
+
+export const updateGrafanaIntegration = ({ state, dispatch }) =>
+ axios
+ .patch(state.operationsSettingsEndpoint, {
+ project: {
+ grafana_integration_attributes: {
+ grafana_url: state.grafanaUrl,
+ token: state.grafanaToken,
+ },
+ },
+ })
+ .then(() => dispatch('receiveGrafanaIntegrationUpdateSuccess'))
+ .catch(error => dispatch('receiveGrafanaIntegrationUpdateError', error));
+
+export const receiveGrafanaIntegrationUpdateSuccess = () => {
+ /**
+ * The operations_controller currently handles successful requests
+ * by creating a flash banner messsage to notify the user.
+ */
+ refreshCurrentPage();
+};
+
+export const receiveGrafanaIntegrationUpdateError = (_, error) => {
+ const { response } = error;
+ const message = response.data && response.data.message ? response.data.message : '';
+
+ createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
+};
diff --git a/app/assets/javascripts/grafana_integration/store/index.js b/app/assets/javascripts/grafana_integration/store/index.js
new file mode 100644
index 00000000000..e96bb1e8aad
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import createState from './state';
+import * as actions from './actions';
+import mutations from './mutations';
+
+Vue.use(Vuex);
+
+export const createStore = initialState =>
+ new Vuex.Store({
+ state: createState(initialState),
+ actions,
+ mutations,
+ });
+
+export default createStore;
diff --git a/app/assets/javascripts/grafana_integration/store/mutation_types.js b/app/assets/javascripts/grafana_integration/store/mutation_types.js
new file mode 100644
index 00000000000..33ce3228823
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/store/mutation_types.js
@@ -0,0 +1,2 @@
+export const SET_GRAFANA_URL = 'SET_GRAFANA_URL';
+export const SET_GRAFANA_TOKEN = 'SET_GRAFANA_TOKEN';
diff --git a/app/assets/javascripts/grafana_integration/store/mutations.js b/app/assets/javascripts/grafana_integration/store/mutations.js
new file mode 100644
index 00000000000..e8d63a9a732
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/store/mutations.js
@@ -0,0 +1,10 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_GRAFANA_URL](state, url) {
+ state.grafanaUrl = url;
+ },
+ [types.SET_GRAFANA_TOKEN](state, token) {
+ state.grafanaToken = token;
+ },
+};
diff --git a/app/assets/javascripts/grafana_integration/store/state.js b/app/assets/javascripts/grafana_integration/store/state.js
new file mode 100644
index 00000000000..c25742c82bc
--- /dev/null
+++ b/app/assets/javascripts/grafana_integration/store/state.js
@@ -0,0 +1,5 @@
+export default (initialState = {}) => ({
+ operationsSettingsEndpoint: initialState.operationsSettingsEndpoint,
+ grafanaToken: initialState.grafanaIntegrationToken || '',
+ grafanaUrl: initialState.grafanaIntegrationUrl || '',
+});
diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
index 98e19705976..7037933bc5a 100644
--- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
@@ -1,9 +1,13 @@
import mountErrorTrackingForm from '~/error_tracking_settings';
import mountOperationSettings from '~/operation_settings';
+import mountGrafanaIntegration from '~/grafana_integration';
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
mountErrorTrackingForm();
mountOperationSettings();
+ if (gon.features.gfmGrafanaIntegration) {
+ mountGrafanaIntegration();
+ }
initSettingsPanels();
});
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index c0e798e004f..66ee2d9303f 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -20,12 +20,17 @@ document.addEventListener('DOMContentLoaded', () => {
// Save the URL fragment from the current window location. This will be present if the user was
// redirected to sign-in after attempting to access a protected URL that included a fragment.
preserveUrlFragment(window.location.hash);
+});
+export default function trackData() {
if (gon.tracking_data) {
const tab = document.querySelector(".new-session-tabs a[href='#register-pane']");
const { category, action, ...data } = gon.tracking_data;
+
tab.addEventListener('click', () => {
Tracking.event(category, action, data);
});
}
-});
+}
+
+trackData();
diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb
index 23f0db0829b..0cdf199a146 100644
--- a/app/models/analytics/cycle_analytics/project_stage.rb
+++ b/app/models/analytics/cycle_analytics/project_stage.rb
@@ -10,6 +10,14 @@ module Analytics
alias_attribute :parent, :project
alias_attribute :parent_id, :project_id
+
+ def self.relative_positioning_query_base(stage)
+ where(project_id: stage.project_id)
+ end
+
+ def self.relative_positioning_parent_column
+ :project_id
+ end
end
end
end
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index 54e9a13d1ea..1376dd97a49 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -4,6 +4,7 @@ module Analytics
module CycleAnalytics
module Stage
extend ActiveSupport::Concern
+ include RelativePositioning
included do
validates :name, presence: true
@@ -17,6 +18,7 @@ module Analytics
alias_attribute :custom_stage?, :custom
scope :default_stages, -> { where(custom: false) }
+ scope :ordered, -> { order(:relative_position, :id) }
end
def parent=(_)
@@ -58,6 +60,10 @@ module Analytics
end_event_identifier.to_s.eql?(stage_params[:end_event_identifier].to_s)
end
+ def find_with_same_parent!(id)
+ parent.cycle_analytics_stages.find(id)
+ end
+
private
def validate_stage_event_pairs
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 4930c6cf5f7..a6d2c894185 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -16,13 +16,19 @@
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
- = _('Overview')
+ - if @group.subgroup?
+ = _('Subgroup overview')
+ - else
+ = _('Group overview')
%ul.sidebar-sub-level-items
= nav_link(path: ['groups#show', 'groups#details', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do
= link_to group_path(@group) do
%strong.fly-out-top-item-name
- = _('Overview')
+ - if @group.subgroup?
+ = _('Subgroup overview')
+ - else
+ = _('Group overview')
%li.divider.fly-out-top-item
= nav_link(path: ['groups#show', 'groups#details', 'groups#subgroups'], html_options: { class: 'home' }) do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 247fbfefde9..fdad2a64a80 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -13,13 +13,13 @@
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
- = _('Project')
+ = _('Project overview')
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
= link_to project_path(@project) do
%strong.fly-out-top-item-name
- = _('Project')
+ = _('Project overview')
%li.divider.fly-out-top-item
= nav_link(path: 'projects#show') do
= link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
diff --git a/app/views/projects/deployments/_confirm_rollback_modal.html.haml b/app/views/projects/deployments/_confirm_rollback_modal.html.haml
index ff40e404e5f..9162827b501 100644
--- a/app/views/projects/deployments/_confirm_rollback_modal.html.haml
+++ b/app/views/projects/deployments/_confirm_rollback_modal.html.haml
@@ -13,7 +13,7 @@
%p= s_('Environments|This action will relaunch the job for commit %{commit_id}, putting the environment in a previous version. Are you sure you want to continue?').html_safe % {commit_id: commit_sha}
- else
%p
- = s_('Environments|This action will run the job defined by staging for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?').html_safe % {commit_id: commit_sha}
+ = s_('Environments|This action will run the job defined by %{environment_name} for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?').html_safe % {commit_id: commit_sha, environment_name: @environment.name}
.modal-footer
= button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' }
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-danger' do
diff --git a/app/views/projects/settings/operations/_grafana_integration.html.haml b/app/views/projects/settings/operations/_grafana_integration.html.haml
new file mode 100644
index 00000000000..ae90b6dd5aa
--- /dev/null
+++ b/app/views/projects/settings/operations/_grafana_integration.html.haml
@@ -0,0 +1,2 @@
+.js-grafana-integration{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
+ grafana_integration: { url: grafana_integration_url, token: grafana_integration_token } } }
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index 0a7a155bc12..3c955e5f558 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -5,4 +5,5 @@
= render_if_exists 'projects/settings/operations/incidents'
= render 'projects/settings/operations/error_tracking'
= render 'projects/settings/operations/external_dashboard'
+= render 'projects/settings/operations/grafana_integration'
= render_if_exists 'projects/settings/operations/tracing'
diff --git a/changelogs/unreleased/31658-add-rollback-dialog-environment.yml b/changelogs/unreleased/31658-add-rollback-dialog-environment.yml
new file mode 100644
index 00000000000..d50feb434a2
--- /dev/null
+++ b/changelogs/unreleased/31658-add-rollback-dialog-environment.yml
@@ -0,0 +1,5 @@
+---
+title: Fix environment name in rollback dialog
+merge_request: 19209
+author:
+type: fixed
diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md
index bf86a549fda..980d4da65df 100644
--- a/doc/administration/packages/container_registry.md
+++ b/doc/administration/packages/container_registry.md
@@ -353,7 +353,7 @@ configuration.
NOTE: **Note:** Enabling a storage driver other than `filesystem` would mean
that your Docker client needs to be able to access the storage backend directly.
-In that case, you must use an address that resolves and is accessible outside GitLab server.
+In that case, you must use an address that resolves and is accessible outside GitLab server. The Docker client will continue to authenticate via GitLab but data transfer will be direct to and from the storage backend.
The different supported drivers are:
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a53be729dae..1097cc0238d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6339,10 +6339,10 @@ msgstr ""
msgid "Environments|This action will relaunch the job for commit %{linkStart}%{commitId}%{linkEnd}, putting the environment in a previous version. Are you sure you want to continue?"
msgstr ""
-msgid "Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
+msgid "Environments|This action will run the job defined by %{environment_name} for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
msgstr ""
-msgid "Environments|This action will run the job defined by staging for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
+msgid "Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
msgstr ""
msgid "Environments|Updated"
@@ -8210,6 +8210,24 @@ msgstr ""
msgid "Grafana URL"
msgstr ""
+msgid "GrafanaIntegration|API Token"
+msgstr ""
+
+msgid "GrafanaIntegration|Embed Grafana charts in GitLab issues."
+msgstr ""
+
+msgid "GrafanaIntegration|Enter the Grafana API Token."
+msgstr ""
+
+msgid "GrafanaIntegration|Enter the base URL of the Grafana instance."
+msgstr ""
+
+msgid "GrafanaIntegration|Grafana Authentication"
+msgstr ""
+
+msgid "GrafanaIntegration|Grafana URL"
+msgstr ""
+
msgid "Grant access"
msgstr ""
@@ -8273,6 +8291,9 @@ msgstr ""
msgid "Group name"
msgstr ""
+msgid "Group overview"
+msgstr ""
+
msgid "Group overview content"
msgstr ""
@@ -12737,6 +12758,9 @@ msgstr ""
msgid "Project name"
msgstr ""
+msgid "Project overview"
+msgstr ""
+
msgid "Project slug"
msgstr ""
@@ -15847,6 +15871,9 @@ msgstr ""
msgid "StorageSize|Unknown"
msgstr ""
+msgid "Subgroup overview"
+msgstr ""
+
msgid "SubgroupCreationLevel|Allowed to create subgroups"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index 3f99ae644c7..319fd510a18 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -3,7 +3,8 @@
require 'pathname'
module QA
- context 'Configure' do
+ # Issue: https://gitlab.com/gitlab-org/gitlab/issues/35156
+ context 'Configure', :quarantine do
def login
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
diff --git a/spec/features/groups/group_page_with_external_authorization_service_spec.rb b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
index c05c3f4f3d6..823c8cc8fad 100644
--- a/spec/features/groups/group_page_with_external_authorization_service_spec.rb
+++ b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
@@ -15,7 +15,7 @@ describe 'The group page' do
def expect_all_sidebar_links
within('.nav-sidebar') do
- expect(page).to have_link('Overview')
+ expect(page).to have_link('Group overview')
expect(page).to have_link('Details')
expect(page).to have_link('Activity')
expect(page).to have_link('Issues')
@@ -44,7 +44,7 @@ describe 'The group page' do
visit group_path(group)
within('.nav-sidebar') do
- expect(page).to have_link('Overview')
+ expect(page).to have_link('Group overview')
expect(page).to have_link('Details')
expect(page).not_to have_link('Activity')
expect(page).not_to have_link('Contribution Analytics')
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 4fd241dedd1..e958ebb1275 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -237,14 +237,28 @@ describe 'Group' do
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, namespace: group) }
- let!(:path) { group_path(group) }
it 'renders projects and groups on the page' do
- visit path
+ visit group_path(group)
wait_for_requests
expect(page).to have_content(nested_group.name)
expect(page).to have_content(project.name)
+ expect(page).to have_link('Group overview')
+ end
+
+ it 'renders subgroup page with the text "Subgroup overview"' do
+ visit group_path(nested_group)
+ wait_for_requests
+
+ expect(page).to have_link('Subgroup overview')
+ end
+
+ it 'renders project page with the text "Project overview"' do
+ visit project_path(project)
+ wait_for_requests
+
+ expect(page).to have_link('Project overview')
end
end
diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb
index d96e243d96b..ca937651af8 100644
--- a/spec/features/projects/settings/operations_settings_spec.rb
+++ b/spec/features/projects/settings/operations_settings_spec.rb
@@ -102,5 +102,40 @@ describe 'Projects > Settings > For a forked project', :js do
end
end
end
+
+ context 'grafana integration settings form' do
+ it 'is not present when the feature flag is disabled' do
+ stub_feature_flags(gfm_grafana_integration: false)
+
+ visit project_settings_operations_path(project)
+
+ wait_for_requests
+
+ expect(page).to have_no_css('.js-grafana-integration')
+ end
+
+ it 'is present when the feature flag is enabled' do
+ visit project_settings_operations_path(project)
+
+ wait_for_requests
+
+ within '.js-grafana-integration' do
+ click_button('Expand')
+ end
+
+ expect(page).to have_content('Grafana URL')
+ expect(page).to have_content('API Token')
+ expect(page).to have_button('Save Changes')
+
+ fill_in('grafana-url', with: 'http://gitlab-test.grafana.net')
+ fill_in('grafana-token', with: 'token')
+
+ click_button('Save Changes')
+
+ wait_for_requests
+
+ assert_text('Your changes have been saved')
+ end
+ end
end
end
diff --git a/spec/frontend/fixtures/static/signin_tabs.html b/spec/frontend/fixtures/static/signin_tabs.html
index 7e66ab9394b..247a6b03054 100644
--- a/spec/frontend/fixtures/static/signin_tabs.html
+++ b/spec/frontend/fixtures/static/signin_tabs.html
@@ -5,4 +5,7 @@
<li>
<a href="#login-pane">Standard</a>
</li>
+<li>
+<a href="#register-pane">Register</a>
+</li>
</ul>
diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
new file mode 100644
index 00000000000..43239da344f
--- /dev/null
+++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
@@ -0,0 +1,92 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`grafana integration component default state to match the default snapshot 1`] = `
+<section
+ class="settings no-animate js-grafana-integration"
+ id="grafana"
+>
+ <div
+ class="settings-header"
+ >
+ <h4
+ class="js-section-header"
+ >
+
+ Grafana Authentication
+
+ </h4>
+
+ <glbutton-stub
+ class="js-settings-toggle"
+ >
+ Expand
+ </glbutton-stub>
+
+ <p
+ class="js-section-sub-header"
+ >
+
+ Embed Grafana charts in GitLab issues.
+
+ </p>
+ </div>
+
+ <div
+ class="settings-content"
+ >
+ <form>
+ <glformgroup-stub
+ description="Enter the base URL of the Grafana instance."
+ label="Grafana URL"
+ label-for="grafana-url"
+ >
+ <glforminput-stub
+ id="grafana-url"
+ placeholder="https://my-url.grafana.net/my-dashboard"
+ value="http://test.host"
+ />
+ </glformgroup-stub>
+
+ <glformgroup-stub
+ label="API Token"
+ label-for="grafana-token"
+ >
+ <glforminput-stub
+ id="grafana-token"
+ value="someToken"
+ />
+
+ <p
+ class="form-text text-muted"
+ >
+
+ Enter the Grafana API Token.
+
+ <a
+ href="https://grafana.com/docs/http_api/auth/#create-api-token"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+
+ More information
+
+ <icon-stub
+ class="vertical-align-middle"
+ name="external-link"
+ size="16"
+ />
+ </a>
+ </p>
+ </glformgroup-stub>
+
+ <glbutton-stub
+ variant="success"
+ >
+
+ Save Changes
+
+ </glbutton-stub>
+ </form>
+ </div>
+</section>
+`;
diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
new file mode 100644
index 00000000000..594ea94dc6a
--- /dev/null
+++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
@@ -0,0 +1,124 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue';
+import { createStore } from '~/grafana_integration/store';
+import axios from '~/lib/utils/axios_utils';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import createFlash from '~/flash';
+import { TEST_HOST } from 'helpers/test_constants';
+
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/flash');
+
+describe('grafana integration component', () => {
+ let wrapper;
+ let store;
+ const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`;
+ const grafanaIntegrationUrl = `${TEST_HOST}`;
+ const grafanaIntegrationToken = 'someToken';
+
+ beforeEach(() => {
+ store = createStore({
+ operationsSettingsEndpoint,
+ grafanaIntegrationUrl,
+ grafanaIntegrationToken,
+ });
+ });
+
+ afterEach(() => {
+ if (wrapper.destroy) {
+ wrapper.destroy();
+ createFlash.mockReset();
+ refreshCurrentPage.mockReset();
+ }
+ });
+
+ describe('default state', () => {
+ it('to match the default snapshot', () => {
+ wrapper = shallowMount(GrafanaIntegration, { store });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('renders header text', () => {
+ wrapper = shallowMount(GrafanaIntegration, { store });
+
+ expect(wrapper.find('.js-section-header').text()).toBe('Grafana Authentication');
+ });
+
+ describe('expand/collapse button', () => {
+ it('renders as an expand button by default', () => {
+ wrapper = shallowMount(GrafanaIntegration, { store });
+
+ const button = wrapper.find(GlButton);
+
+ expect(button.text()).toBe('Expand');
+ });
+ });
+
+ describe('sub-header', () => {
+ it('renders descriptive text', () => {
+ wrapper = shallowMount(GrafanaIntegration, { store });
+
+ expect(wrapper.find('.js-section-sub-header').text()).toContain(
+ 'Embed Grafana charts in GitLab issues.',
+ );
+ });
+ });
+
+ describe('form', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'patch').mockImplementation();
+ });
+
+ afterEach(() => {
+ axios.patch.mockReset();
+ });
+
+ describe('submit button', () => {
+ const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton);
+
+ const endpointRequest = [
+ operationsSettingsEndpoint,
+ {
+ project: {
+ grafana_integration_attributes: {
+ grafana_url: grafanaIntegrationUrl,
+ token: grafanaIntegrationToken,
+ },
+ },
+ },
+ ];
+
+ it('submits form on click', () => {
+ wrapper = mount(GrafanaIntegration, { store });
+ axios.patch.mockResolvedValue();
+
+ findSubmitButton(wrapper).trigger('click');
+
+ expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
+ return wrapper.vm.$nextTick().then(() => expect(refreshCurrentPage).toHaveBeenCalled());
+ });
+
+ it('creates flash banner on error', () => {
+ const message = 'mockErrorMessage';
+ wrapper = mount(GrafanaIntegration, { store });
+ axios.patch.mockRejectedValue({ response: { data: { message } } });
+
+ findSubmitButton().trigger('click');
+
+ expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
+ return wrapper.vm
+ .$nextTick()
+ .then(jest.runAllTicks)
+ .then(() =>
+ expect(createFlash).toHaveBeenCalledWith(
+ `There was an error saving your changes. ${message}`,
+ 'alert',
+ ),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/grafana_integration/store/mutations_spec.js b/spec/frontend/grafana_integration/store/mutations_spec.js
new file mode 100644
index 00000000000..d9b8c258623
--- /dev/null
+++ b/spec/frontend/grafana_integration/store/mutations_spec.js
@@ -0,0 +1,28 @@
+import mutations from '~/grafana_integration/store/mutations';
+import createState from '~/grafana_integration/store/state';
+
+describe('grafana integration mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = createState();
+ });
+
+ describe('SET_GRAFANA_URL', () => {
+ it('sets grafanaUrl', () => {
+ const mockUrl = 'mockUrl';
+ mutations.SET_GRAFANA_URL(localState, mockUrl);
+
+ expect(localState.grafanaUrl).toBe(mockUrl);
+ });
+ });
+
+ describe('SET_GRAFANA_TOKEN', () => {
+ it('sets grafanaToken', () => {
+ const mockToken = 'mockToken';
+ mutations.SET_GRAFANA_TOKEN(localState, mockToken);
+
+ expect(localState.grafanaToken).toBe(mockToken);
+ });
+ });
+});
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js
index ef5c774736b..966ae55ce14 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js
@@ -1,5 +1,7 @@
import AccessorUtilities from '~/lib/utils/accessor';
import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
+import trackData from '~/pages/sessions/new/index';
+import Tracking from '~/tracking';
describe('SigninTabsMemoizer', () => {
const fixtureTemplate = 'static/signin_tabs.html';
@@ -93,6 +95,50 @@ describe('SigninTabsMemoizer', () => {
});
});
+ describe('trackData', () => {
+ beforeEach(() => {
+ spyOn(Tracking, 'event');
+ });
+
+ describe('with tracking data', () => {
+ beforeEach(() => {
+ gon.tracking_data = {
+ category: 'Growth::Acquisition::Experiment::SignUpFlow',
+ action: 'start',
+ label: 'uuid',
+ property: 'control_group',
+ };
+ trackData();
+ });
+
+ it('should track data when the "click" event of the register tab is triggered', () => {
+ document.querySelector('a[href="#register-pane"]').click();
+
+ expect(Tracking.event).toHaveBeenCalledWith(
+ 'Growth::Acquisition::Experiment::SignUpFlow',
+ 'start',
+ {
+ label: 'uuid',
+ property: 'control_group',
+ },
+ );
+ });
+ });
+
+ describe('without tracking data', () => {
+ beforeEach(() => {
+ gon.tracking_data = undefined;
+ trackData();
+ });
+
+ it('should not track data when the "click" event of the register tab is triggered', () => {
+ document.querySelector('a[href="#register-pane"]').click();
+
+ expect(Tracking.event).not.toHaveBeenCalled();
+ });
+ });
+ });
+
describe('saveData', () => {
beforeEach(() => {
memo = {
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index cb6b158f01c..859745ee9fc 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -171,38 +171,7 @@ describe('test errors', () => {
// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
if (process.env.BABEL_ENV === 'coverage') {
// exempt these files from the coverage report
- const troubleMakers = [
- './blob_edit/blob_bundle.js',
- './boards/components/modal/empty_state.vue',
- './boards/components/modal/footer.js',
- './boards/components/modal/header.js',
- './cycle_analytics/cycle_analytics_bundle.js',
- './cycle_analytics/components/stage_plan_component.js',
- './cycle_analytics/components/stage_staging_component.js',
- './cycle_analytics/components/stage_test_component.js',
- './commit/pipelines/pipelines_bundle.js',
- './diff_notes/diff_notes_bundle.js',
- './diff_notes/components/jump_to_discussion.js',
- './diff_notes/components/resolve_count.js',
- './dispatcher.js',
- './environments/environments_bundle.js',
- './graphs/graphs_bundle.js',
- './issuable/time_tracking/time_tracking_bundle.js',
- './main.js',
- './merge_conflicts/merge_conflicts_bundle.js',
- './merge_conflicts/components/inline_conflict_lines.js',
- './merge_conflicts/components/parallel_conflict_lines.js',
- './monitoring/monitoring_bundle.js',
- './network/network_bundle.js',
- './network/branch_graph.js',
- './profile/profile_bundle.js',
- './protected_branches/protected_branches_bundle.js',
- './snippet/snippet_bundle.js',
- './terminal/terminal_bundle.js',
- './users/users_bundle.js',
- './issue_show/index.js',
- './pages/admin/application_settings/general/index.js',
- ];
+ const troubleMakers = ['./pages/admin/application_settings/general/index.js'];
describe('Uncovered files', function() {
const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)];
diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
index 83d6ff754c5..9d18618f638 100644
--- a/spec/models/analytics/cycle_analytics/project_stage_spec.rb
+++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
@@ -16,8 +16,16 @@ describe Analytics::CycleAnalytics::ProjectStage do
end
end
- it_behaves_like "cycle analytics stage" do
+ it_behaves_like 'cycle analytics stage' do
let(:parent) { create(:project) }
let(:parent_name) { :project }
end
+
+ context 'relative positioning' do
+ it_behaves_like 'a class that supports relative positioning' do
+ let(:project) { create(:project) }
+ let(:factory) { :cycle_analytics_project_stage }
+ let(:default_params) { { project: project } }
+ end
+ end
end
diff --git a/spec/views/projects/deployments/_confirm_rollback_modal_spec.html_spec.rb b/spec/views/projects/deployments/_confirm_rollback_modal_spec.html_spec.rb
index 54ec4f32856..9168bc8e833 100644
--- a/spec/views/projects/deployments/_confirm_rollback_modal_spec.html_spec.rb
+++ b/spec/views/projects/deployments/_confirm_rollback_modal_spec.html_spec.rb
@@ -48,7 +48,7 @@ describe 'projects/deployments/_confirm_rollback_modal' do
render
expect(rendered).to have_selector('h4', text: "Rollback environment #{environment.name}?")
- expect(rendered).to have_selector('p', text: "This action will run the job defined by staging for commit #{deployment.short_sha}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?")
+ expect(rendered).to have_selector('p', text: "This action will run the job defined by #{environment.name} for commit #{deployment.short_sha}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?")
expect(rendered).to have_selector('a.btn-danger', text: 'Rollback')
end