Commit 678093c8 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'web-ide-preferred-ci' into 'master'

Make web IDE the preferred editor for .gitlab-ci.yml

See merge request gitlab-org/gitlab!39508
parents a7a15788 52ad6ade
<script>
import { GlAlert, GlButton } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
export default {
components: {
GlAlert,
GlButton,
},
props: {
dismissEndpoint: {
type: String,
required: true,
},
featureId: {
type: String,
required: true,
},
editPath: {
type: String,
required: true,
},
},
data() {
return {
showAlert: true,
};
},
methods: {
dismissAlert() {
this.showAlert = false;
return axios.post(this.dismissEndpoint, {
feature_name: this.featureId,
});
},
},
};
</script>
<template>
<gl-alert v-if="showAlert" class="gl-mt-5" @dismiss="dismissAlert">
{{ __('The Web IDE offers advanced syntax highlighting capabilities and more.') }}
<div class="gl-mt-5">
<gl-button :href="editPath" category="primary" variant="info">{{
__('Open Web IDE')
}}</gl-button>
</div>
</gl-alert>
</template>
import Vue from 'vue';
import WebIdeAlert from './components/web_ide_alert.vue';
export default el => {
const { dismissEndpoint, featureId, editPath } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
render(createElement) {
return createElement(WebIdeAlert, {
props: {
dismissEndpoint,
featureId,
editPath,
},
});
},
});
};
...@@ -7,12 +7,14 @@ import BlobFileDropzone from '../blob/blob_file_dropzone'; ...@@ -7,12 +7,14 @@ import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml'; import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils'; import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import initWebIdeAlert from '~/blob/suggest_web_ide_ci';
export default () => { export default () => {
const editBlobForm = $('.js-edit-blob-form'); const editBlobForm = $('.js-edit-blob-form');
const uploadBlobForm = $('.js-upload-blob-form'); const uploadBlobForm = $('.js-upload-blob-form');
const deleteBlobForm = $('.js-delete-blob-form'); const deleteBlobForm = $('.js-delete-blob-form');
const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml'); const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml');
const alertEl = document.getElementById('js-suggest-web-ide-ci');
if (editBlobForm.length) { if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot'); const urlRoot = editBlobForm.data('relativeUrlRoot');
...@@ -80,4 +82,8 @@ export default () => { ...@@ -80,4 +82,8 @@ export default () => {
}); });
} }
} }
if (alertEl) {
initWebIdeAlert(alertEl);
}
}; };
...@@ -41,7 +41,7 @@ module BlobHelper ...@@ -41,7 +41,7 @@ module BlobHelper
end end
def encode_ide_path(path) def encode_ide_path(path)
url_encode(path).gsub('%2F', '/') ERB::Util.url_encode(path).gsub('%2F', '/')
end end
def edit_blob_button(project = @project, ref = @ref, path = @path, options = {}) def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
...@@ -375,4 +375,9 @@ module BlobHelper ...@@ -375,4 +375,9 @@ module BlobHelper
def human_access def human_access
@project.team.human_max_access(current_user&.id).try(:downcase) @project.team.human_max_access(current_user&.id).try(:downcase)
end end
def editing_ci_config?
@path.to_s.end_with?(Ci::Pipeline::CONFIG_EXTENSION) ||
@path.to_s == @project.ci_config_path_or_default
end
end end
...@@ -9,6 +9,7 @@ module UserCalloutsHelper ...@@ -9,6 +9,7 @@ module UserCalloutsHelper
TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight' TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
WEBHOOKS_MOVED = 'webhooks_moved' WEBHOOKS_MOVED = 'webhooks_moved'
CUSTOMIZE_HOMEPAGE = 'customize_homepage' CUSTOMIZE_HOMEPAGE = 'customize_homepage'
WEB_IDE_ALERT_DISMISSED = 'web_ide_alert_dismissed'
def show_admin_integrations_moved? def show_admin_integrations_moved?
!user_dismissed?(ADMIN_INTEGRATIONS_MOVED) !user_dismissed?(ADMIN_INTEGRATIONS_MOVED)
...@@ -50,6 +51,10 @@ module UserCalloutsHelper ...@@ -50,6 +51,10 @@ module UserCalloutsHelper
customize_homepage && !user_dismissed?(CUSTOMIZE_HOMEPAGE) customize_homepage && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
end end
def show_web_ide_alert?
!user_dismissed?(WEB_IDE_ALERT_DISMISSED)
end
private private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil) def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
......
...@@ -19,6 +19,8 @@ module Ci ...@@ -19,6 +19,8 @@ module Ci
PROJECT_ROUTE_AND_NAMESPACE_ROUTE = { PROJECT_ROUTE_AND_NAMESPACE_ROUTE = {
project: [:project_feature, :route, { namespace: :route }] project: [:project_feature, :route, { namespace: :route }]
}.freeze }.freeze
CONFIG_EXTENSION = '.gitlab-ci.yml'
DEFAULT_CONFIG_PATH = CONFIG_EXTENSION
BridgeStatusError = Class.new(StandardError) BridgeStatusError = Class.new(StandardError)
...@@ -647,7 +649,7 @@ module Ci ...@@ -647,7 +649,7 @@ module Ci
def config_path def config_path
return unless repository_source? || unknown_source? return unless repository_source? || unknown_source?
project.ci_config_path.presence || '.gitlab-ci.yml' project.ci_config_path_or_default
end end
def has_yaml_errors? def has_yaml_errors?
......
...@@ -20,6 +20,7 @@ module Enums ...@@ -20,6 +20,7 @@ module Enums
webhooks_moved: 13, webhooks_moved: 13,
service_templates_deprecated: 14, service_templates_deprecated: 14,
admin_integrations_moved: 15, admin_integrations_moved: 15,
web_ide_alert_dismissed: 16,
personal_access_token_expiry: 21, # EE-only personal_access_token_expiry: 21, # EE-only
suggest_pipeline: 22, suggest_pipeline: 22,
customize_homepage: 23 customize_homepage: 23
......
...@@ -2518,6 +2518,14 @@ class Project < ApplicationRecord ...@@ -2518,6 +2518,14 @@ class Project < ApplicationRecord
.exists? .exists?
end end
def default_branch_or_master
default_branch || 'master'
end
def ci_config_path_or_default
ci_config_path.presence || Ci::Pipeline::DEFAULT_CONFIG_PATH
end
private private
def find_service(services, name) def find_service(services, name)
......
...@@ -7,6 +7,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -7,6 +7,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include StorageHelper include StorageHelper
include TreeHelper include TreeHelper
include IconsHelper include IconsHelper
include BlobHelper
include ChecksCollaboration include ChecksCollaboration
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
...@@ -114,7 +115,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -114,7 +115,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def add_ci_yml_path def add_ci_yml_path
add_special_file_path(file_name: '.gitlab-ci.yml') add_special_file_path(file_name: ci_config_path_or_default)
end
def add_ci_yml_ide_path
ide_edit_path(project, default_branch_or_master, ci_config_path_or_default)
end end
def add_readme_path def add_readme_path
...@@ -219,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -219,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can_current_user_push_to_default_branch? if current_user && can_current_user_push_to_default_branch?
AnchorData.new(false, AnchorData.new(false,
statistic_icon + _('New file'), statistic_icon + _('New file'),
project_new_blob_path(project, default_branch || 'master'), project_new_blob_path(project, default_branch_or_master),
'missing') 'missing')
end end
end end
...@@ -325,7 +330,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -325,7 +330,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if cicd_missing? if cicd_missing?
AnchorData.new(false, AnchorData.new(false,
statistic_icon + _('Set up CI/CD'), statistic_icon + _('Set up CI/CD'),
add_ci_yml_path) add_ci_yml_ide_path)
elsif repository.gitlab_ci_yml.present? elsif repository.gitlab_ci_yml.present?
AnchorData.new(false, AnchorData.new(false,
statistic_icon('doc-text') + _('CI/CD configuration'), statistic_icon('doc-text') + _('CI/CD configuration'),
...@@ -397,7 +402,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -397,7 +402,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name } commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path( project_new_blob_path(
project, project,
project.default_branch || 'master', default_branch_or_master,
file_name: file_name, file_name: file_name,
commit_message: commit_message, commit_message: commit_message,
branch_name: branch_name branch_name: branch_name
......
...@@ -6,6 +6,10 @@ ...@@ -6,6 +6,10 @@
Someone edited the file the same time you did. Please check out Someone edited the file the same time you did. Please check out
= link_to "the file", project_blob_path(@project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer' = link_to "the file", project_blob_path(@project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs. and make sure your changes will not unintentionally remove theirs.
- if editing_ci_config? && show_web_ide_alert?
#js-suggest-web-ide-ci{ data: { dismiss_endpoint: user_callouts_path, feature_id: UserCalloutsHelper::WEB_IDE_ALERT_DISMISSED, edit_path: ide_edit_path } }
.editor-title-row .editor-title-row
%h3.page-title.blob-edit-page-title %h3.page-title.blob-edit-page-title
Edit file Edit file
......
---
title: Add alert when editing .gitlab-ci.yml
merge_request: 39508
author:
type: added
...@@ -17131,6 +17131,9 @@ msgstr "" ...@@ -17131,6 +17131,9 @@ msgstr ""
msgid "Open Selection" msgid "Open Selection"
msgstr "" msgstr ""
msgid "Open Web IDE"
msgstr ""
msgid "Open comment type dropdown" msgid "Open comment type dropdown"
msgstr "" msgstr ""
...@@ -24336,6 +24339,9 @@ msgstr "" ...@@ -24336,6 +24339,9 @@ msgstr ""
msgid "The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., \"http://localhost:9200, http://localhost:9201\")." msgid "The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., \"http://localhost:9200, http://localhost:9201\")."
msgstr "" msgstr ""
msgid "The Web IDE offers advanced syntax highlighting capabilities and more."
msgstr ""
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS." msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr "" msgstr ""
......
...@@ -226,7 +226,7 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do ...@@ -226,7 +226,7 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons' do
expect(project.repository.gitlab_ci_yml).to be_nil expect(project.repository.gitlab_ci_yml).to be_nil
page.within('.project-buttons') do page.within('.project-buttons') do
expect(page).to have_link('Set up CI/CD', href: presenter.add_ci_yml_path) expect(page).to have_link('Set up CI/CD', href: presenter.add_ci_yml_ide_path)
end end
end end
......
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlAlert } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import WebIdeAlert from '~/blob/suggest_web_ide_ci/components/web_ide_alert.vue';
const dismissEndpoint = '/-/user_callouts';
const featureId = 'web_ide_alert_dismissed';
const editPath = 'edit/master/-/.gitlab-ci.yml';
describe('WebIdeAlert', () => {
let wrapper;
let mock;
const findButton = () => wrapper.find(GlButton);
const findAlert = () => wrapper.find(GlAlert);
const dismissAlert = alertWrapper => alertWrapper.vm.$emit('dismiss');
const getPostPayload = () => JSON.parse(mock.history.post[0].data);
const createComponent = () => {
wrapper = shallowMount(WebIdeAlert, {
propsData: {
dismissEndpoint,
featureId,
editPath,
},
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onPost(dismissEndpoint).reply(200);
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mock.restore();
});
describe('with defaults', () => {
it('displays alert correctly', () => {
expect(findAlert().exists()).toBe(true);
});
it('web ide button link has correct path', () => {
expect(findButton().attributes('href')).toBe(editPath);
});
it('dismisses alert correctly', async () => {
const alertWrapper = findAlert();
dismissAlert(alertWrapper);
await waitForPromises();
expect(alertWrapper.exists()).toBe(false);
expect(mock.history.post).toHaveLength(1);
expect(getPostPayload()).toEqual({ feature_name: featureId });
});
});
});
...@@ -481,4 +481,59 @@ RSpec.describe BlobHelper do ...@@ -481,4 +481,59 @@ RSpec.describe BlobHelper do
end end
end end
end end
describe '#editing_ci_config?' do
let(:project) { build(:project) }
subject { helper.editing_ci_config? }
before do
assign(:project, project)
assign(:path, path)
end
context 'when path is nil' do
let(:path) { nil }
it { is_expected.to be_falsey }
end
context 'when path is not a ci file' do
let(:path) { 'some-file.txt' }
it { is_expected.to be_falsey }
end
context 'when path ends is gitlab-ci.yml' do
let(:path) { '.gitlab-ci.yml' }
it { is_expected.to be_truthy }
end
context 'when path ends with gitlab-ci.yml' do
let(:path) { 'template.gitlab-ci.yml' }
it { is_expected.to be_truthy }
end
context 'with custom ci paths' do
let(:path) { 'path/to/ci.yaml' }
before do
project.ci_config_path = 'path/to/ci.yaml'
end
it { is_expected.to be_truthy }
end
context 'with custom ci config and path' do
let(:path) { 'path/to/template.gitlab-ci.yml' }
before do
project.ci_config_path = 'ci/path/.gitlab-ci.yml@another-group/another-project'
end
it { is_expected.to be_truthy }
end
end
end 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