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';
import NamespaceSelect from './namespace_select';
/* global NewCommitForm */
/* global NewBranchForm */
/* global Project */
/* global ProjectAvatar */
import Project from './project';
import projectAvatar from './project_avatar';
/* global MergeRequest */
/* global Compare */
/* global CompareAutocomplete */
......@@ -29,7 +29,7 @@ import NamespaceSelect from './namespace_select';
/* global ProjectFindFile */
/* global ProjectNew */
/* global ProjectShow */
/* global ProjectImport */
import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
/* global Sidebar */
......@@ -386,7 +386,7 @@ import initGroupAnalytics from './init_group_analytics';
GpgBadges.fetch();
break;
case 'projects:imports:show':
new ProjectImport();
projectImport();
break;
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
......@@ -416,7 +416,7 @@ import initGroupAnalytics from './init_group_analytics';
new UserCallout({ className: 'js-mr-approval-callout' });
break;
case 'projects:imports:show':
new ProjectImport();
projectImport();
break;
case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form'));
......@@ -686,7 +686,7 @@ import initGroupAnalytics from './init_group_analytics';
break;
case 'projects':
new Project();
new ProjectAvatar();
projectAvatar();
switch (path[1]) {
case 'compare':
new CompareAutocomplete();
......
......@@ -72,8 +72,6 @@ import './notifications_dropdown';
import './notifications_form';
import './pager';
import './preview_markdown';
import './project';
import './project_avatar';
import './project_find_file';
import './project_import';
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 */
import Cookies from 'js-cookie';
(function() {
this.Project = (function() {
function Project() {
const $cloneOptions = $('ul.clone-options-dropdown');
const $projectCloneField = $('#project_clone');
const $cloneBtnText = $('a.clone-dropdown-btn span');
export default class Project {
constructor() {
const $cloneOptions = $('ul.clone-options-dropdown');
const $projectCloneField = $('#project_clone');
const $cloneBtnText = $('a.clone-dropdown-btn span');
const selectedCloneOption = $cloneBtnText.text().trim();
if (selectedCloneOption.length > 0) {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
$('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
const selectedCloneOption = $cloneBtnText.text().trim();
if (selectedCloneOption.length > 0) {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
e.preventDefault();
$('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
$('.is-active', $cloneOptions).not($this).removeClass('is-active');
$this.toggleClass('is-active');
$projectCloneField.val(url);
$cloneBtnText.text($this.text());
e.preventDefault();
$('#modal-geo-info').data({
cloneUrlSecondary: $this.attr('href'),
cloneUrlPrimary: $this.data('primaryUrl') || ''
});
$('.is-active', $cloneOptions).not($this).removeClass('is-active');
$this.toggleClass('is-active');
$projectCloneField.val(url);
$cloneBtnText.text($this.text());
return $('.clone').text(url);
});
// Ref switcher
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();
$('#modal-geo-info').data({
cloneUrlSecondary: $this.attr('href'),
cloneUrlPrimary: $this.data('primaryUrl') || '',
});
this.projectSelectDropdown();
}
Project.prototype.projectSelectDropdown = function() {
new ProjectSelect();
$('.project-item-select').on('click', (function(_this) {
return function(e) {
return _this.changeProject($(e.currentTarget).val());
};
})(this));
};
Project.prototype.changeProject = function(url) {
return window.location = url;
};
Project.prototype.initRefSwitcher = function() {
var refListItem = document.createElement('li');
var refLink = document.createElement('a');
refLink.href = '#';
return $('.js-project-refs-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
selected = $dropdown.data('selected');
return $dropdown.glDropdown({
data: function(term, callback) {
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref'),
search: term
},
dataType: "json"
}).done(function(refs) {
return callback(refs);
});
},
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';
}
link.textContent = ref;
link.dataset.ref = ref;
li.appendChild(link);
return $('.clone').text(url);
});
// Ref switcher
Project.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();
});
Project.projectSelectDropdown();
}
static projectSelectDropdown() {
new ProjectSelect();
$('.project-item-select').on('click', e => Project.changeProject($(e.currentTarget).val()));
}
static changeProject(url) {
return window.location = url;
}
static initRefSwitcher() {
var refListItem = document.createElement('li');
var refLink = document.createElement('a');
refLink.href = '#';
return $('.js-project-refs-dropdown').each(function() {
var $dropdown, selected;
$dropdown = $(this);
selected = $dropdown.data('selected');
return $dropdown.glDropdown({
data: function(term, callback) {
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref'),
search: term,
},
dataType: 'json',
}).done(function(refs) {
return callback(refs);
});
},
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;
},
id: function(obj, $el) {
return $el.attr('data-ref');
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
},
clicked: function(options) {
const { e } = options;
e.preventDefault();
if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form');
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()}`);
}
link.textContent = ref;
link.dataset.ref = ref;
li.appendChild(link);
}
return li;
},
id: function(obj, $el) {
return $el.attr('data-ref');
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
},
clicked: function(options) {
const { e } = options;
e.preventDefault();
if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form');
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 */
(function() {
this.ProjectAvatar = (function() {
function ProjectAvatar() {
$('.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);
});
}
export default function projectAvatar() {
$('.js-choose-project-avatar-button').bind('click', function onClickAvatar() {
const form = $(this).closest('form');
return form.find('.js-project-avatar-input').click();
});
return ProjectAvatar;
})();
}).call(window);
$('.js-project-avatar-input').bind('change', function onClickAvatarInput() {
const form = $(this).closest('form');
// 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() {
this.ProjectImport = (function() {
function ProjectImport() {
setTimeout(function() {
return gl.utils.visitUrl(location.href);
}, 5000);
}
export default function projectImport() {
setTimeout(() => {
visitUrl(location.href);
}, 5000);
}
return ProjectImport;
})();
}).call(window);
......@@ -7,29 +7,68 @@
width: 100%;
height: 100%;
padding-bottom: 25px;
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
}
.blank-state {
padding-top: 20px;
padding-bottom: 20px;
.blank-state-row {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
height: 100%;
}
.blank-state-welcome {
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 {
.blank-state-welcome-title {
font-size: 24px;
@media (min-width: $screen-sm-min) {
flex: 0 0 49%;
&:nth-child(odd) {
margin-right: 5px;
}
.blank-state-text {
margin-bottom: 0;
&:nth-child(even) {
margin-left: 5px;
}
}
.blank-state-icon {
padding-bottom: 20px;
&:hover {
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 {
display: block;
margin: auto;
......@@ -38,33 +77,46 @@
.blank-state-title {
margin-top: 0;
margin-bottom: 10px;
font-size: 18px;
}
.blank-state-text {
max-width: $container-text-max-width;
margin: 0 auto $gl-padding;
font-size: 14px;
.blank-state-body {
@media (max-width: $screen-xs-max) {
text-align: center;
margin-top: 20px;
}
@media (min-width: $screen-sm-min) {
padding-left: 20px;
}
}
}
/* EE-specific Styles */
@media (min-width: $screen-md-min) {
.blank-state-parent-container.has-start-trial-container {
display: flex;
@media (min-width: $screen-lg-min) {
.column-large {
flex: 2;
}
}
.section-ee-trial {
.section-body {
display: flex;
align-items: center;
justify-content: center;
.column-small {
flex: 1;
margin-bottom: 15px;
.blank-state {
padding: 20px;
text-align: center;
max-width: 400px;
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
def show
respond_to do |format|
format.html do
render show_view
end
format.html
format.json do
render json: serializer.represent(issuable, serializer: params[:serializer])
end
......@@ -154,10 +151,6 @@ module IssuableActions
end
end
def show_view
'show'
end
def serializer
raise NotImplementedError
end
......
......@@ -10,6 +10,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv]
before_action :set_issuables_index, only: [:index]
# Allow write(create) issue
......
......@@ -159,7 +159,7 @@ module IssuablesHelper
label_names.join(', ')
end
def issuables_state_counter_text(issuable_type, state)
def issuables_state_counter_text(issuable_type, state = :all)
titles = {
opened: "Open"
}
......
......@@ -3,6 +3,7 @@ class MergeRequest < ActiveRecord::Base
include Issuable
include Noteable
include Referable
include Elastic::MergeRequestsSearch
include IgnorableColumn
include TimeTrackable
......
......@@ -14,7 +14,7 @@ class JenkinsDeprecatedService < CiService
def compose_service_hook
hook = service_hook || build_service_hook
jenkins_url = project_url.sub(/job\/.*/, '')
hook.url = jenkins_url + "/gitlab/build_now"
hook.url = jenkins_url + "gitlab/build_now"
hook.save
end
......@@ -101,7 +101,13 @@ class JenkinsDeprecatedService < CiService
if response.code == 200
# 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?)
'success'
elsif src =~ /(red|aborted|yellow)\.png$/
......
.blank-state
.blank-state-icon
= custom_icon("add_new_user", size: 50)
.blank-state-body
%h3.blank-state-title
Add user
%p.blank-state-text
Add your team members and others to GitLab.
= link_to new_admin_user_path, class: "btn btn-new" do
New user
.blank-state-row
= link_to new_project_path, class: "blank-state-link" do
.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
Projects are where you store your code, access issues, wiki and other features of GitLab.
.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.
= link_to admin_root_path, class: "btn btn-new" do
Configure
- if current_user.can_create_group?
= link_to admin_root_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
.blank-state-body
%h3.blank-state-title
Create a group
%p.blank-state-text
Groups are a great way to organize projects and people.
- if current_user.can_create_group?
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
.blank-state-body
%h3.blank-state-title
Create a group
%p.blank-state-text
Groups are a great way to organize projects and people.
= link_to new_group_path, class: "btn btn-new" do
New group
= link_to new_admin_user_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_user", size: 50)
.blank-state-body
%h3.blank-state-title
Add people
%p.blank-state-text
Add your team members and others to GitLab.
= 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
- if current_user.can_create_group?
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
.blank-state-body
%h3.blank-state-title
Create a group for several dependent projects.
%p.blank-state-text
Groups are the best way to manage projects and members.
= link_to new_group_path, class: "btn btn-new" do
New group
.blank-state-row
- if current_user.can_create_project?
= link_to new_project_path, class: "blank-state-link" do
.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
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
.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 current_user.can_create_project?
You don't have access to any projects right now.
You can create up to
%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 current_user.can_create_group?
= link_to new_group_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
.blank-state-body
%h3.blank-state-title
Create a group
%p.blank-state-text
Groups are the best way to manage projects and members.
- if public_project_count > 0
.blank-state
.blank-state-icon
= custom_icon("globe", size: 50)
.blank-state-body
%h3.blank-state-title
Explore public projects
%p.blank-state-text
There are
= number_with_delimiter(public_project_count)
public projects on this server.
Public projects are an easy way to allow
everyone to have read-only access.
= link_to trending_explore_projects_path, class: "btn btn-new" do
Browse projects
- if public_project_count > 0
= link_to trending_explore_projects_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("globe", size: 50)
.blank-state-body
%h3.blank-state-title
Explore public projects
%p.blank-state-text
There are
= number_with_delimiter(public_project_count)
public projects on this server.
Public projects are an easy way to allow
everyone to have read-only access.
= 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?
.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) }
.container.section-body
.blank-state.blank-state-welcome
%h2.blank-state-welcome-title
Welcome to GitLab
%p.blank-state-text
Code, test, and deploy together
- if current_user.admin?
= render "blank_state_admin_welcome"
- else
= render "blank_state_welcome"
- if admin_without_ee_license
.col-md-6.section-container.section-ee-trial
.container.section-body
= render "blank_state_ee_trial"
.row
.blank-state-welcome
%h2.blank-state-welcome-title
Welcome to GitLab
%p.blank-state-text
Code, test, and deploy together
.blank-state-row
%div{ class: ('column-large' if admin_without_ee_license) }
- if current_user.admin?
= render "blank_state_admin_welcome"
- else
= render "blank_state_welcome"
- if admin_without_ee_license
.column-small
= render "blank_state_ee_trial"
- 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
- 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']
- if @group.feature_available?(:group_issue_boards)
- issues_sub_menu_items.push('boards#index', 'boards#show')
......@@ -43,6 +45,21 @@
%span
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
= link_to issues_group_path(@group) do
.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>
\ 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="#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>
<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)
- page_context_word = type.to_s.humanize(capitalize: false)
- issuables = @issues || @merge_requests
- issuables = @issues || @merge_requests || @epics
%ul.nav-links.issues-state-filters
%li{ class: active_when(params[:state] == '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 != :epics
%li{ class: active_when(params[:state] == '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
%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
#{issuables_state_counter_text(type, :merged)}
- if type == :merge_requests
%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
#{issuables_state_counter_text(type, :merged)}
%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
#{issuables_state_counter_text(type, :closed)}
- else
%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
#{issuables_state_counter_text(type, :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
#{issuables_state_counter_text(type, :closed)}
- else
%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
#{issuables_state_counter_text(type, :closed)}
%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
......
---
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:
## 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.
Replicating over SSH has been deprecated, and support for this option will be
removed in a future release.
......
......@@ -12,7 +12,6 @@ module EE
def service_desk
@issues = @issuables
@users.push(::User.support_bot)
end
......
class Groups::EpicsController < Groups::ApplicationController
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
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
def epic
......@@ -38,7 +54,22 @@ class Groups::EpicsController < Groups::ApplicationController
Epics::UpdateService.new(nil, current_user, epic_params)
end
def show_view
'groups/ee/epics/show'
def set_issuables_index
@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
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
enable :destroy_epic
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
......
%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
module Main
autoload :Entry, 'qa/page/main/entry'
autoload :Login, 'qa/page/main/login'
autoload :Menu, 'qa/page/main/menu'
end
......
......@@ -4,7 +4,8 @@ module QA
module License
class Add < QA::Scenario::Template
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::Admin::Menu.act { go_to_license }
......
......@@ -23,7 +23,7 @@ module QA
def password=(pass)
@password = pass
@uri.password = pass
@uri.password = CGI.escape(pass)
end
def use_default_credentials
......
......@@ -2,9 +2,14 @@ module QA
module Page
module Main
class Entry < Page::Base
def initialize
visit(Runtime::Scenario.gitlab_address)
def visit_login_page
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
#
start = Time.now
......@@ -14,18 +19,6 @@ module QA
refresh
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
......
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
feature 'standard root login', :core 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
# this is the only way to tell if user is signed in correctly.
......
module QA
feature 'create a new group', :mattermost 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::Dashboard::Groups.perform do |page|
......
module QA
feature 'logging in to Mattermost', :mattermost 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::Main.perform do |page|
......
module QA
feature 'create a new project', :core 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|
project.name = 'awesome-project'
......
......@@ -9,7 +9,8 @@ module QA
end
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.name = 'project-with-code'
......
......@@ -2,7 +2,8 @@ module QA
feature 'push code to repository', :core do
context 'with regular account over http' 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.name = 'project_with_code'
......
......@@ -9,6 +9,42 @@ describe Groups::EpicsController do
sign_in(user)
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
def show_epic(format = :html)
get :show, group_id: group, id: epic.to_param, format: format
......@@ -20,7 +56,7 @@ describe Groups::EpicsController do
show_epic
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
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
describe 'commits methods' do
def status_body_for_icon(state)
<<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">
Build #188
(Oct 15, 2014 9:45:21 PM)
</h1>
<h1 class="build-caption page-headline">
<img src="/static/8b0a9b52/images/48x48/#{state}" alt="Success" tooltip="Success" style="width: 48px; height: 48px; " class="icon-#{state} icon-xlg" />
Build #188
(Oct 15, 2014 9:45:21 PM)
</h1>
eos
end
......@@ -49,6 +50,13 @@ eos
expect(@service.calculate_reactive_cache('2ab7834c', 'master')).to eq(commit_status: 'success')
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
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