Commit 30800334 authored by Simon Knox's avatar Simon Knox

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into 3551-epic-issues

parents 347fec08 f0c5977a
...@@ -20,8 +20,8 @@ import groupsSelect from './groups_select'; ...@@ -20,8 +20,8 @@ import groupsSelect from './groups_select';
import NamespaceSelect from './namespace_select'; import NamespaceSelect from './namespace_select';
/* global NewCommitForm */ /* global NewCommitForm */
/* global NewBranchForm */ /* global NewBranchForm */
/* global Project */ import Project from './project';
/* global ProjectAvatar */ import projectAvatar from './project_avatar';
/* global MergeRequest */ /* global MergeRequest */
/* global Compare */ /* global Compare */
/* global CompareAutocomplete */ /* global CompareAutocomplete */
...@@ -29,7 +29,7 @@ import NamespaceSelect from './namespace_select'; ...@@ -29,7 +29,7 @@ import NamespaceSelect from './namespace_select';
/* global ProjectFindFile */ /* global ProjectFindFile */
/* global ProjectNew */ /* global ProjectNew */
/* global ProjectShow */ /* global ProjectShow */
/* global ProjectImport */ import projectImport from './project_import';
import Labels from './labels'; import Labels from './labels';
import LabelManager from './label_manager'; import LabelManager from './label_manager';
/* global Sidebar */ /* global Sidebar */
...@@ -386,7 +386,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -386,7 +386,7 @@ import initGroupAnalytics from './init_group_analytics';
GpgBadges.fetch(); GpgBadges.fetch();
break; break;
case 'projects:imports:show': case 'projects:imports:show':
new ProjectImport(); projectImport();
break; break;
case 'projects:show': case 'projects:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
...@@ -416,7 +416,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -416,7 +416,7 @@ import initGroupAnalytics from './init_group_analytics';
new UserCallout({ className: 'js-mr-approval-callout' }); new UserCallout({ className: 'js-mr-approval-callout' });
break; break;
case 'projects:imports:show': case 'projects:imports:show':
new ProjectImport(); projectImport();
break; break;
case 'projects:pipelines:new': case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form')); new NewBranchForm($('.js-new-pipeline-form'));
...@@ -686,7 +686,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -686,7 +686,7 @@ import initGroupAnalytics from './init_group_analytics';
break; break;
case 'projects': case 'projects':
new Project(); new Project();
new ProjectAvatar(); projectAvatar();
switch (path[1]) { switch (path[1]) {
case 'compare': case 'compare':
new CompareAutocomplete(); new CompareAutocomplete();
......
...@@ -72,8 +72,6 @@ import './notifications_dropdown'; ...@@ -72,8 +72,6 @@ import './notifications_dropdown';
import './notifications_form'; import './notifications_form';
import './pager'; import './pager';
import './preview_markdown'; import './preview_markdown';
import './project';
import './project_avatar';
import './project_find_file'; import './project_find_file';
import './project_import'; import './project_import';
import './project_label_subscription'; import './project_label_subscription';
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
/* global ProjectSelect */ /* global ProjectSelect */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
(function() { export default class Project {
this.Project = (function() { constructor() {
function Project() { const $cloneOptions = $('ul.clone-options-dropdown');
const $cloneOptions = $('ul.clone-options-dropdown'); const $projectCloneField = $('#project_clone');
const $projectCloneField = $('#project_clone'); const $cloneBtnText = $('a.clone-dropdown-btn span');
const $cloneBtnText = $('a.clone-dropdown-btn span');
const selectedCloneOption = $cloneBtnText.text().trim(); const selectedCloneOption = $cloneBtnText.text().trim();
if (selectedCloneOption.length > 0) { if (selectedCloneOption.length > 0) {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
} }
$('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
e.preventDefault(); $('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
$('.is-active', $cloneOptions).not($this).removeClass('is-active'); e.preventDefault();
$this.toggleClass('is-active');
$projectCloneField.val(url);
$cloneBtnText.text($this.text());
$('#modal-geo-info').data({ $('.is-active', $cloneOptions).not($this).removeClass('is-active');
cloneUrlSecondary: $this.attr('href'), $this.toggleClass('is-active');
cloneUrlPrimary: $this.data('primaryUrl') || '' $projectCloneField.val(url);
}); $cloneBtnText.text($this.text());
return $('.clone').text(url); $('#modal-geo-info').data({
}); cloneUrlSecondary: $this.attr('href'),
// Ref switcher cloneUrlPrimary: $this.data('primaryUrl') || '',
this.initRefSwitcher();
$('.project-refs-select').on('change', function() {
return $(this).parents('form').submit();
});
$('.hide-no-ssh-message').on('click', function(e) {
Cookies.set('hide_no_ssh_message', 'false');
$(this).parents('.no-ssh-key-message').remove();
return e.preventDefault();
});
$('.hide-no-password-message').on('click', function(e) {
Cookies.set('hide_no_password_message', 'false');
$(this).parents('.no-password-message').remove();
return e.preventDefault();
});
$('.hide-shared-runner-limit-message').on('click', function(e) {
var $alert = $(this).parents('.shared-runner-quota-message');
var scope = $alert.data('scope');
Cookies.set('hide_shared_runner_quota_message', 'false', { path: scope });
$alert.remove();
e.preventDefault();
}); });
this.projectSelectDropdown();
}
Project.prototype.projectSelectDropdown = function() { return $('.clone').text(url);
new ProjectSelect(); });
$('.project-item-select').on('click', (function(_this) { // Ref switcher
return function(e) { Project.initRefSwitcher();
return _this.changeProject($(e.currentTarget).val()); $('.project-refs-select').on('change', function() {
}; return $(this).parents('form').submit();
})(this)); });
}; $('.hide-no-ssh-message').on('click', function(e) {
Cookies.set('hide_no_ssh_message', 'false');
Project.prototype.changeProject = function(url) { $(this).parents('.no-ssh-key-message').remove();
return window.location = url; return e.preventDefault();
}; });
$('.hide-no-password-message').on('click', function(e) {
Project.prototype.initRefSwitcher = function() { Cookies.set('hide_no_password_message', 'false');
var refListItem = document.createElement('li'); $(this).parents('.no-password-message').remove();
var refLink = document.createElement('a'); return e.preventDefault();
});
refLink.href = '#'; $('.hide-shared-runner-limit-message').on('click', function(e) {
var $alert = $(this).parents('.shared-runner-quota-message');
return $('.js-project-refs-dropdown').each(function() { var scope = $alert.data('scope');
var $dropdown, selected; Cookies.set('hide_shared_runner_quota_message', 'false', { path: scope });
$dropdown = $(this); $alert.remove();
selected = $dropdown.data('selected'); e.preventDefault();
return $dropdown.glDropdown({ });
data: function(term, callback) { Project.projectSelectDropdown();
return $.ajax({ }
url: $dropdown.data('refs-url'),
data: { static projectSelectDropdown() {
ref: $dropdown.data('ref'), new ProjectSelect();
search: term $('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
}, }
dataType: "json"
}).done(function(refs) { static changeProject(url) {
return callback(refs); return window.location = url;
}); }
},
selectable: true, static initRefSwitcher() {
filterable: true, var refListItem = document.createElement('li');
filterRemote: true, var refLink = document.createElement('a');
filterByText: true,
inputFieldName: $dropdown.data('input-field-name'), refLink.href = '#';
fieldName: $dropdown.data('field-name'),
renderRow: function(ref) { return $('.js-project-refs-dropdown').each(function() {
var li = refListItem.cloneNode(false); var $dropdown, selected;
$dropdown = $(this);
if (ref.header != null) { selected = $dropdown.data('selected');
li.className = 'dropdown-header'; return $dropdown.glDropdown({
li.textContent = ref.header; data: function(term, callback) {
} else { return $.ajax({
var link = refLink.cloneNode(false); url: $dropdown.data('refs-url'),
data: {
if (ref === selected) { ref: $dropdown.data('ref'),
link.className = 'is-active'; search: term,
} },
dataType: 'json',
link.textContent = ref; }).done(function(refs) {
link.dataset.ref = ref; return callback(refs);
});
li.appendChild(link); },
selectable: true,
filterable: true,
filterRemote: true,
filterByText: true,
inputFieldName: $dropdown.data('input-field-name'),
fieldName: $dropdown.data('field-name'),
renderRow: function(ref) {
var li = refListItem.cloneNode(false);
if (ref.header != null) {
li.className = 'dropdown-header';
li.textContent = ref.header;
} else {
var link = refLink.cloneNode(false);
if (ref === selected) {
link.className = 'is-active';
} }
return li; link.textContent = ref;
}, link.dataset.ref = ref;
id: function(obj, $el) {
return $el.attr('data-ref'); li.appendChild(link);
}, }
toggleLabel: function(obj, $el) {
return $el.text().trim(); return li;
}, },
clicked: function(options) { id: function(obj, $el) {
const { e } = options; return $el.attr('data-ref');
e.preventDefault(); },
if ($('input[name="ref"]').length) { toggleLabel: function(obj, $el) {
var $form = $dropdown.closest('form'); return $el.text().trim();
},
var $visit = $dropdown.data('visit'); clicked: function(options) {
var shouldVisit = $visit ? true : $visit; const { e } = options;
var action = $form.attr('action'); e.preventDefault();
var divider = action.indexOf('?') === -1 ? '?' : '&'; if ($('input[name="ref"]').length) {
if (shouldVisit) { var $form = $dropdown.closest('form');
gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
} var $visit = $dropdown.data('visit');
var shouldVisit = $visit ? true : $visit;
var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&';
if (shouldVisit) {
gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
} }
} }
}); },
}); });
}; });
}
return Project; }
})();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ export default function projectAvatar() {
(function() { $('.js-choose-project-avatar-button').bind('click', function onClickAvatar() {
this.ProjectAvatar = (function() { const form = $(this).closest('form');
function ProjectAvatar() { return form.find('.js-project-avatar-input').click();
$('.js-choose-project-avatar-button').bind('click', function() { });
var form;
form = $(this).closest('form');
return form.find('.js-project-avatar-input').click();
});
$('.js-project-avatar-input').bind('change', function() {
var filename, form;
form = $(this).closest('form');
filename = $(this).val().replace(/^.*[\\\/]/, '');
return form.find('.js-avatar-filename').text(filename);
});
}
return ProjectAvatar; $('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
})(); const form = $(this).closest('form');
}).call(window); // eslint-disable-next-line no-useless-escape
const filename = $(this).val().replace(/^.*[\\\/]/, '');
return form.find('.js-avatar-filename').text(filename);
});
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ import { visitUrl } from './lib/utils/url_utility';
(function() { export default function projectImport() {
this.ProjectImport = (function() { setTimeout(() => {
function ProjectImport() { visitUrl(location.href);
setTimeout(function() { }, 5000);
return gl.utils.visitUrl(location.href); }
}, 5000);
}
return ProjectImport;
})();
}).call(window);
...@@ -7,29 +7,68 @@ ...@@ -7,29 +7,68 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
padding-bottom: 25px; padding-bottom: 25px;
border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
} }
} }
.blank-state { .blank-state-row {
padding-top: 20px; display: flex;
padding-bottom: 20px; flex-wrap: wrap;
justify-content: space-around;
height: 100%;
}
.blank-state-welcome {
text-align: center; text-align: center;
padding: 20px 0 40px;
.blank-state-welcome-title {
font-size: 24px;
}
.blank-state-text {
margin-bottom: 0;
}
}
.blank-state-link {
display: block;
color: $gl-text-color;
flex: 0 0 100%;
margin-bottom: 15px;
&.blank-state-welcome { @media (min-width: $screen-sm-min) {
.blank-state-welcome-title { flex: 0 0 49%;
font-size: 24px;
&:nth-child(odd) {
margin-right: 5px;
} }
.blank-state-text { &:nth-child(even) {
margin-bottom: 0; margin-left: 5px;
} }
} }
.blank-state-icon { &:hover {
padding-bottom: 20px; background-color: $gray-light;
text-decoration: none;
color: $gl-text-color;
}
}
.blank-state {
padding: 20px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
@media (min-width: $screen-sm-min) {
display: flex;
height: 100%;
align-items: center;
padding: 50px 30px;
}
.blank-state-icon {
svg { svg {
display: block; display: block;
margin: auto; margin: auto;
...@@ -38,33 +77,46 @@ ...@@ -38,33 +77,46 @@
.blank-state-title { .blank-state-title {
margin-top: 0; margin-top: 0;
margin-bottom: 10px;
font-size: 18px; font-size: 18px;
} }
.blank-state-text { .blank-state-body {
max-width: $container-text-max-width; @media (max-width: $screen-xs-max) {
margin: 0 auto $gl-padding; text-align: center;
font-size: 14px; margin-top: 20px;
}
@media (min-width: $screen-sm-min) {
padding-left: 20px;
}
} }
} }
/* EE-specific Styles */ /* EE-specific Styles */
@media (min-width: $screen-md-min) {
.blank-state-parent-container.has-start-trial-container { @media (min-width: $screen-lg-min) {
display: flex; .column-large {
flex: 2;
} }
}
.section-ee-trial { .column-small {
.section-body { flex: 1;
display: flex; margin-bottom: 15px;
align-items: center;
justify-content: center;
.blank-state { .blank-state {
padding: 20px; max-width: 400px;
text-align: center; flex-wrap: wrap;
margin-left: 15px;
} }
.blank-state-icon {
margin-bottom: 30px;
}
}
}
@media (max-width: $screen-xs-max) {
.blank-state-icon svg {
width: 315px;
} }
} }
...@@ -9,10 +9,7 @@ module IssuableActions ...@@ -9,10 +9,7 @@ module IssuableActions
def show def show
respond_to do |format| respond_to do |format|
format.html do format.html
render show_view
end
format.json do format.json do
render json: serializer.represent(issuable, serializer: params[:serializer]) render json: serializer.represent(issuable, serializer: params[:serializer])
end end
...@@ -154,10 +151,6 @@ module IssuableActions ...@@ -154,10 +151,6 @@ module IssuableActions
end end
end end
def show_view
'show'
end
def serializer def serializer
raise NotImplementedError raise NotImplementedError
end end
......
...@@ -10,6 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -10,6 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :check_issues_available! before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv] before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv]
before_action :set_issuables_index, only: [:index] before_action :set_issuables_index, only: [:index]
# Allow write(create) issue # Allow write(create) issue
......
...@@ -159,7 +159,7 @@ module IssuablesHelper ...@@ -159,7 +159,7 @@ module IssuablesHelper
label_names.join(', ') label_names.join(', ')
end end
def issuables_state_counter_text(issuable_type, state) def issuables_state_counter_text(issuable_type, state = :all)
titles = { titles = {
opened: "Open" opened: "Open"
} }
......
...@@ -3,6 +3,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -3,6 +3,7 @@ class MergeRequest < ActiveRecord::Base
include Issuable include Issuable
include Noteable include Noteable
include Referable include Referable
include Elastic::MergeRequestsSearch
include IgnorableColumn include IgnorableColumn
include TimeTrackable include TimeTrackable
......
...@@ -14,7 +14,7 @@ class JenkinsDeprecatedService < CiService ...@@ -14,7 +14,7 @@ class JenkinsDeprecatedService < CiService
def compose_service_hook def compose_service_hook
hook = service_hook || build_service_hook hook = service_hook || build_service_hook
jenkins_url = project_url.sub(/job\/.*/, '') jenkins_url = project_url.sub(/job\/.*/, '')
hook.url = jenkins_url + "/gitlab/build_now" hook.url = jenkins_url + "gitlab/build_now"
hook.save hook.save
end end
...@@ -101,7 +101,13 @@ class JenkinsDeprecatedService < CiService ...@@ -101,7 +101,13 @@ class JenkinsDeprecatedService < CiService
if response.code == 200 if response.code == 200
# img.build-caption-status-icon for old jenkins version # img.build-caption-status-icon for old jenkins version
src = Nokogiri.parse(response).css('img.build-caption-status-icon,.build-caption>img').first.attributes['src'].value begin
src = Nokogiri.parse(response).css('img.build-caption-status-icon,.build-caption>img').first.attributes['src'].value
rescue NoMethodError => ex
Raven.capture_exception(ex, extra: { 'response' => response })
return :error
end
if src =~ /blue\.png$/ || (src =~ /yellow\.png/ && pass_unstable?) if src =~ /blue\.png$/ || (src =~ /yellow\.png/ && pass_unstable?)
'success' 'success'
elsif src =~ /(red|aborted|yellow)\.png$/ elsif src =~ /(red|aborted|yellow)\.png$/
......
.blank-state .blank-state-row
.blank-state-icon = link_to new_project_path, class: "blank-state-link" do
= custom_icon("add_new_user", size: 50) .blank-state
.blank-state-body .blank-state-icon
%h3.blank-state-title = custom_icon("add_new_project", size: 50)
Add user .blank-state-body
%p.blank-state-text %h3.blank-state-title
Add your team members and others to GitLab. Create a project
= link_to new_admin_user_path, class: "btn btn-new" do %p.blank-state-text
New user Projects are where you store your code, access issues, wiki and other features of GitLab.
.blank-state - if current_user.can_create_group?
.blank-state-icon = link_to admin_root_path, class: "blank-state-link" do
= custom_icon("configure_server", size: 50) .blank-state
.blank-state-body .blank-state-icon
%h3.blank-state-title = custom_icon("add_new_group", size: 50)
Configure GitLab .blank-state-body
%p.blank-state-text %h3.blank-state-title
Make adjustments to how your GitLab instance is set up. Create a group
= link_to admin_root_path, class: "btn btn-new" do %p.blank-state-text
Configure Groups are a great way to organize projects and people.
- if current_user.can_create_group? = link_to new_admin_user_path, class: "blank-state-link" do
.blank-state .blank-state
.blank-state-icon .blank-state-icon
= custom_icon("add_new_group", size: 50) = custom_icon("add_new_user", size: 50)
.blank-state-body .blank-state-body
%h3.blank-state-title %h3.blank-state-title
Create a group Add people
%p.blank-state-text %p.blank-state-text
Groups are a great way to organize projects and people. Add your team members and others to GitLab.
= link_to new_group_path, class: "btn btn-new" do
New group = link_to admin_root_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("configure_server", size: 50)
.blank-state-body
%h3.blank-state-title
Configure GitLab
%p.blank-state-text
Make adjustments to how your GitLab instance is set up.
- public_project_count = ProjectsFinder.new(current_user: current_user).execute.count - public_project_count = ProjectsFinder.new(current_user: current_user).execute.count
- if current_user.can_create_group? .blank-state-row
.blank-state - if current_user.can_create_project?
.blank-state-icon = link_to new_project_path, class: "blank-state-link" do
= custom_icon("add_new_group", size: 50) .blank-state
.blank-state-body .blank-state-icon
%h3.blank-state-title = custom_icon("add_new_project", size: 50)
Create a group for several dependent projects. .blank-state-body
%p.blank-state-text %h3.blank-state-title
Groups are the best way to manage projects and members. Create a project
= link_to new_group_path, class: "btn btn-new" do %p.blank-state-text
New group Projects are where you store your code, access issues, wiki and other features of GitLab.
- else
.blank-state
.blank-state-icon
= custom_icon("add_new_project", size: 50)
.blank-state-body
%h3.blank-state-title
Create a project
%p.blank-state-text
If you are added to a project, it will be displayed here.
.blank-state - if current_user.can_create_group?
.blank-state-icon = link_to new_group_path, class: "blank-state-link" do
= custom_icon("add_new_project", size: 50) .blank-state
.blank-state-body .blank-state-icon
%h3.blank-state-title = custom_icon("add_new_group", size: 50)
Create a project .blank-state-body
%p.blank-state-text %h3.blank-state-title
- if current_user.can_create_project? Create a group
You don't have access to any projects right now. %p.blank-state-text
You can create up to Groups are the best way to manage projects and members.
%strong= number_with_delimiter(current_user.projects_limit)
= succeed "." do
= "project".pluralize(current_user.projects_limit)
- else
If you are added to a project, it will be displayed here.
- if current_user.can_create_project?
= link_to new_project_path, class: "btn btn-new" do
New project
- if public_project_count > 0 - if public_project_count > 0
.blank-state = link_to trending_explore_projects_path, class: "blank-state-link" do
.blank-state-icon .blank-state
= custom_icon("globe", size: 50) .blank-state-icon
.blank-state-body = custom_icon("globe", size: 50)
%h3.blank-state-title .blank-state-body
Explore public projects %h3.blank-state-title
%p.blank-state-text Explore public projects
There are %p.blank-state-text
= number_with_delimiter(public_project_count) There are
public projects on this server. = number_with_delimiter(public_project_count)
Public projects are an easy way to allow public projects on this server.
everyone to have read-only access. Public projects are an easy way to allow
= link_to trending_explore_projects_path, class: "btn btn-new" do everyone to have read-only access.
Browse projects
= link_to "https://docs.gitlab.com/", class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("lightbulb", size: 50)
.blank-state-body
%h3.blank-state-title
Learn more about GitLab
%p.blank-state-text
Take a look at the documentation to discover all of GitLab's capabilities.
- admin_without_ee_license = !current_license && current_user.admin? - admin_without_ee_license = !current_license && current_user.admin?
.row.blank-state-parent-container{ class: ('has-start-trial-container' if admin_without_ee_license) } .blank-state-parent-container{ class: ('has-start-trial-container' if admin_without_ee_license) }
.section-container.section-welcome{ class: ('col-md-6' if admin_without_ee_license) } .section-container.section-welcome{ class: ('col-md-6' if admin_without_ee_license) }
.container.section-body .container.section-body
.blank-state.blank-state-welcome .row
%h2.blank-state-welcome-title .blank-state-welcome
Welcome to GitLab %h2.blank-state-welcome-title
%p.blank-state-text Welcome to GitLab
Code, test, and deploy together %p.blank-state-text
- if current_user.admin? Code, test, and deploy together
= render "blank_state_admin_welcome" .blank-state-row
- else %div{ class: ('column-large' if admin_without_ee_license) }
= render "blank_state_welcome" - if current_user.admin?
- if admin_without_ee_license = render "blank_state_admin_welcome"
.col-md-6.section-container.section-ee-trial - else
.container.section-body = render "blank_state_welcome"
= render "blank_state_ee_trial" - if admin_without_ee_license
.column-small
= render "blank_state_ee_trial"
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
- epics = EpicsFinder.new(current_user, group_id: @group.id).execute
- epics_items = ['epics#show', 'epics#index']
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index'] - issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
- if @group.feature_available?(:group_issue_boards) - if @group.feature_available?(:group_issue_boards)
- issues_sub_menu_items.push('boards#index', 'boards#show') - issues_sub_menu_items.push('boards#index', 'boards#show')
...@@ -43,6 +45,21 @@ ...@@ -43,6 +45,21 @@
%span %span
Contribution Analytics Contribution Analytics
-# TODO: Add the flag check to only show epics if available
= nav_link(path: epics_items) do
= link_to group_epics_path(@group) do
.nav-icon-container
= sprite_icon('epic')
%span.nav-item-name
Epics
%span.badge.count= number_with_delimiter(epics.count)
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: epics_items, html_options: { class: "fly-out-top-item" } ) do
= link_to group_epics_path(@group) do
%strong.fly-out-top-item-name
#{ _('Epics') }
%span.badge.count.epic_counter.fly-out-badge= number_with_delimiter(epics.count)
= nav_link(path: issues_sub_menu_items) do = nav_link(path: issues_sub_menu_items) do
= link_to issues_group_path(@group) do = link_to issues_group_path(@group) do
.nav-icon-container .nav-icon-container
......
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76 19.575 76 3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#FEE1D3" fill-rule="nonzero" d="M30 24a4 4 0 0 0-4 4v22a4 4 0 0 0 4 4h18a4 4 0 0 0 4-4V28a4 4 0 0 0-4-4H30zm0-4h18a8 8 0 0 1 8 8v22a8 8 0 0 1-8 8H30a8 8 0 0 1-8-8V28a8 8 0 0 1 8-8z"/><path fill="#FC6D26" d="M33 30h8a2 2 0 1 1 0 4h-8a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4zm0 7h12a2 2 0 1 1 0 4H33a2 2 0 1 1 0-4z"/></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M30 24c-2.21 0-4 1.79-4 4v22c0 2.21 1.79 4 4 4h18c2.21 0 4-1.79 4-4V28c0-2.21-1.79-4-4-4H30zm0-4h18c4.418 0 8 3.582 8 8v22c0 4.418-3.582 8-8 8H30c-4.418 0-8-3.582-8-8V28c0-4.418 3.582-8 8-8z"/><path fill="#6B4FBB" d="M33 30h8c1.105 0 2 .895 2 2s-.895 2-2 2h-8c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm0 7h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2z"/></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="78" height="82" viewBox="0 0 78 82"><g fill="none" fill-rule="evenodd"><path fill="#F9F9F9" d="M2.12 42c-.08.99-.12 1.99-.12 3 0 20.435 16.565 37 37 37s37-16.565 37-37c0-1.01-.04-2.01-.12-3C74.353 61.032 58.425 76 39 76S3.647 61.032 2.12 42z"/><path fill="#EEE" fill-rule="nonzero" d="M39 78C17.46 78 0 60.54 0 39S17.46 0 39 0s39 17.46 39 39-17.46 39-39 39zm0-4c19.33 0 35-15.67 35-35S58.33 4 39 4 4 19.67 4 39s15.67 35 35 35z"/><path fill="#6B4FBB" d="M33 52h12c1.105 0 2 .895 2 2s-.895 2-2 2H33c-1.105 0-2-.895-2-2s.895-2 2-2zm1 5h10c1.105 0 2 .895 2 2s-.895 2-2 2H34c-1.105 0-2-.895-2-2s.895-2 2-2z"/><path fill="#E1DBF2" fill-rule="nonzero" d="M45.542 46.932l.346-2.36c.198-1.348.737-2.623 1.566-3.705 3.025-3.946 4.485-7.29 4.547-9.96C52.153 24.41 46.843 20 39 20c-7.777 0-13 4.374-13 11 0 2.4 1.462 5.73 4.573 9.846.815 1.08 1.343 2.345 1.536 3.683l.353 2.456 13.08-.054zm-17.038.624L28.15 45.1c-.097-.67-.36-1.303-.768-1.842C23.794 38.51 22 34.424 22 31c0-9.39 7.61-15 17-15s17.218 5.614 17 15c-.085 3.64-1.875 7.74-5.37 12.3-.416.54-.685 1.18-.784 1.853l-.346 2.36c-.288 1.958-1.963 3.41-3.942 3.42l-13.08.053c-1.994.008-3.69-1.455-3.974-3.43z"/><path fill="#6B4FBB" d="M41 38.732c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268zm-6 0c-.598-.345-1-.992-1-1.732 0-1.105.895-2 2-2s2 .895 2 2c0 .74-.402 1.387-1 1.732V42c0 .552-.448 1-1 1s-1-.448-1-1v-3.268z"/></g></svg>
- type = local_assigns.fetch(:type, :issues) - type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false) - page_context_word = type.to_s.humanize(capitalize: false)
- issuables = @issues || @merge_requests - issuables = @issues || @merge_requests || @epics
%ul.nav-links.issues-state-filters %ul.nav-links.issues-state-filters
%li{ class: active_when(params[:state] == 'opened') }> - if type != :epics
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do %li{ class: active_when(params[:state] == 'opened') }>
#{issuables_state_counter_text(type, :opened)} = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened)}
- if type == :merge_requests - if type == :merge_requests
%li{ class: active_when(params[:state] == 'merged') }> %li{ class: active_when(params[:state] == 'merged') }>
= link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do
#{issuables_state_counter_text(type, :merged)} #{issuables_state_counter_text(type, :merged)}
%li{ class: active_when(params[:state] == 'closed') }> %li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed)} #{issuables_state_counter_text(type, :closed)}
- else - else
%li{ class: active_when(params[:state] == 'closed') }> %li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed)} #{issuables_state_counter_text(type, :closed)}
%li{ class: active_when(params[:state] == 'all') }> %li{ class: active_when(params[:state] == 'all') }>
= link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
......
---
title: Add epics list and add epics to nav sidebar
merge_request:
author:
type: added
---
title: Reorganize welcome page for new users
merge_request:
author:
type: other
...@@ -16,6 +16,20 @@ all you need to do is update GitLab itself: ...@@ -16,6 +16,20 @@ all you need to do is update GitLab itself:
## Upgrading to GitLab 10.2 ## Upgrading to GitLab 10.2
### Secure PostgreSQL replication
Support for TLS-secured PostgreSQL replication has been added. If you are
currently using PostgreSQL replication across the open internet without an
external means of securing the connection (e.g., a site-to-site VPN), then you
should immediately reconfigure your primary and secondary PostgreSQL instances
according to the [updated instructions](#database.md).
If you *are* securing the connections externally and wish to continue doing so,
ensure you include the new option `--sslmode=prefer` in future invocations of
`gitlab-ctl replicate-geo-database`.
### HTTPS repository sync
Support for replicating repositories and wikis over HTTP/HTTPS has been added. Support for replicating repositories and wikis over HTTP/HTTPS has been added.
Replicating over SSH has been deprecated, and support for this option will be Replicating over SSH has been deprecated, and support for this option will be
removed in a future release. removed in a future release.
......
...@@ -12,7 +12,6 @@ module EE ...@@ -12,7 +12,6 @@ module EE
def service_desk def service_desk
@issues = @issuables @issues = @issuables
@users.push(::User.support_bot) @users.push(::User.support_bot)
end end
......
class Groups::EpicsController < Groups::ApplicationController class Groups::EpicsController < Groups::ApplicationController
include IssuableActions include IssuableActions
include IssuableCollections
before_action :epic before_action :epic, except: :index
before_action :set_issuables_index, only: :index
before_action :authorize_update_issuable!, only: :update before_action :authorize_update_issuable!, only: :update
skip_before_action :labels skip_before_action :labels
def index
set_default_state
@epics = @issuables
respond_to do |format|
format.html
format.json do
render json: {
html: view_to_html_string("groups/epics/_epics")
}
end
end
end
private private
def epic def epic
...@@ -38,7 +54,22 @@ class Groups::EpicsController < Groups::ApplicationController ...@@ -38,7 +54,22 @@ class Groups::EpicsController < Groups::ApplicationController
Epics::UpdateService.new(nil, current_user, epic_params) Epics::UpdateService.new(nil, current_user, epic_params)
end end
def show_view def set_issuables_index
'groups/ee/epics/show' @finder_type = EpicsFinder
super
end
def collection_type
@collection_type ||= 'Epic'
end
def preload_for_collection
@preload_for_collection ||= [:group, :author]
end
# we need to override the default state which is opened for now because we don't have
# states for epics and need all as default for navigation to work correctly (#4017)
def set_default_state
params[:state] = 'all'
end end
end end
class EpicsFinder < IssuableFinder
def klass
Epic
end
def execute
raise ArgumentError, 'group_id argument is missing' unless group
items = init_collection
items = by_created_at(items)
items = by_search(items)
items = by_author(items)
items = by_iids(items)
sort(items)
end
def row_count
execute.count
end
# we don't have states for epics for now this method (#4017)
def count_by_state
{
all: row_count
}
end
def group
return nil unless params[:group_id]
return @group if defined?(@group)
group = Group.find(params[:group_id])
group = nil unless Ability.allowed?(current_user, :read_epic, group)
@group = group
end
def init_collection
group.epics
end
end
...@@ -30,7 +30,12 @@ module EE ...@@ -30,7 +30,12 @@ module EE
enable :destroy_epic enable :destroy_epic
end end
rule { auditor }.enable :read_group rule { auditor }.policy do
enable :read_group
enable :read_epic
end
rule { admin }.enable :read_epic
rule { has_projects }.enable :read_epic
rule { admin | (can_owners_manage_ldap & owner) }.enable :admin_ldap_group_links rule { admin | (can_owners_manage_ldap & owner) }.enable :admin_ldap_group_links
......
%li
.issue-box
.issue-info-container
.issue-main-info
.issue-title.title
%span.issue-title-text
= link_to epic.title, epic_path(epic)
.issuable-info
%span.issuable-reference
-# TODO: Use to_reference
= "&#{epic.iid}"
%span.issuable-authored.hidden-xs
&middot;
opened #{time_ago_with_tooltip(epic.created_at, placement: 'bottom')}
by #{link_to_member(@group, epic.author, avatar: false)}
- page_title "Epics"
- if @epics.to_a.any?
= render 'shared/epics'
- else
= render 'shared/empty_states/epics'
.top-area
= render 'shared/issuable/nav', type: :epics
%ul.content-list.issuable-list
= render partial: 'groups/epics/epic', collection: @epics
= paginate @epics, theme: "gitlab"
.row.empty-state
.col-xs-12
.svg-content
= image_tag('illustrations/epics.svg')
.col-xs-12.text-center
.text-content
%h4
= _('Epics let you manage your portfolio of projects more efficiently and with less effort')
%p
= _('Track groups of issues that share a theme, across projects and milestones')
%button.btn.btn-new{ type: 'button' }
New epic
...@@ -62,6 +62,7 @@ module QA ...@@ -62,6 +62,7 @@ module QA
module Main module Main
autoload :Entry, 'qa/page/main/entry' autoload :Entry, 'qa/page/main/entry'
autoload :Login, 'qa/page/main/login'
autoload :Menu, 'qa/page/main/menu' autoload :Menu, 'qa/page/main/menu'
end end
......
...@@ -4,7 +4,8 @@ module QA ...@@ -4,7 +4,8 @@ module QA
module License module License
class Add < QA::Scenario::Template class Add < QA::Scenario::Template
def perform(license) def perform(license)
QA::Page::Main::Entry.act { sign_in_using_credentials } QA::Page::Main::Entry.act { visit_login_page }
QA::Page::Main::Login.act { sign_in_using_credentials }
QA::Page::Main::Menu.act { go_to_admin_area } QA::Page::Main::Menu.act { go_to_admin_area }
QA::Page::Admin::Menu.act { go_to_license } QA::Page::Admin::Menu.act { go_to_license }
......
...@@ -23,7 +23,7 @@ module QA ...@@ -23,7 +23,7 @@ module QA
def password=(pass) def password=(pass)
@password = pass @password = pass
@uri.password = pass @uri.password = CGI.escape(pass)
end end
def use_default_credentials def use_default_credentials
......
...@@ -2,9 +2,14 @@ module QA ...@@ -2,9 +2,14 @@ module QA
module Page module Page
module Main module Main
class Entry < Page::Base class Entry < Page::Base
def initialize def visit_login_page
visit(Runtime::Scenario.gitlab_address) visit("#{Runtime::Scenario.gitlab_address}/users/sign_in")
wait_for_instance_to_be_ready
end
private
def wait_for_instance_to_be_ready
# This resolves cold boot / background tasks problems # This resolves cold boot / background tasks problems
# #
start = Time.now start = Time.now
...@@ -14,18 +19,6 @@ module QA ...@@ -14,18 +19,6 @@ module QA
refresh refresh
end end
end end
def sign_in_using_credentials
if page.has_content?('Change your password')
fill_in :user_password, with: Runtime::User.password
fill_in :user_password_confirmation, with: Runtime::User.password
click_button 'Change your password'
end
fill_in :user_login, with: Runtime::User.name
fill_in :user_password, with: Runtime::User.password
click_button 'Sign in'
end
end end
end end
end end
......
module QA
module Page
module Main
class Login < Page::Base
def sign_in_using_credentials
if page.has_content?('Change your password')
fill_in :user_password, with: Runtime::User.password
fill_in :user_password_confirmation, with: Runtime::User.password
click_button 'Change your password'
end
fill_in :user_login, with: Runtime::User.name
fill_in :user_password, with: Runtime::User.password
click_button 'Sign in'
end
end
end
end
end
module QA module QA
feature 'standard root login', :core do feature 'standard root login', :core do
scenario 'user logs in using credentials' do scenario 'user logs in using credentials' do
Page::Main::Entry.act { sign_in_using_credentials } Page::Main::Entry.act { visit_login_page }
Page::Main::Login.act { sign_in_using_credentials }
# TODO, since `Signed in successfully` message was removed # TODO, since `Signed in successfully` message was removed
# this is the only way to tell if user is signed in correctly. # this is the only way to tell if user is signed in correctly.
......
module QA module QA
feature 'create a new group', :mattermost do feature 'create a new group', :mattermost do
scenario 'creating a group with a mattermost team' do scenario 'creating a group with a mattermost team' do
Page::Main::Entry.act { sign_in_using_credentials } Page::Main::Entry.act { visit_login_page }
Page::Main::Login.act { sign_in_using_credentials }
Page::Main::Menu.act { go_to_groups } Page::Main::Menu.act { go_to_groups }
Page::Dashboard::Groups.perform do |page| Page::Dashboard::Groups.perform do |page|
......
module QA module QA
feature 'logging in to Mattermost', :mattermost do feature 'logging in to Mattermost', :mattermost do
scenario 'can use gitlab oauth' do scenario 'can use gitlab oauth' do
Page::Main::Entry.act { sign_in_using_credentials } Page::Main::Entry.act { visit_login_page }
Page::Main::Login.act { sign_in_using_credentials }
Page::Mattermost::Login.act { sign_in_using_oauth } Page::Mattermost::Login.act { sign_in_using_oauth }
Page::Mattermost::Main.perform do |page| Page::Mattermost::Main.perform do |page|
......
module QA module QA
feature 'create a new project', :core do feature 'create a new project', :core do
scenario 'user creates a new project' do scenario 'user creates a new project' do
Page::Main::Entry.act { sign_in_using_credentials } Page::Main::Entry.act { visit_login_page }
Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |project| Scenario::Gitlab::Project::Create.perform do |project|
project.name = 'awesome-project' project.name = 'awesome-project'
......
...@@ -9,7 +9,8 @@ module QA ...@@ -9,7 +9,8 @@ module QA
end end
before do before do
Page::Main::Entry.act { sign_in_using_credentials } Page::Main::Entry.act { visit_login_page }
Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario| Scenario::Gitlab::Project::Create.perform do |scenario|
scenario.name = 'project-with-code' scenario.name = 'project-with-code'
......
...@@ -2,7 +2,8 @@ module QA ...@@ -2,7 +2,8 @@ module QA
feature 'push code to repository', :core do feature 'push code to repository', :core do
context 'with regular account over http' do context 'with regular account over http' do
scenario 'user pushes code to the repository' do scenario 'user pushes code to the repository' do
Page::Main::Entry.act { sign_in_using_credentials } Page::Main::Entry.act { visit_login_page }
Page::Main::Login.act { sign_in_using_credentials }
Scenario::Gitlab::Project::Create.perform do |scenario| Scenario::Gitlab::Project::Create.perform do |scenario|
scenario.name = 'project_with_code' scenario.name = 'project_with_code'
......
...@@ -9,6 +9,42 @@ describe Groups::EpicsController do ...@@ -9,6 +9,42 @@ describe Groups::EpicsController do
sign_in(user) sign_in(user)
end end
describe "GET #index" do
let!(:epic_list) { create_list(:epic, 2, group: group) }
before do
sign_in(user)
group.add_developer(user)
end
it "returns index" do
get :index, group_id: group
expect(response).to have_gitlab_http_status(200)
end
context 'with page param' do
let(:last_page) { group.epics.page.total_pages }
before do
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
end
it 'redirects to last_page if page number is larger than number of pages' do
get :index, group_id: group, page: (last_page + 1).to_param
expect(response).to redirect_to(group_epics_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
end
it 'renders the specified page' do
get :index, group_id: group, page: last_page.to_param
expect(assigns(:epics).current_page).to eq(last_page)
expect(response).to have_gitlab_http_status(200)
end
end
end
describe 'GET #show' do describe 'GET #show' do
def show_epic(format = :html) def show_epic(format = :html)
get :show, group_id: group, id: epic.to_param, format: format get :show, group_id: group, id: epic.to_param, format: format
...@@ -20,7 +56,7 @@ describe Groups::EpicsController do ...@@ -20,7 +56,7 @@ describe Groups::EpicsController do
show_epic show_epic
expect(response.content_type).to eq 'text/html' expect(response.content_type).to eq 'text/html'
expect(response).to render_template 'groups/ee/epics/show' expect(response).to render_template 'groups/epics/show'
end end
context 'with unauthorized user' do context 'with unauthorized user' do
......
require 'spec_helper'
describe 'epics list', :js do
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
before do
sign_in(user)
end
context 'when epics exist for the group' do
let!(:epics) { create_list(:epic, 2, group: group) }
before do
visit group_epics_path(group)
end
it 'shows the epics in the navigation sidebar' do
expect(first('.nav-sidebar .active a .nav-item-name')).to have_content('Epics')
expect(first('.nav-sidebar .active a .count')).to have_content('2')
end
it 'renders the list correctly' do
page.within('.page-with-new-nav .content') do
expect(find('.top-area')).to have_content('All 2')
within('.issuable-list') do
expect(page).to have_content(epics.first.title)
expect(page).to have_content(epics.second.title)
end
end
end
it 'renders the epic detail correctly after clicking the link' do
page.within('.page-with-new-nav .content .issuable-list') do
click_link(epics.first.title)
end
wait_for_requests
expect(page.find('.issuable-details h2.title')).to have_content(epics.first.title)
end
end
context 'when no epics exist for the group' do
it 'renders the empty list page' do
visit group_epics_path(group)
within('#content-body') do
expect(find('.empty-state h4'))
.to have_content('Epics let you manage your portfolio of projects more efficiently and with less effort')
end
end
end
end
require 'spec_helper'
describe EpicsFinder do
let(:user) { create(:user) }
let(:search_user) { create(:user) }
let(:group) { create(:group, :private) }
let(:another_group) { create(:group) }
let!(:epic1) { create(:epic, group: group, title: 'This is awesome epic', created_at: 1.week.ago) }
let!(:epic2) { create(:epic, group: group, created_at: 4.days.ago, author: user) }
let!(:epic3) { create(:epic, group: group, description: 'not so awesome') }
let!(:epic4) { create(:epic, group: another_group) }
describe '#execute' do
def epics(params = {})
params[:group_id] = group.id
described_class.new(search_user, params).execute
end
context 'without param' do
it 'raises an error when group_id param is missing' do
expect { described_class.new(search_user).execute }.to raise_error { ArgumentError }
end
end
context 'when user can not read epics of a group' do
it 'raises an error when group_id param is missing' do
expect { epics }.to raise_error { ArgumentError }
end
end
context 'wtih correct params' do
before do
group.add_developer(search_user)
end
it 'returns all epics that belong to the given group' do
expect(epics).to contain_exactly(epic1, epic2, epic3)
end
context 'by created_at' do
it 'returns all epics created before the given date' do
expect(epics(created_before: 2.days.ago)).to contain_exactly(epic1, epic2)
end
it 'returns all epics created after the given date' do
expect(epics(created_after: 2.days.ago)).to contain_exactly(epic3)
end
it 'returns all epics created within the given interval' do
expect(epics(created_after: 5.days.ago, created_before: 1.day.ago)).to contain_exactly(epic2)
end
end
context 'by search' do
it 'returns all epics that match the search' do
expect(epics(search: 'awesome')).to contain_exactly(epic1, epic3)
end
end
context 'by author' do
it 'returns all epics authored by the given user' do
expect(epics(author_id: user.id)).to contain_exactly(epic2)
end
end
context 'by iids' do
it 'returns all epics by the given iids' do
expect(epics(iids: [epic1.iid, epic3.iid])).to contain_exactly(epic1, epic3)
end
end
end
end
end
...@@ -11,10 +11,11 @@ describe JenkinsDeprecatedService, use_clean_rails_memory_store_caching: true do ...@@ -11,10 +11,11 @@ describe JenkinsDeprecatedService, use_clean_rails_memory_store_caching: true do
describe 'commits methods' do describe 'commits methods' do
def status_body_for_icon(state) def status_body_for_icon(state)
<<eos <<eos
<h1 class="build-caption page-headline"><img style="width: 48px; height: 48px; " alt="Success" class="icon-#{state} icon-xlg" src="/static/855d7c3c/images/48x48/#{state}" tooltip="Success" title="Success"> <h1 class="build-caption page-headline">
Build #188 <img src="/static/8b0a9b52/images/48x48/#{state}" alt="Success" tooltip="Success" style="width: 48px; height: 48px; " class="icon-#{state} icon-xlg" />
(Oct 15, 2014 9:45:21 PM) Build #188
</h1> (Oct 15, 2014 9:45:21 PM)
</h1>
eos eos
end end
...@@ -49,6 +50,13 @@ eos ...@@ -49,6 +50,13 @@ eos
expect(@service.calculate_reactive_cache('2ab7834c', 'master')).to eq(commit_status: 'success') expect(@service.calculate_reactive_cache('2ab7834c', 'master')).to eq(commit_status: 'success')
end end
end end
context 'with bad response' do
it 'has a commit_status of error' do
stub_request(:get, "http://jenkins.gitlab.org/job/2/scm/bySHA1/2ab7834c").to_return(status: 200, body: '<h1>404</h1>', headers: {})
expect(@service.calculate_reactive_cache('2ab7834c', 'master')).to eq(commit_status: :error)
end
end
end end
describe '#commit_status' do describe '#commit_status' do
......
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