Commit 4c016ad0 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent cb3e6b9c
<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>
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);
},
});
};
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');
};
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;
export const SET_GRAFANA_URL = 'SET_GRAFANA_URL';
export const SET_GRAFANA_TOKEN = 'SET_GRAFANA_TOKEN';
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;
},
};
export default (initialState = {}) => ({
operationsSettingsEndpoint: initialState.operationsSettingsEndpoint,
grafanaToken: initialState.grafanaIntegrationToken || '',
grafanaUrl: initialState.grafanaIntegrationUrl || '',
});
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();
});
......@@ -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();
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
.js-grafana-integration{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
grafana_integration: { url: grafana_integration_url, token: grafana_integration_token } } }
......@@ -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'
---
title: Fix environment name in rollback dialog
merge_request: 19209
author:
type: fixed
......@@ -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:
......
......@@ -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 ""
......
......@@ -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)
......
......@@ -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')
......
......@@ -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
......
......@@ -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
......@@ -5,4 +5,7 @@
<li>
<a href="#login-pane">Standard</a>
</li>
<li>
<a href="#register-pane">Register</a>
</li>
</ul>
// 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>
`;
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',
),
);
});
});
});
});
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);
});
});
});
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 = {
......
......@@ -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)$/)];
......
......@@ -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
......@@ -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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment