Commit 5e6b5832 authored by Achilleas Pipinellis's avatar Achilleas Pipinellis

Merge branch 'master' into 'docs/migrating-from-tfs'

# Conflicts:
#   doc/user/project/import/index.md
parents 794e3766 cdd8f2f3
...@@ -43,6 +43,7 @@ stages: ...@@ -43,6 +43,7 @@ stages:
# Predefined scopes # Predefined scopes
.dedicated-runner: &dedicated-runner .dedicated-runner: &dedicated-runner
retry: 1
tags: tags:
- gitlab-org - gitlab-org
...@@ -208,11 +209,10 @@ update-tests-metadata: ...@@ -208,11 +209,10 @@ update-tests-metadata:
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH' - '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/${CI_PROJECT_NAME}/all_node_*.json - rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json
flaky-examples-check: flaky-examples-check:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
image: ruby:2.3-alpine image: ruby:2.3-alpine
services: [] services: []
before_script: [] before_script: []
...@@ -227,6 +227,7 @@ flaky-examples-check: ...@@ -227,6 +227,7 @@ flaky-examples-check:
- branches - branches
except: except:
- master - master
- /(^docs[\/-].*|.*-docs$)/
artifacts: artifacts:
expire_in: 30d expire_in: 30d
paths: paths:
......
...@@ -181,7 +181,7 @@ gem 'connection_pool', '~> 2.0' ...@@ -181,7 +181,7 @@ gem 'connection_pool', '~> 2.0'
gem 'hipchat', '~> 1.5.0' gem 'hipchat', '~> 1.5.0'
# JIRA integration # JIRA integration
gem 'jira-ruby', '~> 1.1.2' gem 'jira-ruby', '~> 1.4'
# Flowdock integration # Flowdock integration
gem 'gitlab-flowdock-git-hook', '~> 1.0.1' gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
...@@ -397,7 +397,7 @@ group :ed25519 do ...@@ -397,7 +397,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.31.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.32.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -275,7 +275,7 @@ GEM ...@@ -275,7 +275,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.31.0) gitaly-proto (0.32.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -404,8 +404,9 @@ GEM ...@@ -404,8 +404,9 @@ GEM
cause cause
json json
ipaddress (0.8.3) ipaddress (0.8.3)
jira-ruby (1.1.2) jira-ruby (1.4.1)
activesupport activesupport
multipart-post
oauth (~> 0.5, >= 0.5.0) oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2) jquery-atwho-rails (1.3.2)
jquery-rails (4.1.1) jquery-rails (4.1.1)
...@@ -1020,7 +1021,7 @@ DEPENDENCIES ...@@ -1020,7 +1021,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.31.0) gitaly-proto (~> 0.32.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1) gitlab-markup (~> 1.5.1)
...@@ -1042,7 +1043,7 @@ DEPENDENCIES ...@@ -1042,7 +1043,7 @@ DEPENDENCIES
html2text html2text
httparty (~> 0.13.3) httparty (~> 0.13.3)
influxdb (~> 0.2) influxdb (~> 0.2)
jira-ruby (~> 1.1.2) jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2) jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0) jquery-rails (~> 4.1.0)
json-schema (~> 2.6.2) json-schema (~> 2.6.2)
......
...@@ -199,26 +199,8 @@ available in the package repositories. ...@@ -199,26 +199,8 @@ available in the package repositories.
## Release retrospective and kickoff ## Release retrospective and kickoff
### Retrospective - [Retrospective](https://about.gitlab.com/handbook/engineering/workflow/#retrospective)
- [Kickoff](https://about.gitlab.com/handbook/engineering/workflow/#kickoff)
After each release, we have a retrospective call where we discuss what went well,
what went wrong, and what we can improve for the next release. The
[retrospective notes] are public and you are invited to comment on them.
If you're interested, you can even join the
[retrospective call][retro-kickoff-call], on the first working day after the
22nd at 6pm CET / 9am PST.
### Kickoff
Before working on the next release, we have a
kickoff call to explain what we expect to ship in the next release. The
[kickoff notes] are public and you are invited to comment on them.
If you're interested, you can even join the [kickoff call][retro-kickoff-call],
on the first working day after the 7th at 6pm CET / 9am PST..
[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing
[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing
[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206
## Copy & paste responses ## Copy & paste responses
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines) [![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
[![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.svg)](https://gemnasium.com/gitlabhq/gitlabhq)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
......
...@@ -5,7 +5,7 @@ const Api = { ...@@ -5,7 +5,7 @@ const Api = {
groupPath: '/api/:version/groups/:id.json', groupPath: '/api/:version/groups/:id.json',
namespacesPath: '/api/:version/namespaces.json', namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json', groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json?simple=true', projectsPath: '/api/:version/projects.json',
labelsPath: '/:namespace_path/:project_path/labels', labelsPath: '/:namespace_path/:project_path/labels',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key', gitignorePath: '/api/:version/templates/gitignores/:key',
...@@ -58,6 +58,7 @@ const Api = { ...@@ -58,6 +58,7 @@ const Api = {
const defaults = { const defaults = {
search: query, search: query,
per_page: 20, per_page: 20,
simple: true,
}; };
if (gon.current_user_id) { if (gon.current_user_id) {
......
export const addTooltipToEl = (el) => {
const textEl = el.querySelector('.js-breadcrumb-item-text');
if (textEl && textEl.scrollWidth > textEl.offsetWidth) {
el.setAttribute('title', el.textContent);
el.setAttribute('data-container', 'body');
el.classList.add('has-tooltip');
}
};
export default () => {
const breadcrumbs = document.querySelector('.js-breadcrumbs-list');
if (breadcrumbs) {
const topLevelLinks = [...breadcrumbs.children].filter(el => !el.classList.contains('dropdown'))
.map(el => el.querySelector('a'))
.filter(el => el);
const $expander = $('.js-breadcrumbs-collapsed-expander');
topLevelLinks.forEach(el => addTooltipToEl(el));
$expander.closest('.dropdown')
.on('show.bs.dropdown hide.bs.dropdown', (e) => {
$('.js-breadcrumbs-collapsed-expander', e.currentTarget).toggleClass('open')
.tooltip('hide');
});
}
};
...@@ -2,3 +2,4 @@ import 'underscore'; ...@@ -2,3 +2,4 @@ import 'underscore';
import './polyfills'; import './polyfills';
import './jquery'; import './jquery';
import './bootstrap'; import './bootstrap';
import './vue';
import Vue from 'vue'; import Vue from 'vue';
import './vue_resource_interceptor';
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
Vue.config.productionTip = false; Vue.config.productionTip = false;
......
...@@ -21,11 +21,13 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -21,11 +21,13 @@ const DiffNoteAvatars = Vue.extend({
}, },
template: ` template: `
<div class="diff-comment-avatar-holders" <div class="diff-comment-avatar-holders"
:class="discussionClassName"
v-show="notesCount !== 0"> v-show="notesCount !== 0">
<div v-if="!isVisible"> <div v-if="!isVisible">
<!-- FIXME: Pass an alt attribute here for accessibility --> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image <user-avatar-image
v-for="note in notesSubset" v-for="note in notesSubset"
:key="note.id"
class="diff-comment-avatar js-diff-comment-avatar" class="diff-comment-avatar js-diff-comment-avatar"
@click.native="clickedAvatar($event)" @click.native="clickedAvatar($event)"
:img-src="note.authorAvatar" :img-src="note.authorAvatar"
...@@ -68,7 +70,8 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -68,7 +70,8 @@ const DiffNoteAvatars = Vue.extend({
}); });
}); });
}, },
destroyed() { beforeDestroy() {
this.addNoCommentClass();
$(document).off('toggle.comments'); $(document).off('toggle.comments');
}, },
watch: { watch: {
...@@ -85,6 +88,9 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -85,6 +88,9 @@ const DiffNoteAvatars = Vue.extend({
}, },
}, },
computed: { computed: {
discussionClassName() {
return `js-diff-avatars-${this.discussionId}`;
},
notesSubset() { notesSubset() {
let notes = []; let notes = [];
......
...@@ -32,6 +32,10 @@ $(() => { ...@@ -32,6 +32,10 @@ $(() => {
const tmpApp = new tmp().$mount(); const tmpApp = new tmp().$mount();
$(this).replaceWith(tmpApp.$el); $(this).replaceWith(tmpApp.$el);
$(tmpApp.$el).one('remove.vue', () => {
tmpApp.$destroy();
tmpApp.$el.remove();
});
}); });
const $components = $(COMPONENT_SELECTOR).filter(function () { const $components = $(COMPONENT_SELECTOR).filter(function () {
......
...@@ -41,7 +41,6 @@ import Issue from './issue'; ...@@ -41,7 +41,6 @@ import Issue from './issue';
import BindInOut from './behaviors/bind_in_out'; import BindInOut from './behaviors/bind_in_out';
import DeleteModal from './branches/branches_delete_modal'; import DeleteModal from './branches/branches_delete_modal';
import Group from './group'; import Group from './group';
import GroupName from './group_name';
import GroupsList from './groups_list'; import GroupsList from './groups_list';
import ProjectsList from './projects_list'; import ProjectsList from './projects_list';
import setupProjectEdit from './project_edit'; import setupProjectEdit from './project_edit';
...@@ -554,9 +553,6 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -554,9 +553,6 @@ import initChangesDropdown from './init_changes_dropdown';
case 'root': case 'root':
new UserCallout(); new UserCallout();
break; break;
case 'groups':
new GroupName();
break;
case 'profiles': case 'profiles':
new NotificationsForm(); new NotificationsForm();
new NotificationsDropdown(); new NotificationsDropdown();
...@@ -564,7 +560,6 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -564,7 +560,6 @@ import initChangesDropdown from './init_changes_dropdown';
case 'projects': case 'projects':
new Project(); new Project();
new ProjectAvatar(); new ProjectAvatar();
new GroupName();
switch (path[1]) { switch (path[1]) {
case 'compare': case 'compare':
new CompareAutocomplete(); new CompareAutocomplete();
......
...@@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown { ...@@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown {
.map(tokenKey => ({ .map(tokenKey => ({
icon: `fa-${tokenKey.icon}`, icon: `fa-${tokenKey.icon}`,
hint: tokenKey.key, hint: tokenKey.key,
tag: `<${tokenKey.tag}>`, tag: `:${tokenKey.tag}`,
type: tokenKey.type, type: tokenKey.type,
})); }));
......
...@@ -637,11 +637,15 @@ GitLabDropdown = (function() { ...@@ -637,11 +637,15 @@ GitLabDropdown = (function() {
value = this.options.id ? this.options.id(data) : data.id; value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName; fieldName = this.options.fieldName;
if (value) { value = value.toString().replace(/'/g, '\\\''); } if (value) {
value = value.toString().replace(/'/g, '\\\'');
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
if (field.length) { if (field.length) {
selected = true; selected = true;
}
} else {
field = this.dropdown.parent().find(`input[name='${fieldName}']`);
selected = !field.length;
} }
} }
// Set URL // Set URL
......
import Cookies from 'js-cookie';
import _ from 'underscore';
export default class GroupName {
constructor() {
this.titleContainer = document.querySelector('.js-title-container');
this.title = this.titleContainer.querySelector('.title');
if (this.title) {
this.titleWidth = this.title.offsetWidth;
this.groupTitle = this.titleContainer.querySelector('.group-title');
this.groups = this.titleContainer.querySelectorAll('.group-path');
this.toggle = null;
this.isHidden = false;
this.init();
}
}
init() {
if (this.groups.length > 0) {
this.groups[this.groups.length - 1].classList.remove('hidable');
this.toggleHandler();
window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100));
}
this.render();
}
toggleHandler() {
if (this.titleWidth > this.titleContainer.offsetWidth) {
if (!this.toggle) this.createToggle();
this.showToggle();
} else if (this.toggle) {
this.hideToggle();
}
}
createToggle() {
this.toggle = document.createElement('button');
this.toggle.setAttribute('type', 'button');
this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path');
if (Cookies.get('new_nav') === 'true') {
this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>';
} else {
this.toggle.innerHTML = '...';
}
this.toggle.addEventListener('click', this.toggleGroups.bind(this));
if (Cookies.get('new_nav') === 'true') {
this.title.insertBefore(this.toggle, this.groupTitle);
} else {
this.titleContainer.insertBefore(this.toggle, this.title);
}
this.toggleGroups();
}
showToggle() {
this.title.classList.add('wrap');
this.toggle.classList.remove('hidden');
if (this.isHidden) this.groupTitle.classList.add('hidden');
}
hideToggle() {
this.title.classList.remove('wrap');
this.toggle.classList.add('hidden');
if (this.isHidden) this.groupTitle.classList.remove('hidden');
}
toggleGroups() {
this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('hidden');
}
render() {
this.title.classList.remove('initializing');
}
}
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
/* global SubscriptionSelect */ /* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import SidebarHeightManager from './sidebar_height_manager';
const HIDDEN_CLASS = 'hidden'; const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content'; const DISABLED_CONTENT_CLASS = 'disabled-content';
...@@ -50,13 +49,6 @@ export default class IssuableBulkUpdateSidebar { ...@@ -50,13 +49,6 @@ export default class IssuableBulkUpdateSidebar {
new SubscriptionSelect(); new SubscriptionSelect();
} }
getNavHeight() {
const navbarHeight = $('.navbar-gitlab').outerHeight();
const layoutNavHeight = $('.layout-nav').outerHeight();
const subNavScroll = $('.sub-nav-scroll').outerHeight();
return navbarHeight + layoutNavHeight + subNavScroll;
}
setupBulkUpdateActions() { setupBulkUpdateActions() {
IssuableBulkUpdateActions.setOriginalDropdownData(); IssuableBulkUpdateActions.setOriginalDropdownData();
} }
...@@ -84,23 +76,6 @@ export default class IssuableBulkUpdateSidebar { ...@@ -84,23 +76,6 @@ export default class IssuableBulkUpdateSidebar {
this.toggleBulkEditButtonDisabled(enable); this.toggleBulkEditButtonDisabled(enable);
this.toggleOtherFiltersDisabled(enable); this.toggleOtherFiltersDisabled(enable);
this.toggleCheckboxDisplay(enable); this.toggleCheckboxDisplay(enable);
if (enable) {
this.initAffix();
SidebarHeightManager.init();
}
}
initAffix() {
if (!this.$sidebar.hasClass('affix-top')) {
const offsetTop = $('.scrolling-tabs-container').outerHeight() + $('.sub-nav-scroll').outerHeight();
this.$sidebar.affix({
offset: {
top: offsetTop,
},
});
}
} }
updateSelectedIssuableIds() { updateSelectedIssuableIds() {
......
...@@ -50,19 +50,10 @@ import initFlyOutNav from './fly_out_nav'; ...@@ -50,19 +50,10 @@ import initFlyOutNav from './fly_out_nav';
}); });
}); });
function applyScrollNavClass() {
const scrollOpacityHeight = 40;
$('.navbar-border').css('opacity', Math.min($(window).scrollTop() / scrollOpacityHeight, 1));
}
$(() => { $(() => {
if (Cookies.get('new_nav') === 'true') { const newNavSidebar = new NewNavSidebar();
const newNavSidebar = new NewNavSidebar(); newNavSidebar.bindEvents();
newNavSidebar.bindEvents();
initFlyOutNav();
}
$(window).on('scroll', _.throttle(applyScrollNavClass, 100)); initFlyOutNav();
}); });
}).call(window); }).call(window);
...@@ -132,6 +132,7 @@ import './project_new'; ...@@ -132,6 +132,7 @@ import './project_new';
import './project_select'; import './project_select';
import './project_show'; import './project_show';
import './project_variables'; import './project_variables';
import './projects_dropdown';
import './projects_list'; import './projects_list';
import './syntax_highlight'; import './syntax_highlight';
import './render_math'; import './render_math';
...@@ -143,6 +144,7 @@ import './smart_interval'; ...@@ -143,6 +144,7 @@ import './smart_interval';
import './star'; import './star';
import './subscription'; import './subscription';
import './subscription_select'; import './subscription_select';
import initBreadcrumbs from './breadcrumb';
import './dispatcher'; import './dispatcher';
...@@ -180,6 +182,8 @@ $(function () { ...@@ -180,6 +182,8 @@ $(function () {
var bootstrapBreakpoint = bp.getBreakpointSize(); var bootstrapBreakpoint = bp.getBreakpointSize();
var fitSidebarForSize; var fitSidebarForSize;
initBreadcrumbs();
// Set the default path for all cookies to GitLab's root directory // Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/'; Cookies.defaults.path = gon.relative_url_root || '/';
...@@ -249,7 +253,10 @@ $(function () { ...@@ -249,7 +253,10 @@ $(function () {
// Initialize popovers // Initialize popovers
$body.popover({ $body.popover({
selector: '[data-toggle="popover"]', selector: '[data-toggle="popover"]',
trigger: 'focus' trigger: 'focus',
// set the viewport to the main content, excluding the navigation bar, so
// the navigation can't overlap the popover
viewport: '.page-with-sidebar'
}); });
$('.trigger-submit').on('change', function () { $('.trigger-submit').on('change', function () {
return $(this).parents('form').submit(); return $(this).parents('form').submit();
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import statusCodes from '../../lib/utils/http_status'; import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service'; import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
import GraphRow from './graph_row.vue'; import Graph from './graph.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store'; import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
...@@ -32,8 +32,8 @@ ...@@ -32,8 +32,8 @@
}, },
components: { components: {
Graph,
GraphGroup, GraphGroup,
GraphRow,
EmptyState, EmptyState,
}, },
...@@ -127,10 +127,10 @@ ...@@ -127,10 +127,10 @@
:key="index" :key="index"
:name="groupData.group" :name="groupData.group"
> >
<graph-row <graph
v-for="(row, index) in groupData.metrics" v-for="(graphData, index) in groupData.metrics"
:key="index" :key="index"
:row-data="row" :graph-data="graphData"
:update-aspect-ratio="updateAspectRatio" :update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData" :deployment-data="store.deploymentData"
/> />
......
...@@ -3,11 +3,12 @@ ...@@ -3,11 +3,12 @@
import GraphLegend from './graph/legend.vue'; import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue'; import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue'; import GraphDeployment from './graph/deployment.vue';
import monitoringPaths from './monitoring_paths.vue';
import MonitoringMixin from '../mixins/monitoring_mixins'; import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import measurements from '../utils/measurements'; import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils';
import { timeScaleFormat } from '../utils/date_time_formatters'; import { timeScaleFormat } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints'; import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left; const bisectDate = d3.bisector(d => d.time).left;
...@@ -18,10 +19,6 @@ ...@@ -18,10 +19,6 @@
type: Object, type: Object,
required: true, required: true,
}, },
classType: {
type: String,
required: true,
},
updateAspectRatio: { updateAspectRatio: {
type: Boolean, type: Boolean,
required: true, required: true,
...@@ -36,32 +33,29 @@ ...@@ -36,32 +33,29 @@
data() { data() {
return { return {
baseGraphHeight: 450,
baseGraphWidth: 600,
graphHeight: 450, graphHeight: 450,
graphWidth: 600, graphWidth: 600,
graphHeightOffset: 120, graphHeightOffset: 120,
xScale: {},
yScale: {},
margin: {}, margin: {},
data: [],
unitOfDisplay: '', unitOfDisplay: '',
areaColorRgb: '#8fbce8', areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1', lineColorRgb: '#1f78d1',
yAxisLabel: '', yAxisLabel: '',
legendTitle: '', legendTitle: '',
reducedDeploymentData: [], reducedDeploymentData: [],
area: '',
line: '',
measurements: measurements.large, measurements: measurements.large,
currentData: { currentData: {
time: new Date(), time: new Date(),
value: 0, value: 0,
}, },
currentYCoordinate: 0, currentDataIndex: 0,
currentXCoordinate: 0, currentXCoordinate: 0,
currentFlagPosition: 0, currentFlagPosition: 0,
metricUsage: '',
showFlag: false, showFlag: false,
showDeployInfo: true, showDeployInfo: true,
timeSeries: [],
}; };
}, },
...@@ -69,16 +63,17 @@ ...@@ -69,16 +63,17 @@
GraphLegend, GraphLegend,
GraphFlag, GraphFlag,
GraphDeployment, GraphDeployment,
monitoringPaths,
}, },
computed: { computed: {
outterViewBox() { outterViewBox() {
return `0 0 ${this.graphWidth} ${this.graphHeight}`; return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
}, },
innerViewBox() { innerViewBox() {
if ((this.graphWidth - 150) > 0) { if ((this.baseGraphWidth - 150) > 0) {
return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`; return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
} }
return '0 0 0 0'; return '0 0 0 0';
}, },
...@@ -89,7 +84,7 @@ ...@@ -89,7 +84,7 @@
paddingBottomRootSvg() { paddingBottomRootSvg() {
return { return {
paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`, paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
}; };
}, },
}, },
...@@ -104,17 +99,16 @@ ...@@ -104,17 +99,16 @@
this.margin = measurements.small.margin; this.margin = measurements.small.margin;
this.measurements = measurements.small; this.measurements = measurements.small;
} }
this.data = query.result[0].values;
this.unitOfDisplay = query.unit || ''; this.unitOfDisplay = query.unit || '';
this.yAxisLabel = this.graphData.y_label || 'Values'; this.yAxisLabel = this.graphData.y_label || 'Values';
this.legendTitle = query.label || 'Average'; this.legendTitle = query.label || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth - this.graphWidth = this.$refs.baseSvg.clientWidth -
this.margin.left - this.margin.right; this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
if (this.data !== undefined) { this.baseGraphHeight = this.graphHeight;
this.renderAxesPaths(); this.baseGraphWidth = this.graphWidth;
this.formatDeployments(); this.renderAxesPaths();
} this.formatDeployments();
}, },
handleMouseOverGraph(e) { handleMouseOverGraph(e) {
...@@ -123,16 +117,17 @@ ...@@ -123,16 +117,17 @@
point.y = e.clientY; point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x = point.x += 7; point.x = point.x += 7;
const timeValueOverlay = this.xScale.invert(point.x); const firstTimeSeries = this.timeSeries[0];
const overlayIndex = bisectDate(this.data, timeValueOverlay, 1); const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
const d0 = this.data[overlayIndex - 1]; const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
const d1 = this.data[overlayIndex]; const d0 = firstTimeSeries.values[overlayIndex - 1];
const d1 = firstTimeSeries.values[overlayIndex];
if (d0 === undefined || d1 === undefined) return; if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
this.currentData = evalTime ? d1 : d0; this.currentData = evalTime ? d1 : d0;
this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time)); this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time));
const currentDeployXPos = this.mouseOverDeployInfo(point.x); const currentDeployXPos = this.mouseOverDeployInfo(point.x);
this.currentYCoordinate = this.yScale(this.currentData.value);
if (this.currentXCoordinate > (this.graphWidth - 200)) { if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103; this.currentFlagPosition = this.currentXCoordinate - 103;
...@@ -145,17 +140,25 @@ ...@@ -145,17 +140,25 @@
} else { } else {
this.showFlag = true; this.showFlag = true;
} }
this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`;
}, },
renderAxesPaths() { renderAxesPaths() {
this.timeSeries = createTimeSeries(this.graphData.queries[0].result,
this.graphWidth,
this.graphHeight,
this.graphHeightOffset);
if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
const axisXScale = d3.time.scale() const axisXScale = d3.time.scale()
.range([0, this.graphWidth]); .range([0, this.graphWidth]);
this.yScale = d3.scale.linear() const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]); .range([this.graphHeight - this.graphHeightOffset, 0]);
axisXScale.domain(d3.extent(this.data, d => d.time));
this.yScale.domain([0, d3.max(this.data.map(d => d.value))]); axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time));
axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]);
const xAxis = d3.svg.axis() const xAxis = d3.svg.axis()
.scale(axisXScale) .scale(axisXScale)
...@@ -164,7 +167,7 @@ ...@@ -164,7 +167,7 @@
.orient('bottom'); .orient('bottom');
const yAxis = d3.svg.axis() const yAxis = d3.svg.axis()
.scale(this.yScale) .scale(axisYScale)
.ticks(measurements.yTicks) .ticks(measurements.yTicks)
.orient('left'); .orient('left');
...@@ -180,25 +183,6 @@ ...@@ -180,25 +183,6 @@
.attr('class', 'axis-tick'); .attr('class', 'axis-tick');
} // Avoid adding the class to the first tick, to prevent coloring } // Avoid adding the class to the first tick, to prevent coloring
}); // This will select all of the ticks once they're rendered }); // This will select all of the ticks once they're rendered
this.xScale = d3.time.scale()
.range([0, this.graphWidth - 70]);
this.xScale.domain(d3.extent(this.data, d => d.time));
const areaFunction = d3.svg.area()
.x(d => this.xScale(d.time))
.y0(this.graphHeight - this.graphHeightOffset)
.y1(d => this.yScale(d.value))
.interpolate('linear');
const lineFunction = d3.svg.line()
.x(d => this.xScale(d.time))
.y(d => this.yScale(d.value));
this.line = lineFunction(this.data);
this.area = areaFunction(this.data);
}, },
}, },
...@@ -219,12 +203,11 @@ ...@@ -219,12 +203,11 @@
}, },
}; };
</script> </script>
<template> <template>
<div <div class="prometheus-graph">
:class="classType"> <h5 class="text-center graph-title">
<h5 {{graphData.title}}
class="text-center graph-title">
{{graphData.title}}
</h5> </h5>
<div <div
class="prometheus-svg-container" class="prometheus-svg-container"
...@@ -245,30 +228,25 @@ ...@@ -245,30 +228,25 @@
:graph-height="graphHeight" :graph-height="graphHeight"
:margin="margin" :margin="margin"
:measurements="measurements" :measurements="measurements"
:area-color-rgb="areaColorRgb"
:legend-title="legendTitle" :legend-title="legendTitle"
:y-axis-label="yAxisLabel" :y-axis-label="yAxisLabel"
:metric-usage="metricUsage" :time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
/> />
<svg <svg
class="graph-data" class="graph-data"
:viewBox="innerViewBox" :viewBox="innerViewBox"
ref="graphData"> ref="graphData">
<path <monitoring-paths
class="metric-area" v-for="(path, index) in timeSeries"
:d="area" :key="index"
:fill="areaColorRgb" :generated-line-path="path.linePath"
transform="translate(-5, 20)"> :generated-area-path="path.areaPath"
</path> :line-color="path.lineColor"
<path :area-color="path.areaColor"
class="metric-line" />
:d="line" <monitoring-deployment
:stroke="lineColorRgb"
fill="none"
stroke-width="2"
transform="translate(-5, 20)">
</path>
<graph-deployment
:show-deploy-info="showDeployInfo" :show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
:graph-height="graphHeight" :graph-height="graphHeight"
...@@ -277,7 +255,6 @@ ...@@ -277,7 +255,6 @@
<graph-flag <graph-flag
v-if="showFlag" v-if="showFlag"
:current-x-coordinate="currentXCoordinate" :current-x-coordinate="currentXCoordinate"
:current-y-coordinate="currentYCoordinate"
:current-data="currentData" :current-data="currentData"
:current-flag-position="currentFlagPosition" :current-flag-position="currentFlagPosition"
:graph-height="graphHeight" :graph-height="graphHeight"
......
...@@ -7,10 +7,6 @@ ...@@ -7,10 +7,6 @@
type: Number, type: Number,
required: true, required: true,
}, },
currentYCoordinate: {
type: Number,
required: true,
},
currentFlagPosition: { currentFlagPosition: {
type: Number, type: Number,
required: true, required: true,
...@@ -60,16 +56,7 @@ ...@@ -60,16 +56,7 @@
:y2="calculatedHeight" :y2="calculatedHeight"
transform="translate(-5, 20)"> transform="translate(-5, 20)">
</line> </line>
<circle <svg
class="circle-metric"
:fill="circleColorRgb"
stroke="#000"
:cx="currentXCoordinate"
:cy="currentYCoordinate"
r="5"
transform="translate(-5, 20)">
</circle>
<svg
class="rect-text-metric" class="rect-text-metric"
:x="currentFlagPosition" :x="currentFlagPosition"
y="0"> y="0">
......
<script> <script>
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
export default { export default {
props: { props: {
graphWidth: { graphWidth: {
...@@ -17,10 +19,6 @@ ...@@ -17,10 +19,6 @@
type: Object, type: Object,
required: true, required: true,
}, },
areaColorRgb: {
type: String,
required: true,
},
legendTitle: { legendTitle: {
type: String, type: String,
required: true, required: true,
...@@ -29,15 +27,25 @@ ...@@ -29,15 +27,25 @@
type: String, type: String,
required: true, required: true,
}, },
metricUsage: { timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String, type: String,
required: true, required: true,
}, },
currentDataIndex: {
type: Number,
required: true,
},
}, },
data() { data() {
return { return {
yLabelWidth: 0, yLabelWidth: 0,
yLabelHeight: 0, yLabelHeight: 0,
seriesXPosition: 0,
metricUsageXPosition: 0,
}; };
}, },
computed: { computed: {
...@@ -63,10 +71,28 @@ ...@@ -63,10 +71,28 @@
yPosition() { yPosition() {
return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0; return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
}, },
},
methods: {
translateLegendGroup(index) {
return `translate(0, ${12 * (index)})`;
},
formatMetricUsage(series) {
return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`;
},
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
const bbox = this.$refs.ylabel.getBBox(); const bbox = this.$refs.ylabel.getBBox();
this.metricUsageXPosition = 0;
this.seriesXPosition = 0;
if (this.$refs.legendTitleSvg != null) {
this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
}
if (this.$refs.seriesTitleSvg != null) {
this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
}
this.yLabelWidth = bbox.width + 10; // Added some padding this.yLabelWidth = bbox.width + 10; // Added some padding
this.yLabelHeight = bbox.height + 5; this.yLabelHeight = bbox.height + 5;
}); });
...@@ -121,24 +147,33 @@ ...@@ -121,24 +147,33 @@
dy=".35em"> dy=".35em">
Time Time
</text> </text>
<rect <g class="legend-group"
:fill="areaColorRgb" v-for="(series, index) in timeSeries"
:width="measurements.legends.width" :key="index"
:height="measurements.legends.height" :transform="translateLegendGroup(index)">
x="20" <rect
:y="graphHeight - measurements.legendOffset"> :fill="series.areaColor"
</rect> :width="measurements.legends.width"
<text :height="measurements.legends.height"
class="text-metric-title" x="20"
x="50" :y="graphHeight - measurements.legendOffset">
:y="graphHeight - 25"> </rect>
{{legendTitle}} <text
</text> v-if="timeSeries.length > 1"
<text class="legend-metric-title"
class="text-metric-usage" ref="legendTitleSvg"
x="50" x="38"
:y="graphHeight - 10"> :y="graphHeight - 30">
{{metricUsage}} {{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}}
</text> </text>
<text
v-else
class="legend-metric-title"
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30">
{{legendTitle}} {{formatMetricUsage(series)}}
</text>
</g>
</g> </g>
</template> </template>
...@@ -14,7 +14,7 @@ export default { ...@@ -14,7 +14,7 @@ export default {
<div class="panel-heading"> <div class="panel-heading">
<h4>{{name}}</h4> <h4>{{name}}</h4>
</div> </div>
<div class="panel-body"> <div class="panel-body prometheus-graph-group">
<slot /> <slot />
</div> </div>
</div> </div>
......
<script>
import Graph from './graph.vue';
export default {
props: {
rowData: {
type: Array,
required: true,
},
updateAspectRatio: {
type: Boolean,
required: true,
},
deploymentData: {
type: Array,
required: true,
},
},
components: {
Graph,
},
computed: {
bootstrapClass() {
return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12';
},
},
};
</script>
<template>
<div class="prometheus-row row">
<graph
v-for="(graphData, index) in rowData"
:graph-data="graphData"
:class-type="bootstrapClass"
:key="index"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="deploymentData"
/>
</div>
</template>
<script>
export default {
props: {
generatedLinePath: {
type: String,
required: true,
},
generatedAreaPath: {
type: String,
required: true,
},
lineColor: {
type: String,
required: true,
},
areaColor: {
type: String,
required: true,
},
},
};
</script>
<template>
<g>
<path
class="metric-area"
:d="generatedAreaPath"
:fill="areaColor"
transform="translate(-5, 20)">
</path>
<path
class="metric-line"
:d="generatedLinePath"
:stroke="lineColor"
fill="none"
stroke-width="1"
transform="translate(-5, 20)">
</path>
</g>
</template>
...@@ -21,9 +21,9 @@ const mixins = { ...@@ -21,9 +21,9 @@ const mixins = {
formatDeployments() { formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at); const time = new Date(deployment.created_at);
const xPos = Math.floor(this.xScale(time)); const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time));
time.setSeconds(this.data[0].time.getSeconds()); time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
if (xPos >= 0) { if (xPos >= 0) {
deploymentDataArray.push({ deploymentDataArray.push({
......
import _ from 'underscore'; import _ from 'underscore';
class MonitoringStore { function sortMetrics(metrics) {
return _.chain(metrics).sortBy('weight').sortBy('title').value();
}
function normalizeMetrics(metrics) {
return metrics.map(metric => ({
...metric,
queries: metric.queries.map(query => ({
...query,
result: query.result.map(result => ({
...result,
values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000),
value,
})),
})),
})),
}));
}
export default class MonitoringStore {
constructor() { constructor() {
this.groups = []; this.groups = [];
this.deploymentData = []; this.deploymentData = [];
} }
// eslint-disable-next-line class-methods-use-this
createArrayRows(metrics = []) {
const currentMetrics = metrics;
const availableMetrics = [];
let metricsRow = [];
let index = 1;
Object.keys(currentMetrics).forEach((key) => {
const metricValues = currentMetrics[key].queries[0].result[0].values;
if (metricValues != null) {
const literalMetrics = metricValues.map(metric => ({
time: new Date(metric[0] * 1000),
value: metric[1],
}));
currentMetrics[key].queries[0].result[0].values = literalMetrics;
metricsRow.push(currentMetrics[key]);
if (index % 2 === 0) {
availableMetrics.push(metricsRow);
metricsRow = [];
}
index = index += 1;
}
});
if (metricsRow.length > 0) {
availableMetrics.push(metricsRow);
}
return availableMetrics;
}
storeMetrics(groups = []) { storeMetrics(groups = []) {
this.groups = groups.map((group) => { this.groups = groups.map(group => ({
const currentGroup = group; ...group,
currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value(); metrics: normalizeMetrics(sortMetrics(group.metrics)),
currentGroup.metrics = this.createArrayRows(currentGroup.metrics); }));
return currentGroup;
});
} }
storeDeploymentData(deploymentData = []) { storeDeploymentData(deploymentData = []) {
...@@ -48,14 +38,6 @@ class MonitoringStore { ...@@ -48,14 +38,6 @@ class MonitoringStore {
} }
getMetricsCount() { getMetricsCount() {
let metricsCount = 0; return this.groups.reduce((count, group) => count + group.metrics.length, 0);
this.groups.forEach((group) => {
group.metrics.forEach((metric) => {
metricsCount = metricsCount += metric.length;
});
});
return metricsCount;
} }
} }
export default MonitoringStore;
...@@ -7,15 +7,15 @@ export default { ...@@ -7,15 +7,15 @@ export default {
left: 40, left: 40,
}, },
legends: { legends: {
width: 15, width: 10,
height: 25, height: 3,
}, },
backgroundLegend: { backgroundLegend: {
width: 30, width: 30,
height: 50, height: 50,
}, },
axisLabelLineOffset: -20, axisLabelLineOffset: -20,
legendOffset: 35, legendOffset: 33,
}, },
large: { // This covers both md and lg screen sizes large: { // This covers both md and lg screen sizes
margin: { margin: {
...@@ -25,15 +25,15 @@ export default { ...@@ -25,15 +25,15 @@ export default {
left: 80, left: 80,
}, },
legends: { legends: {
width: 20, width: 15,
height: 30, height: 3,
}, },
backgroundLegend: { backgroundLegend: {
width: 30, width: 30,
height: 150, height: 150,
}, },
axisLabelLineOffset: 20, axisLabelLineOffset: 20,
legendOffset: 38, legendOffset: 36,
}, },
xTicks: 8, xTicks: 8,
yTicks: 3, yTicks: 3,
......
import d3 from 'd3';
import _ from 'underscore';
export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) {
const maxValues = seriesData.map((timeSeries, index) => {
const maxValue = d3.max(timeSeries.values.map(d => d.value));
return {
maxValue,
index,
};
});
const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
let timeSeriesNumber = 1;
let lineColor = '#1f78d1';
let areaColor = '#8fbce8';
return seriesData.map((timeSeries) => {
const timeSeriesScaleX = d3.time.scale()
.range([0, graphWidth - 70]);
const timeSeriesScaleY = d3.scale.linear()
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
const lineFunction = d3.svg.line()
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area()
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
.y1(d => timeSeriesScaleY(d.value))
.interpolate('linear');
switch (timeSeriesNumber) {
case 1:
lineColor = '#1f78d1';
areaColor = '#8fbce8';
break;
case 2:
lineColor = '#fc9403';
areaColor = '#feca81';
break;
case 3:
lineColor = '#db3b21';
areaColor = '#ed9d90';
break;
case 4:
lineColor = '#1aaa55';
areaColor = '#8dd5aa';
break;
case 5:
lineColor = '#6666c4';
areaColor = '#d1d1f0';
break;
default:
lineColor = '#1f78d1';
areaColor = '#8fbce8';
break;
}
if (timeSeriesNumber <= 5) {
timeSeriesNumber = timeSeriesNumber += 1;
} else {
timeSeriesNumber = 1;
}
return {
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
values: timeSeries.values,
lineColor,
areaColor,
};
});
}
...@@ -63,7 +63,7 @@ export default class NewNavSidebar { ...@@ -63,7 +63,7 @@ export default class NewNavSidebar {
if (breakpoint === 'sm' || breakpoint === 'md') { if (breakpoint === 'sm' || breakpoint === 'md') {
this.toggleCollapsedSidebar(true); this.toggleCollapsedSidebar(true);
} else if (breakpoint === 'lg') { } else if (breakpoint === 'lg') {
const collapse = Cookies.get('sidebar_collapsed') === 'true'; const collapse = this.$sidebar.hasClass('sidebar-icons-only');
this.toggleCollapsedSidebar(collapse); this.toggleCollapsedSidebar(collapse);
} }
} }
......
...@@ -464,7 +464,6 @@ export default class Notes { ...@@ -464,7 +464,6 @@ export default class Notes {
} }
renderDiscussionAvatar(diffAvatarContainer, noteEntity) { renderDiscussionAvatar(diffAvatarContainer, noteEntity) {
var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
if (!avatarHolder.length) { if (!avatarHolder.length) {
...@@ -475,10 +474,6 @@ export default class Notes { ...@@ -475,10 +474,6 @@ export default class Notes {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
if (commentButton.length) {
commentButton.remove();
}
} }
/** /**
...@@ -767,6 +762,7 @@ export default class Notes { ...@@ -767,6 +762,7 @@ export default class Notes {
var $note, $notes; var $note, $notes;
$note = $(el); $note = $(el);
$notes = $note.closest('.discussion-notes'); $notes = $note.closest('.discussion-notes');
const discussionId = $('.notes', $notes).data('discussion-id');
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteElId]) { if (gl.diffNoteApps[noteElId]) {
...@@ -783,6 +779,8 @@ export default class Notes { ...@@ -783,6 +779,8 @@ export default class Notes {
// "Discussions" tab // "Discussions" tab
$notes.closest('.timeline-entry').remove(); $notes.closest('.timeline-entry').remove();
$(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
// The notes tr can contain multiple lists of notes, like on the parallel diff // The notes tr can contain multiple lists of notes, like on the parallel diff
if (notesTr.find('.discussion-notes').length > 1) { if (notesTr.find('.discussion-notes').length > 1) {
$notes.remove(); $notes.remove();
......
...@@ -53,10 +53,6 @@ import Cookies from 'js-cookie'; ...@@ -53,10 +53,6 @@ import Cookies from 'js-cookie';
return _this.changeProject($(e.currentTarget).val()); return _this.changeProject($(e.currentTarget).val());
}; };
})(this)); })(this));
return $('.js-projects-dropdown-toggle').on('click', function(e) {
e.preventDefault();
return $('.js-projects-dropdown').select2('open');
});
}; };
Project.prototype.changeProject = function(url) { Project.prototype.changeProject = function(url) {
......
...@@ -5,48 +5,6 @@ import ProjectSelectComboButton from './project_select_combo_button'; ...@@ -5,48 +5,6 @@ import ProjectSelectComboButton from './project_select_combo_button';
(function() { (function() {
this.ProjectSelect = (function() { this.ProjectSelect = (function() {
function ProjectSelect() { function ProjectSelect() {
$('.js-projects-dropdown-toggle').each(function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return $dropdown.glDropdown({
filterable: true,
filterRemote: true,
search: {
fields: ['name_with_namespace']
},
data: function(term, callback) {
var finalCallback, projectsCallback;
var orderBy = $dropdown.data('order-by');
finalCallback = function(projects) {
return callback(projects);
};
if (this.includeGroups) {
projectsCallback = function(projects) {
var groupsCallback;
groupsCallback = function(groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(term, {}, groupsCallback);
};
} else {
projectsCallback = finalCallback;
}
if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback);
} else {
return Api.projects(term, { order_by: orderBy }, projectsCallback);
}
},
url: function(project) {
return project.web_url;
},
text: function(project) {
return project.name_with_namespace;
}
});
});
$('.ajax-project-select').each(function(i, select) { $('.ajax-project-select').each(function(i, select) {
var placeholder; var placeholder;
this.groupId = $(select).data('group-id'); this.groupId = $(select).data('group-id');
......
<script>
import bs from '../../breakpoints';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import projectsListFrequent from './projects_list_frequent.vue';
import projectsListSearch from './projects_list_search.vue';
import search from './search.vue';
export default {
components: {
search,
loadingIcon,
projectsListFrequent,
projectsListSearch,
},
props: {
currentProject: {
type: Object,
required: true,
},
store: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
},
data() {
return {
isLoadingProjects: false,
isFrequentsListVisible: false,
isSearchListVisible: false,
isLocalStorageFailed: false,
isSearchFailed: false,
searchQuery: '',
};
},
computed: {
frequentProjects() {
return this.store.getFrequentProjects();
},
searchProjects() {
return this.store.getSearchedProjects();
},
},
methods: {
toggleFrequentProjectsList(state) {
this.isLoadingProjects = !state;
this.isSearchListVisible = !state;
this.isFrequentsListVisible = state;
},
toggleSearchProjectsList(state) {
this.isLoadingProjects = !state;
this.isFrequentsListVisible = !state;
this.isSearchListVisible = state;
},
toggleLoader(state) {
this.isFrequentsListVisible = !state;
this.isSearchListVisible = !state;
this.isLoadingProjects = state;
},
fetchFrequentProjects() {
const screenSize = bs.getBreakpointSize();
if (this.searchQuery && (screenSize !== 'sm' && screenSize !== 'xs')) {
this.toggleSearchProjectsList(true);
} else {
this.toggleLoader(true);
this.isLocalStorageFailed = false;
const projects = this.service.getFrequentProjects();
if (projects) {
this.toggleFrequentProjectsList(true);
this.store.setFrequentProjects(projects);
} else {
this.isLocalStorageFailed = true;
this.toggleFrequentProjectsList(true);
this.store.setFrequentProjects([]);
}
}
},
fetchSearchedProjects(searchQuery) {
this.searchQuery = searchQuery;
this.toggleLoader(true);
this.service.getSearchedProjects(this.searchQuery)
.then(res => res.json())
.then((results) => {
this.toggleSearchProjectsList(true);
this.store.setSearchedProjects(results);
})
.catch(() => {
this.isSearchFailed = true;
this.toggleSearchProjectsList(true);
});
},
logCurrentProjectAccess() {
this.service.logProjectAccess(this.currentProject);
},
handleSearchClear() {
this.searchQuery = '';
this.toggleFrequentProjectsList(true);
this.store.clearSearchedProjects();
},
handleSearchFailure() {
this.isSearchFailed = true;
this.toggleSearchProjectsList(true);
},
},
created() {
if (this.currentProject.id) {
this.logCurrentProjectAccess();
}
eventHub.$on('dropdownOpen', this.fetchFrequentProjects);
eventHub.$on('searchProjects', this.fetchSearchedProjects);
eventHub.$on('searchCleared', this.handleSearchClear);
eventHub.$on('searchFailed', this.handleSearchFailure);
},
beforeDestroy() {
eventHub.$off('dropdownOpen', this.fetchFrequentProjects);
eventHub.$off('searchProjects', this.fetchSearchedProjects);
eventHub.$off('searchCleared', this.handleSearchClear);
eventHub.$off('searchFailed', this.handleSearchFailure);
},
};
</script>
<template>
<div>
<search/>
<loading-icon
class="loading-animation prepend-top-20"
size="2"
v-if="isLoadingProjects"
:label="s__('ProjectsDropdown|Loading projects')"
/>
<div
class="section-header"
v-if="isFrequentsListVisible"
>
{{ s__('ProjectsDropdown|Frequently visited') }}
</div>
<projects-list-frequent
v-if="isFrequentsListVisible"
:local-storage-failed="isLocalStorageFailed"
:projects="frequentProjects"
/>
<projects-list-search
v-if="isSearchListVisible"
:search-failed="isSearchFailed"
:matcher="searchQuery"
:projects="searchProjects"
/>
</div>
</template>
<script>
import { s__ } from '../../locale';
import projectsListItem from './projects_list_item.vue';
export default {
components: {
projectsListItem,
},
props: {
projects: {
type: Array,
required: true,
},
localStorageFailed: {
type: Boolean,
required: true,
},
},
computed: {
isListEmpty() {
return this.projects.length === 0;
},
listEmptyMessage() {
return this.localStorageFailed ?
s__('ProjectsDropdown|This feature requires browser localStorage support') :
s__('ProjectsDropdown|Projects you visit often will appear here');
},
},
};
</script>
<template>
<div
class="projects-list-frequent-container"
>
<ul
class="list-unstyled"
>
<li
class="section-empty"
v-if="isListEmpty"
>
{{listEmptyMessage}}
</li>
<projects-list-item
v-else
v-for="(project, index) in projects"
:key="index"
:project-id="project.id"
:project-name="project.name"
:namespace="project.namespace"
:web-url="project.webUrl"
:avatar-url="project.avatarUrl"
/>
</ul>
</div>
</template>
<script>
import identicon from '../../vue_shared/components/identicon.vue';
export default {
components: {
identicon,
},
props: {
matcher: {
type: String,
required: false,
},
projectId: {
type: Number,
required: true,
},
projectName: {
type: String,
required: true,
},
namespace: {
type: String,
required: true,
},
webUrl: {
type: String,
required: true,
},
avatarUrl: {
required: true,
validator(value) {
return value === null || typeof value === 'string';
},
},
},
computed: {
hasAvatar() {
return this.avatarUrl !== null;
},
highlightedProjectName() {
if (this.matcher) {
const matcherRegEx = new RegExp(this.matcher, 'gi');
const matches = this.projectName.match(matcherRegEx);
if (matches && matches.length > 0) {
return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`);
}
}
return this.projectName;
},
},
};
</script>
<template>
<li
class="projects-list-item-container"
>
<a
class="clearfix"
:href="webUrl"
>
<div
class="project-item-avatar-container"
>
<img
v-if="hasAvatar"
class="avatar s32"
:src="avatarUrl"
/>
<identicon
v-else
size-class="s32"
:entity-id=projectId
:entity-name="projectName"
/>
</div>
<div
class="project-item-metadata-container"
>
<div
class="project-title"
:title="projectName"
v-html="highlightedProjectName"
>
</div>
<div
class="project-namespace"
:title="namespace"
>
{{namespace}}
</div>
</div>
</a>
</li>
</template>
<script>
import { s__ } from '../../locale';
import projectsListItem from './projects_list_item.vue';
export default {
components: {
projectsListItem,
},
props: {
matcher: {
type: String,
required: true,
},
projects: {
type: Array,
required: true,
},
searchFailed: {
type: Boolean,
required: true,
},
},
computed: {
isListEmpty() {
return this.projects.length === 0;
},
listEmptyMessage() {
return this.searchFailed ?
s__('ProjectsDropdown|Something went wrong on our end.') :
s__('ProjectsDropdown|No projects matched your query');
},
},
};
</script>
<template>
<div
class="projects-list-search-container"
>
<ul
class="list-unstyled"
>
<li
v-if="isListEmpty"
:class="{ 'section-failure': searchFailed }"
class="section-empty"
>
{{ listEmptyMessage }}
</li>
<projects-list-item
v-else
v-for="(project, index) in projects"
:key="index"
:project-id="project.id"
:project-name="project.name"
:namespace="project.namespace"
:web-url="project.webUrl"
:avatar-url="project.avatarUrl"
:matcher="matcher"
/>
</ul>
</div>
</template>
<script>
import _ from 'underscore';
import eventHub from '../event_hub';
export default {
data() {
return {
searchQuery: '',
};
},
watch: {
searchQuery() {
this.handleInput();
},
},
methods: {
setFocus() {
this.$refs.search.focus();
},
emitSearchEvents() {
if (this.searchQuery) {
eventHub.$emit('searchProjects', this.searchQuery);
} else {
eventHub.$emit('searchCleared');
}
},
/**
* Callback function within _.debounce is intentionally
* kept as ES5 `function() {}` instead of ES6 `() => {}`
* as it otherwise messes up function context
* and component reference is no longer accessible via `this`
*/
// eslint-disable-next-line func-names
handleInput: _.debounce(function () {
this.emitSearchEvents();
}, 500),
},
mounted() {
eventHub.$on('dropdownOpen', this.setFocus);
},
beforeDestroy() {
eventHub.$off('dropdownOpen', this.setFocus);
},
};
</script>
<template>
<div
class="search-input-container hidden-xs"
>
<input
type="search"
class="form-control"
ref="search"
v-model="searchQuery"
:placeholder="s__('ProjectsDropdown|Search projects')"
/>
<i
v-if="!searchQuery"
class="search-icon fa fa-fw fa-search"
aria-hidden="true"
/>
</div>
</template>
export const FREQUENT_PROJECTS = {
MAX_COUNT: 20,
LIST_COUNT_DESKTOP: 5,
LIST_COUNT_MOBILE: 3,
ELIGIBLE_FREQUENCY: 3,
};
export const HOUR_IN_MS = 3600000;
export const STORAGE_KEY = 'frequent-projects';
import Vue from 'vue';
export default new Vue();
import Vue from 'vue';
import Translate from '../vue_shared/translate';
import eventHub from './event_hub';
import ProjectsService from './service/projects_service';
import ProjectsStore from './store/projects_store';
import projectsDropdownApp from './components/app.vue';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('js-projects-dropdown');
const navEl = document.getElementById('nav-projects-dropdown');
// Don't do anything if element doesn't exist (No projects dropdown)
// This is for when the user accesses GitLab without logging in
if (!el || !navEl) {
return;
}
$(navEl).on('show.bs.dropdown', (e) => {
const dropdownEl = $(e.currentTarget).find('.projects-dropdown-menu');
dropdownEl.one('transitionend', () => {
eventHub.$emit('dropdownOpen');
});
});
// eslint-disable-next-line no-new
new Vue({
el,
components: {
projectsDropdownApp,
},
data() {
const dataset = this.$options.el.dataset;
const store = new ProjectsStore();
const service = new ProjectsService(dataset.userName);
const project = {
id: Number(dataset.projectId),
name: dataset.projectName,
namespace: dataset.projectNamespace,
webUrl: dataset.projectWebUrl,
avatarUrl: dataset.projectAvatarUrl || null,
lastAccessedOn: Date.now(),
};
return {
store,
service,
state: store.state,
currentUserName: dataset.userName,
currentProject: project,
};
},
render(createElement) {
return createElement('projects-dropdown-app', {
props: {
currentUserName: this.currentUserName,
currentProject: this.currentProject,
store: this.store,
service: this.service,
},
});
},
});
});
import Vue from 'vue';
import VueResource from 'vue-resource';
import bp from '../../breakpoints';
import Api from '../../api';
import AccessorUtilities from '../../lib/utils/accessor';
import { FREQUENT_PROJECTS, HOUR_IN_MS, STORAGE_KEY } from '../constants';
Vue.use(VueResource);
export default class ProjectsService {
constructor(currentUserName) {
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.currentUserName = currentUserName;
this.storageKey = `${this.currentUserName}/${STORAGE_KEY}`;
this.projectsPath = Vue.resource(Api.buildUrl(Api.projectsPath));
}
getSearchedProjects(searchQuery) {
return this.projectsPath.get({
simple: false,
per_page: 20,
membership: !!gon.current_user_id,
order_by: 'last_activity_at',
search: searchQuery,
});
}
getFrequentProjects() {
if (this.isLocalStorageAvailable) {
return this.getTopFrequentProjects();
}
return null;
}
logProjectAccess(project) {
let matchFound = false;
let storedFrequentProjects;
if (this.isLocalStorageAvailable) {
const storedRawProjects = localStorage.getItem(this.storageKey);
// Check if there's any frequent projects list set
if (!storedRawProjects) {
// No frequent projects list set, set one up.
storedFrequentProjects = [];
storedFrequentProjects.push({ ...project, frequency: 1 });
} else {
// Check if project is already present in frequents list
// When found, update metadata of it.
storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => {
if (projectItem.id === project.id) {
matchFound = true;
const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
const updatedProject = {
...project,
frequency: projectItem.frequency,
lastAccessedOn: projectItem.lastAccessedOn,
};
// Check if duration since last access of this project
// is over an hour
if (diff > 1) {
return {
...updatedProject,
frequency: updatedProject.frequency + 1,
lastAccessedOn: Date.now(),
};
}
return {
...updatedProject,
};
}
return projectItem;
});
// Check whether currently logged project is present in frequents list
if (!matchFound) {
// We always keep size of frequents collection to 20 projects
// out of which only 5 projects with
// highest value of `frequency` and most recent `lastAccessedOn`
// are shown in projects dropdown
if (storedFrequentProjects.length === FREQUENT_PROJECTS.MAX_COUNT) {
storedFrequentProjects.shift(); // Remove an item from head of array
}
storedFrequentProjects.push({ ...project, frequency: 1 });
}
}
localStorage.setItem(this.storageKey, JSON.stringify(storedFrequentProjects));
}
}
getTopFrequentProjects() {
const storedFrequentProjects = JSON.parse(localStorage.getItem(this.storageKey));
let frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_DESKTOP;
if (!storedFrequentProjects) {
return [];
}
if (bp.getBreakpointSize() === 'sm' ||
bp.getBreakpointSize() === 'xs') {
frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
}
const frequentProjects = storedFrequentProjects
.filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY);
// Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first
frequentProjects.sort((projectA, projectB) => {
if (projectA.frequency < projectB.frequency) {
return 1;
} else if (projectA.frequency > projectB.frequency) {
return -1;
} else if (projectA.lastAccessedOn < projectB.lastAccessedOn) {
return 1;
} else if (projectA.lastAccessedOn > projectB.lastAccessedOn) {
return -1;
}
return 0;
});
return _.first(frequentProjects, frequentProjectsCount);
}
}
export default class ProjectsStore {
constructor() {
this.state = {};
this.state.frequentProjects = [];
this.state.searchedProjects = [];
}
setFrequentProjects(rawProjects) {
this.state.frequentProjects = rawProjects;
}
getFrequentProjects() {
return this.state.frequentProjects;
}
setSearchedProjects(rawProjects) {
this.state.searchedProjects = rawProjects.map(rawProject => ({
id: rawProject.id,
name: rawProject.name,
namespace: rawProject.name_with_namespace,
webUrl: rawProject.web_url,
avatarUrl: rawProject.avatar_url,
}));
}
getSearchedProjects() {
return this.state.searchedProjects;
}
clearSearchedProjects() {
this.state.searchedProjects = [];
}
}
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import SidebarHeightManager from './sidebar_height_manager';
(function() { (function() {
this.Sidebar = (function() { this.Sidebar = (function() {
...@@ -23,7 +22,6 @@ import SidebarHeightManager from './sidebar_height_manager'; ...@@ -23,7 +22,6 @@ import SidebarHeightManager from './sidebar_height_manager';
}; };
Sidebar.prototype.addEventListeners = function() { Sidebar.prototype.addEventListeners = function() {
SidebarHeightManager.init();
const $document = $(document); const $document = $(document);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
......
import _ from 'underscore';
import Cookies from 'js-cookie';
export default {
init() {
if (!this.initialized) {
if (Cookies.get('new_nav') === 'true' && $('.js-issuable-sidebar').length) return;
this.$window = $(window);
this.$rightSidebar = $('.js-right-sidebar');
this.$navHeight = $('.navbar-gitlab').outerHeight() +
$('.layout-nav').outerHeight() +
$('.sub-nav-scroll').outerHeight();
const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20);
const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200);
this.$window.on('scroll', throttledSetSidebarHeight);
this.$window.on('resize', debouncedSetSidebarHeight);
this.initialized = true;
}
},
setSidebarHeight() {
const currentScrollDepth = window.pageYOffset || 0;
const diff = this.$navHeight - currentScrollDepth;
if (diff > 0) {
const newSidebarHeight = window.innerHeight - diff;
this.$rightSidebar.outerHeight(newSidebarHeight);
this.sidebarHeightIsCustom = true;
} else if (this.sidebarHeightIsCustom) {
this.$rightSidebar.outerHeight('100%');
this.sidebarHeightIsCustom = false;
}
},
};
...@@ -75,18 +75,20 @@ export default { ...@@ -75,18 +75,20 @@ export default {
class="btn btn-small inline"> class="btn btn-small inline">
Check out branch Check out branch
</a> </a>
<span class="dropdown inline prepend-left-10"> <span class="dropdown prepend-left-10">
<a <a
class="btn btn-xs dropdown-toggle" class="btn btn-small inline dropdown-toggle"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Download as" aria-label="Download as"
role="button"> role="button">
<i <i
class="fa fa-download" class="fa fa-download"
aria-hidden="true" /> aria-hidden="true">
</i>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true" /> aria-hidden="true">
</i>
</a> </a>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li> <li>
......
...@@ -9,6 +9,11 @@ export default { ...@@ -9,6 +9,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
sizeClass: {
type: String,
required: false,
default: 's40',
},
}, },
computed: { computed: {
/** /**
...@@ -38,7 +43,8 @@ export default { ...@@ -38,7 +43,8 @@ export default {
<template> <template>
<div <div
class="avatar s40 identicon" class="avatar identicon"
:class="sizeClass"
:style="identiconStyles"> :style="identiconStyles">
{{identiconTitle}} {{identiconTitle}}
</div> </div>
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
.append-right-default { margin-right: $gl-padding; } .append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; } .append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; } .append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-10 { margin-bottom: 10px; } .append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; } .append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; } .append-bottom-20 { margin-bottom: 20px; }
......
...@@ -737,6 +737,8 @@ ...@@ -737,6 +737,8 @@
@mixin new-style-dropdown($selector: '') { @mixin new-style-dropdown($selector: '') {
#{$selector}.dropdown-menu, #{$selector}.dropdown-menu,
#{$selector}.dropdown-menu-nav { #{$selector}.dropdown-menu-nav {
margin-bottom: 24px;
li { li {
display: block; display: block;
padding: 0 1px; padding: 0 1px;
...@@ -764,11 +766,12 @@ ...@@ -764,11 +766,12 @@
box-shadow: none; box-shadow: none;
padding: 8px 16px; padding: 8px 16px;
text-align: left; text-align: left;
white-space: normal;
width: 100%; width: 100%;
// make sure the text color is not overriden // make sure the text color is not overriden
&.text-danger { &.text-danger {
@extend .text-danger; color: $brand-danger;
} }
&.is-focused, &.is-focused,
...@@ -777,6 +780,11 @@ ...@@ -777,6 +780,11 @@
&:focus { &:focus {
background-color: $dropdown-item-hover-bg; background-color: $dropdown-item-hover-bg;
color: $gl-text-color; color: $gl-text-color;
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
} }
&.is-active { &.is-active {
...@@ -821,4 +829,154 @@ ...@@ -821,4 +829,154 @@
} }
} }
@include new-style-dropdown('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + '); @include new-style-dropdown('.js-namespace-select + ');
header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu {
padding: 0;
@media (max-width: $screen-xs-max) {
display: table;
left: -50px;
min-width: 300px;
}
}
.projects-dropdown-container {
display: flex;
flex-direction: row;
width: 500px;
height: 334px;
.project-dropdown-sidebar,
.project-dropdown-content {
padding: 8px 0;
}
.loading-animation {
color: $almost-black;
}
.project-dropdown-sidebar {
width: 30%;
border-right: 1px solid $border-color;
}
.project-dropdown-content {
position: relative;
width: 70%;
}
@media (max-width: $screen-xs-max) {
flex-direction: column;
width: 100%;
height: auto;
flex: 1;
.project-dropdown-sidebar,
.project-dropdown-content {
width: 100%;
}
.project-dropdown-sidebar {
border-bottom: 1px solid $border-color;
border-right: 0;
}
}
}
.projects-dropdown-container {
.projects-list-frequent-container,
.projects-list-search-container, {
padding: 8px 0;
overflow-y: auto;
}
.section-header,
.projects-list-frequent-container li.section-empty,
.projects-list-search-container li.section-empty {
padding: 0 15px;
}
.section-header,
.projects-list-frequent-container li.section-empty,
.projects-list-search-container li.section-empty {
color: $gl-text-color-secondary;
font-size: $gl-font-size;
}
.projects-list-frequent-container,
.projects-list-search-container {
li.section-empty.section-failure {
color: $callout-danger-color;
}
}
.search-input-container {
position: relative;
padding: 4px $gl-padding;
.search-icon {
position: absolute;
top: 13px;
right: 25px;
color: $md-area-border;
}
}
.section-header {
font-weight: 700;
margin-top: 8px;
}
.projects-list-search-container {
height: 284px;
}
@media (max-width: $screen-xs-max) {
.projects-list-frequent-container {
width: auto;
height: auto;
padding-bottom: 0;
}
}
}
.projects-list-item-container {
.project-item-avatar-container
.project-item-metadata-container {
float: left;
}
.project-title,
.project-namespace {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
.project-item-avatar-container .avatar {
border-color: $md-area-border;
}
}
.project-title {
font-size: $gl-font-size;
font-weight: 400;
line-height: 16px;
}
.project-namespace {
margin-top: 4px;
font-size: 12px;
line-height: 12px;
color: $gl-text-color-secondary;
}
@media (max-width: $screen-xs-max) {
.project-item-metadata-container {
float: none;
}
}
}
...@@ -105,12 +105,11 @@ header { ...@@ -105,12 +105,11 @@ header {
top: -3px; top: -3px;
font-size: 10px; font-size: 10px;
} }
}
.user-counter {
svg { svg {
position: relative; height: 16px;
top: 2px;
height: 17px;
// hack to get SVG to line up with FA icons
width: 23px; width: 23px;
fill: currentColor; fill: currentColor;
} }
...@@ -325,12 +324,12 @@ header { ...@@ -325,12 +324,12 @@ header {
li { li {
.badge { .badge {
position: inherit; position: inherit;
top: -8px;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
margin-left: -11px; margin-left: -6px;
font-size: 11px; font-size: 11px;
color: $white-light; color: $white-light;
padding: 1px 5px 2px; padding: 0 5px;
line-height: 12px;
border-radius: 7px; border-radius: 7px;
box-shadow: 0 1px 0 rgba($gl-header-color, .2); box-shadow: 0 1px 0 rgba($gl-header-color, .2);
......
...@@ -267,14 +267,26 @@ ...@@ -267,14 +267,26 @@
// TODO: change global style // TODO: change global style
.ajax-project-dropdown, .ajax-project-dropdown,
.ajax-users-dropdown,
body[data-page="projects:edit"] #select2-drop,
body[data-page="projects:new"] #select2-drop, body[data-page="projects:new"] #select2-drop,
body[data-page="projects:merge_requests:edit"] #select2-drop,
body[data-page="projects:blob:new"] #select2-drop, body[data-page="projects:blob:new"] #select2-drop,
body[data-page="profiles:show"] #select2-drop, body[data-page="profiles:show"] #select2-drop,
body[data-page="admin:groups:show"] #select2-drop,
body[data-page="projects:issues:show"] #select2-drop,
body[data-page="projects:blob:edit"] #select2-drop { body[data-page="projects:blob:edit"] #select2-drop {
&.select2-drop { &.select2-drop {
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
color: $gl-text-color; color: $gl-text-color;
} }
&.select2-drop-above {
border-top: none;
margin-top: -4px;
}
.select2-results { .select2-results {
.select2-no-results, .select2-no-results,
.select2-searching, .select2-searching,
......
...@@ -177,13 +177,14 @@ $row-hover: $blue-25; ...@@ -177,13 +177,14 @@ $row-hover: $blue-25;
$row-hover-border: $blue-100; $row-hover-border: $blue-100;
$progress-color: #c0392b; $progress-color: #c0392b;
$header-height: 50px; $header-height: 50px;
$new-navbar-height: 40px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$limited-layout-width: 990px; $limited-layout-width: 990px;
$limited-layout-width-sm: 790px; $limited-layout-width-sm: 790px;
$container-text-max-width: 540px; $container-text-max-width: 540px;
$gl-avatar-size: 40px; $gl-avatar-size: 40px;
$error-exclamation-point: $red-500; $error-exclamation-point: $red-500;
$border-radius-default: 3px; $border-radius-default: 4px;
$settings-icon-size: 18px; $settings-icon-size: 18px;
$provider-btn-not-active-color: $blue-500; $provider-btn-not-active-color: $blue-500;
$link-underline-blue: $blue-500; $link-underline-blue: $blue-500;
......
This diff is collapsed.
...@@ -26,7 +26,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -26,7 +26,7 @@ $new-sidebar-collapsed-width: 50px;
// Override position: absolute // Override position: absolute
.right-sidebar { .right-sidebar {
position: fixed; position: fixed;
height: calc(100% - #{$header-height}); height: calc(100% - #{$new-navbar-height});
} }
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
...@@ -45,7 +45,6 @@ $new-sidebar-collapsed-width: 50px; ...@@ -45,7 +45,6 @@ $new-sidebar-collapsed-width: 50px;
margin-right: 2px; margin-right: 2px;
a { a {
border-bottom: 1px solid $border-color;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -93,7 +92,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -93,7 +92,7 @@ $new-sidebar-collapsed-width: 50px;
z-index: 400; z-index: 400;
width: $new-sidebar-width; width: $new-sidebar-width;
transition: left $sidebar-transition-duration; transition: left $sidebar-transition-duration;
top: $header-height; top: $new-navbar-height;
bottom: 0; bottom: 0;
left: 0; left: 0;
background-color: $gray-normal; background-color: $gray-normal;
...@@ -189,7 +188,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -189,7 +188,7 @@ $new-sidebar-collapsed-width: 50px;
} }
.with-performance-bar .nav-sidebar { .with-performance-bar .nav-sidebar {
top: $header-height + $performance-bar-height; top: $new-navbar-height + $performance-bar-height;
} }
.sidebar-sub-level-items { .sidebar-sub-level-items {
...@@ -453,7 +452,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -453,7 +452,7 @@ $new-sidebar-collapsed-width: 50px;
// Make issue boards full-height now that sub-nav is gone // Make issue boards full-height now that sub-nav is gone
.boards-list { .boards-list {
height: calc(100vh - #{$header-height}); height: calc(100vh - #{$new-navbar-height});
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
...@@ -464,7 +463,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -464,7 +463,7 @@ $new-sidebar-collapsed-width: 50px;
} }
.with-performance-bar .boards-list { .with-performance-bar .boards-list {
height: calc(100vh - #{$header-height} - #{$performance-bar-height}); height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height});
} }
......
...@@ -440,6 +440,7 @@ ...@@ -440,6 +440,7 @@
&.right-sidebar { &.right-sidebar {
top: 0; top: 0;
bottom: 0; bottom: 0;
height: 100%;
} }
.issuable-sidebar-header { .issuable-sidebar-header {
......
...@@ -141,17 +141,17 @@ ...@@ -141,17 +141,17 @@
display: inline-block; display: inline-block;
background: $white-light; background: $white-light;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
padding: 0 5px; padding: 0 4px;
cursor: pointer; cursor: pointer;
border: 1px solid $border-gray-dark; border: 1px solid $border-gray-dark;
border-radius: $border-radius-default; border-radius: $border-radius-default;
margin-left: 5px; margin-left: 5px;
font-size: $gl-font-size; font-size: 12px;
line-height: $gl-font-size; line-height: $gl-font-size;
outline: none; outline: none;
&.open { &.open {
background: $gray-light; background-color: darken($gray-light, 10%);
box-shadow: inset 0 0 2px rgba($black, 0.2); box-shadow: inset 0 0 2px rgba($black, 0.2);
} }
......
...@@ -169,7 +169,7 @@ ...@@ -169,7 +169,7 @@
} }
.metric-area { .metric-area {
opacity: 0.8; opacity: 0.25;
} }
.prometheus-graph-overlay { .prometheus-graph-overlay {
...@@ -227,6 +227,26 @@ ...@@ -227,6 +227,26 @@
margin-top: 20px; margin-top: 20px;
} }
.prometheus-graph-group {
display: flex;
flex-wrap: wrap;
padding: $gl-padding / 2;
}
.prometheus-graph {
flex: 1 0 auto;
min-width: 450px;
padding: $gl-padding / 2;
h5 {
font-size: 16px;
}
@media (max-width: $screen-sm-max) {
min-width: 100%;
}
}
.prometheus-svg-container { .prometheus-svg-container {
position: relative; position: relative;
height: 0; height: 0;
...@@ -251,8 +271,14 @@ ...@@ -251,8 +271,14 @@
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
.label-axis-text, .label-axis-text {
.text-metric-usage { fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 10px;
}
.text-metric-usage,
.legend-metric-title {
fill: $black; fill: $black;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
font-size: 12px; font-size: 12px;
...@@ -291,9 +317,3 @@ ...@@ -291,9 +317,3 @@
} }
} }
} }
.prometheus-row {
h5 {
font-size: 16px;
}
}
...@@ -617,6 +617,8 @@ ...@@ -617,6 +617,8 @@
} }
.issuable-actions { .issuable-actions {
@include new-style-dropdown;
padding-top: 10px; padding-top: 10px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
......
...@@ -143,8 +143,12 @@ ul.related-merge-requests > li { ...@@ -143,8 +143,12 @@ ul.related-merge-requests > li {
} }
} }
.issue-form .select2-container { .issue-form {
width: 250px !important; @include new-style-dropdown;
.select2-container {
width: 250px !important;
}
} }
.issues-footer { .issues-footer {
......
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
.new-note, .new-note,
.note-edit-form { .note-edit-form {
.note-form-actions { .note-form-actions {
@include new-style-dropdown;
position: relative; position: relative;
margin: $gl-padding 0 0; margin: $gl-padding 0 0;
} }
......
...@@ -800,8 +800,10 @@ pre.light-well { ...@@ -800,8 +800,10 @@ pre.light-well {
} }
} }
.new_protected_branch, .new-protected-branch,
.new-protected-tag { .new-protected-tag {
@include new-style-dropdown;
label { label {
margin-top: 6px; margin-top: 6px;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
...@@ -821,19 +823,9 @@ pre.light-well { ...@@ -821,19 +823,9 @@ pre.light-well {
.protected-branches-list, .protected-branches-list,
.protected-tags-list { .protected-tags-list {
margin-bottom: 30px; @include new-style-dropdown;
a {
color: $gl-text-color;
&:hover {
color: $gl-link-color;
}
&.is-active { margin-bottom: 30px;
font-weight: $gl-font-weight-bold;
}
}
.settings-message { .settings-message {
margin: 0; margin: 0;
......
...@@ -190,6 +190,8 @@ input[type="checkbox"]:hover { ...@@ -190,6 +190,8 @@ input[type="checkbox"]:hover {
} }
.search-holder { .search-holder {
@include new-style-dropdown;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
......
...@@ -36,6 +36,34 @@ module IssuableCollections ...@@ -36,6 +36,34 @@ module IssuableCollections
@merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder) @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder)
end end
def redirect_out_of_range(relation, total_pages)
return false if total_pages.zero?
out_of_range = relation.current_page > total_pages
if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
end
out_of_range
end
def issues_page_count(relation)
page_count_for_relation(relation, issues_finder.row_count)
end
def merge_requests_page_count(relation)
page_count_for_relation(relation, merge_requests_finder.row_count)
end
def page_count_for_relation(relation, row_count)
limit = relation.limit_value.to_f
return 1 if limit.zero?
(row_count.to_f / limit).ceil
end
def issuable_finder_for(finder_class) def issuable_finder_for(finder_class)
finder_class.new(current_user, filter_params) finder_class.new(current_user, filter_params)
end end
......
...@@ -27,10 +27,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -27,10 +27,9 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = issues_collection @issues = issues_collection
@issues = @issues.page(params[:page]) @issues = @issues.page(params[:page])
@issuable_meta_data = issuable_meta_data(@issues, @collection_type) @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
@total_pages = issues_page_count(@issues)
if @issues.out_of_range? && @issues.total_pages != 0 return if redirect_out_of_range(@issues, @total_pages)
return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true))
end
if params[:label_name].present? if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
......
...@@ -18,10 +18,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -18,10 +18,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(merge_request_diff: :merge_request) @merge_requests = @merge_requests.preload(merge_request_diff: :merge_request)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
@total_pages = merge_requests_page_count(@merge_requests)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 return if redirect_out_of_range(@merge_requests, @total_pages)
return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true))
end
if params[:label_name].present? if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] } labels_params = { project_id: @project.id, title: params[:label_name] }
......
...@@ -323,6 +323,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -323,6 +323,7 @@ class ProjectsController < Projects::ApplicationController
:build_allow_git_fetch, :build_allow_git_fetch,
:build_coverage_regex, :build_coverage_regex,
:build_timeout_in_minutes, :build_timeout_in_minutes,
:resolve_outdated_diff_discussions,
:container_registry_enabled, :container_registry_enabled,
:default_branch, :default_branch,
:description, :description,
......
...@@ -61,6 +61,10 @@ class IssuableFinder ...@@ -61,6 +61,10 @@ class IssuableFinder
execute.find_by(*params) execute.find_by(*params)
end end
def row_count
Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state])
end
# We often get counts for each state by running a query per state, and # We often get counts for each state by running a query per state, and
# counting those results. This is typically slower than running one query # counting those results. This is typically slower than running one query
# (even if that query is slower than any of the individual state queries) and # (even if that query is slower than any of the individual state queries) and
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
# search: string # search: string
# label_name: string # label_name: string
# sort: string # sort: string
# my_reaction_emoji: string
# #
class IssuesFinder < IssuableFinder class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# label_name: string # label_name: string
# sort: string # sort: string
# non_archived: boolean # non_archived: boolean
# my_reaction_emoji: string
# #
class MergeRequestsFinder < IssuableFinder class MergeRequestsFinder < IssuableFinder
def klass def klass
......
...@@ -302,10 +302,6 @@ module ApplicationHelper ...@@ -302,10 +302,6 @@ module ApplicationHelper
end end
end end
def show_new_nav?
true
end
def collapsed_sidebar? def collapsed_sidebar?
cookies["sidebar_collapsed"] == "true" cookies["sidebar_collapsed"] == "true"
end end
......
...@@ -22,4 +22,16 @@ module BreadcrumbsHelper ...@@ -22,4 +22,16 @@ module BreadcrumbsHelper
@breadcrumb_title = title @breadcrumb_title = title
end end
def breadcrumb_list_item(link)
content_tag "li" do
link + icon("angle-right", class: "breadcrumbs-list-angle")
end
end
def add_to_breadcrumb_dropdown(link, location: :before)
@breadcrumb_dropdown_links ||= {}
@breadcrumb_dropdown_links[location] ||= []
@breadcrumb_dropdown_links[location] << link
end
end end
...@@ -15,18 +15,20 @@ module GroupsHelper ...@@ -15,18 +15,20 @@ module GroupsHelper
@has_group_title = true @has_group_title = true
full_title = '' full_title = ''
group.ancestors.reverse.each do |parent| group.ancestors.reverse.each_with_index do |parent, index|
full_title += group_title_link(parent, hidable: true) if index > 0
add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true), location: :before)
full_title += '<span class="hidable"> / </span>'.html_safe else
full_title += breadcrumb_list_item group_title_link(parent, hidable: false)
end
end end
full_title += group_title_link(group) full_title += render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups")
full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name
content_tag :span, class: 'group-title' do full_title += breadcrumb_list_item group_title_link(group)
full_title.html_safe full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path breadcrumb-item-text js-breadcrumb-item-text') if name
end
full_title.html_safe
end end
def projects_lfs_status(group) def projects_lfs_status(group)
...@@ -65,11 +67,11 @@ module GroupsHelper ...@@ -65,11 +67,11 @@ module GroupsHelper
private private
def group_title_link(group, hidable: false) def group_title_link(group, hidable: false, show_avatar: false)
link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do link_to(group_path(group), class: "group-path breadcrumb-item-text js-breadcrumb-item-text #{'hidable' if hidable}") do
output = output =
if show_new_nav? && !Rails.env.test? if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16) image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15)
else else
"" ""
end end
......
...@@ -126,12 +126,8 @@ module IssuablesHelper ...@@ -126,12 +126,8 @@ module IssuablesHelper
end end
def issuable_meta(issuable, project, text) def issuable_meta(issuable, project, text)
output = content_tag(:strong, class: "identifier") do output = ""
concat("#{text} ") output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
concat(to_url_reference(issuable))
end
output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true) author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg") author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
...@@ -141,7 +137,7 @@ module IssuablesHelper ...@@ -141,7 +137,7 @@ module IssuablesHelper
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm") output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm")
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg") output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg")
output output.html_safe
end end
def issuable_todo(issuable) def issuable_todo(issuable)
...@@ -240,7 +236,8 @@ module IssuablesHelper ...@@ -240,7 +236,8 @@ module IssuablesHelper
def issuables_count_for_state(issuable_type, state) def issuables_count_for_state(issuable_type, state)
finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend
finder.count_by_state[state]
Gitlab::IssuablesCountForState.new(finder)[state]
end end
def close_issuable_url(issuable) def close_issuable_url(issuable)
......
module NavHelper module NavHelper
def page_with_sidebar_class def page_with_sidebar_class
class_name = page_gutter_class class_name = page_gutter_class
class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar class_name << 'page-with-new-sidebar' if defined?(@left_sidebar) && @left_sidebar
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @new_sidebar class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
class_name class_name
end end
...@@ -30,24 +30,15 @@ module NavHelper ...@@ -30,24 +30,15 @@ module NavHelper
end end
end end
def nav_header_class def nav_control_class
class_names = [] "nav-control" if current_user
class_names << 'with-horizontal-nav' if defined?(nav) && nav
class_names
end end
def layout_nav_class def user_dropdown_class
return [] if show_new_nav?
class_names = [] class_names = []
class_names << 'page-with-layout-nav' if defined?(nav) && nav class_names << 'header-user-dropdown-toggle'
class_names << 'page-with-sub-nav' if content_for?(:sub_nav) class_names << 'impersonated-user' if session[:impersonator_id]
class_names class_names
end end
def nav_control_class
"nav-control" if current_user
end
end end
...@@ -146,4 +146,8 @@ module NotesHelper ...@@ -146,4 +146,8 @@ module NotesHelper
autocomplete: autocomplete autocomplete: autocomplete
} }
end end
def discussion_resolved_intro(discussion)
discussion.resolved_by_push? ? 'Automatically resolved' : 'Resolved'
end
end end
...@@ -4,7 +4,7 @@ module PageLayoutHelper ...@@ -4,7 +4,7 @@ module PageLayoutHelper
@page_title.push(*titles.compact) if titles.any? @page_title.push(*titles.compact) if titles.any?
if show_new_nav? && titles.any? && !defined?(@breadcrumb_title) if titles.any? && !defined?(@breadcrumb_title)
@breadcrumb_title = @page_title.last @breadcrumb_title = @page_title.last
end end
...@@ -80,7 +80,9 @@ module PageLayoutHelper ...@@ -80,7 +80,9 @@ module PageLayoutHelper
@header_title = title @header_title = title
@header_title_url = title_url @header_title_url = title_url
else else
@header_title_url ? link_to(@header_title, @header_title_url) : @header_title return @header_title unless @header_title_url
breadcrumb_list_item(link_to(@header_title, @header_title_url))
end end
end end
......
...@@ -54,31 +54,28 @@ module ProjectsHelper ...@@ -54,31 +54,28 @@ module ProjectsHelper
def project_title(project) def project_title(project)
namespace_link = namespace_link =
if project.group if project.group
group_title(project.group) group_title(project.group, nil, nil)
else else
owner = project.namespace.owner owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner)) link_to(simple_sanitize(owner.name), user_path(owner))
end end
project_link = link_to project_path(project), { class: "project-item-select-holder" } do project_link = link_to project_path(project) do
output = output =
if show_new_nav? && !Rails.env.test? if project.avatar_url && !Rails.env.test?
project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16) project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15)
else else
"" ""
end end
output << simple_sanitize(project.name) output << content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text")
output.html_safe output.html_safe
end end
if current_user namespace_link = breadcrumb_list_item(namespace_link) unless project.group
project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do project_link = breadcrumb_list_item project_link
icon("chevron-down")
end
end
"#{namespace_link} / #{project_link}".html_safe "#{namespace_link} #{project_link}".html_safe
end end
def remove_project_message(project) def remove_project_message(project)
......
...@@ -10,6 +10,7 @@ module SearchHelper ...@@ -10,6 +10,7 @@ module SearchHelper
search_pattern = Regexp.new(Regexp.escape(term), "i") search_pattern = Regexp.new(Regexp.escape(term), "i")
generic_results = project_autocomplete + default_autocomplete + help_autocomplete generic_results = project_autocomplete + default_autocomplete + help_autocomplete
generic_results.concat(default_autocomplete_admin) if current_user.admin?
generic_results.select! { |result| result[:label] =~ search_pattern } generic_results.select! { |result| result[:label] =~ search_pattern }
[ [
...@@ -41,8 +42,14 @@ module SearchHelper ...@@ -41,8 +42,14 @@ module SearchHelper
[ [
{ category: "Settings", label: "User settings", url: profile_path }, { category: "Settings", label: "User settings", url: profile_path },
{ category: "Settings", label: "SSH Keys", url: profile_keys_path }, { category: "Settings", label: "SSH Keys", url: profile_keys_path },
{ category: "Settings", label: "Dashboard", url: root_path }, { category: "Settings", label: "Dashboard", url: root_path }
{ category: "Settings", label: "Admin Section", url: admin_root_path } ]
end
# Autocomplete results for settings pages, for admins
def default_autocomplete_admin
[
{ category: "Settings", label: "Admin Section", url: admin_root_path }
] ]
end end
......
...@@ -10,4 +10,15 @@ module WikiHelper ...@@ -10,4 +10,15 @@ module WikiHelper
.map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize } .map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }
.join(' / ') .join(' / ')
end end
def wiki_breadcrumb_dropdown_links(page_slug)
page_slug_split = page_slug.split('/')
page_slug_split.pop(1)
current_slug = ""
page_slug_split
.map do |dir_or_page|
current_slug = "#{current_slug}#{dir_or_page}/"
add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, project_wiki_path(@project, current_slug)), location: :after
end
end
end end
...@@ -451,6 +451,10 @@ module Ci ...@@ -451,6 +451,10 @@ module Ci
trace trace
end end
def serializable_hash(options = {})
super(options).merge(when: read_attribute(:when))
end
private private
def update_artifacts_size def update_artifacts_size
......
...@@ -6,6 +6,10 @@ module Ci ...@@ -6,6 +6,10 @@ module Ci
belongs_to :pipeline, foreign_key: :commit_id belongs_to :pipeline, foreign_key: :commit_id
has_many :builds has_many :builds
# We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables.
# Ci::TriggerRequest doesn't save variables anymore.
validates :variables, absence: true
serialize :variables # rubocop:disable Cop/ActiveRecordSerialize serialize :variables # rubocop:disable Cop/ActiveRecordSerialize
def user_variables def user_variables
......
...@@ -405,6 +405,6 @@ class Commit ...@@ -405,6 +405,6 @@ class Commit
end end
def gpg_commit def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self) @gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end end
end end
...@@ -38,6 +38,14 @@ class CommitStatus < ActiveRecord::Base ...@@ -38,6 +38,14 @@ class CommitStatus < ActiveRecord::Base
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) } scope :after_stage, -> (index) { where('stage_idx > ?', index) }
enum failure_reason: {
unknown_failure: nil,
script_failure: 1,
api_failure: 2,
stuck_or_timeout_failure: 3,
runner_system_failure: 4
}
state_machine :status do state_machine :status do
event :process do event :process do
transition [:skipped, :manual] => :created transition [:skipped, :manual] => :created
...@@ -79,6 +87,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -79,6 +87,11 @@ class CommitStatus < ActiveRecord::Base
commit_status.finished_at = Time.now commit_status.finished_at = Time.now
end end
before_transition any => :failed do |commit_status, transition|
failure_reason = transition.args.first
commit_status.failure_reason = failure_reason
end
after_transition do |commit_status, transition| after_transition do |commit_status, transition|
next if transition.loopback? next if transition.loopback?
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
# #
module Issuable module Issuable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Gitlab::SQL::Pattern
include CacheMarkdownField include CacheMarkdownField
include Participable include Participable
include Mentionable include Mentionable
...@@ -122,7 +123,9 @@ module Issuable ...@@ -122,7 +123,9 @@ module Issuable
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search(query) def search(query)
where(arel_table[:title].matches("%#{query}%")) title = to_fuzzy_arel(:title, query)
where(title)
end end
# Searches for records with a matching title or description. # Searches for records with a matching title or description.
...@@ -133,10 +136,10 @@ module Issuable ...@@ -133,10 +136,10 @@ module Issuable
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def full_search(query) def full_search(query)
t = arel_table title = to_fuzzy_arel(:title, query)
pattern = "%#{query}%" description = to_fuzzy_arel(:description, query)
where(t[:title].matches(pattern).or(t[:description].matches(pattern))) where(title&.or(description))
end end
def sort(method, excluded_labels: []) def sort(method, excluded_labels: [])
......
...@@ -24,6 +24,7 @@ module ResolvableDiscussion ...@@ -24,6 +24,7 @@ module ResolvableDiscussion
delegate :resolved_at, delegate :resolved_at,
:resolved_by, :resolved_by,
:resolved_by_push?,
to: :last_resolved_note, to: :last_resolved_note,
allow_nil: true allow_nil: true
......
...@@ -51,22 +51,34 @@ module ResolvableNote ...@@ -51,22 +51,34 @@ module ResolvableNote
end end
# If you update this method remember to also update `.resolve!` # If you update this method remember to also update `.resolve!`
def resolve!(current_user) def resolve_without_save(current_user, resolved_by_push: false)
return unless resolvable? return false unless resolvable?
return if resolved? return false if resolved?
self.resolved_at = Time.now self.resolved_at = Time.now
self.resolved_by = current_user self.resolved_by = current_user
save! self.resolved_by_push = resolved_by_push
true
end end
# If you update this method remember to also update `.unresolve!` # If you update this method remember to also update `.unresolve!`
def unresolve! def unresolve_without_save
return unless resolvable? return false unless resolvable?
return unless resolved? return false unless resolved?
self.resolved_at = nil self.resolved_at = nil
self.resolved_by = nil self.resolved_by = nil
save!
true
end
def resolve!(current_user, resolved_by_push: false)
resolve_without_save(current_user, resolved_by_push: resolved_by_push) &&
save!
end
def unresolve!
unresolve_without_save && save!
end end
end end
...@@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base ...@@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base
def verified_user_infos def verified_user_infos
user_infos.select do |user_info| user_infos.select do |user_info|
user_info[:email] == user.email user.verified_email?(user_info[:email])
end end
end end
...@@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base ...@@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base
user_infos.map do |user_info| user_infos.map do |user_info|
[ [
user_info[:email], user_info[:email],
user_info[:email] == user.email user.verified_email?(user_info[:email])
] ]
end.to_h end.to_h
end end
def verified? def verified?
emails_with_verified_status.any? { |_email, verified| verified } emails_with_verified_status.values.any?
end
def verified_and_belongs_to_email?(email)
emails_with_verified_status.fetch(email, false)
end end
def update_invalid_gpg_signatures def update_invalid_gpg_signatures
...@@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base ...@@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base
end end
def revoke def revoke
GpgSignature.where(gpg_key: self, valid_signature: true).update_all( GpgSignature
gpg_key_id: nil, .where(gpg_key: self)
valid_signature: false, .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
updated_at: Time.zone.now .update_all(
) gpg_key_id: nil,
verification_status: GpgSignature.verification_statuses[:unknown_key],
updated_at: Time.zone.now
)
destroy destroy
end end
......
class GpgSignature < ActiveRecord::Base class GpgSignature < ActiveRecord::Base
include ShaAttribute include ShaAttribute
include IgnorableColumn
ignore_column :valid_signature
sha_attribute :commit_sha sha_attribute :commit_sha
sha_attribute :gpg_key_primary_keyid sha_attribute :gpg_key_primary_keyid
enum verification_status: {
unverified: 0,
verified: 1,
same_user_different_email: 2,
other_user: 3,
unverified_key: 4,
unknown_key: 5
}
belongs_to :project belongs_to :project
belongs_to :gpg_key belongs_to :gpg_key
...@@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base ...@@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base
end end
def gpg_commit def gpg_commit
Gitlab::Gpg::Commit.new(project, commit_sha) Gitlab::Gpg::Commit.new(commit)
end end
end end
...@@ -16,6 +16,7 @@ class Group < Namespace ...@@ -16,6 +16,7 @@ class Group < Namespace
source: :user source: :user
has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
...@@ -126,20 +126,11 @@ class Member < ActiveRecord::Base ...@@ -126,20 +126,11 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token) find_by(invite_token: invite_token)
end end
def add_user(source, user, access_level, current_user: nil, expires_at: nil) def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil)
user = retrieve_user(user) # `user` can be either a User object, User ID or an email to be invited
member = retrieve_member(source, user, existing_members)
access_level = retrieve_access_level(access_level) access_level = retrieve_access_level(access_level)
# `user` can be either a User object or an email to be invited
member =
if user.is_a?(User)
source.members.find_by(user_id: user.id) ||
source.requesters.find_by(user_id: user.id) ||
source.members.build(user_id: user.id)
else
source.members.build(invite_email: user)
end
return member unless can_update_member?(current_user, member) return member unless can_update_member?(current_user, member)
member.attributes = { member.attributes = {
...@@ -165,17 +156,15 @@ class Member < ActiveRecord::Base ...@@ -165,17 +156,15 @@ class Member < ActiveRecord::Base
def add_users(source, users, access_level, current_user: nil, expires_at: nil) def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present? return [] unless users.present?
# Collect all user ids into separate array emails, users, existing_members = parse_users_list(source, users)
# so we can use single sql query to get user objects
user_ids = users.select { |user| user =~ /\A\d+\Z/ }
users = users - user_ids + User.where(id: user_ids)
self.transaction do self.transaction do
users.map do |user| (emails + users).map! do |user|
add_user( add_user(
source, source,
user, user,
access_level, access_level,
existing_members: existing_members,
current_user: current_user, current_user: current_user,
expires_at: expires_at expires_at: expires_at
) )
...@@ -189,6 +178,31 @@ class Member < ActiveRecord::Base ...@@ -189,6 +178,31 @@ class Member < ActiveRecord::Base
private private
def parse_users_list(source, list)
emails, user_ids, users = [], [], []
existing_members = {}
list.each do |item|
case item
when User
users << item
when Integer
user_ids << item
when /\A\d+\Z/
user_ids << item.to_i
when Devise.email_regexp
emails << item
end
end
if user_ids.present?
users.concat(User.where(id: user_ids))
existing_members = source.members_and_requesters.where(user_id: user_ids).index_by(&:user_id)
end
[emails, users, existing_members]
end
# This method is used to find users that have been entered into the "Add members" field. # This method is used to find users that have been entered into the "Add members" field.
# These can be the User objects directly, their IDs, their emails, or new emails to be invited. # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
def retrieve_user(user) def retrieve_user(user)
...@@ -197,6 +211,20 @@ class Member < ActiveRecord::Base ...@@ -197,6 +211,20 @@ class Member < ActiveRecord::Base
User.find_by(id: user) || User.find_by(email: user) || user User.find_by(id: user) || User.find_by(email: user) || user
end end
def retrieve_member(source, user, existing_members)
user = retrieve_user(user)
if user.is_a?(User)
if existing_members
existing_members[user.id] || source.members.build(user_id: user.id)
else
source.members_and_requesters.find_or_initialize_by(user_id: user.id)
end
else
source.members.build(invite_email: user)
end
end
def retrieve_access_level(access_level) def retrieve_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i } access_levels.fetch(access_level) { access_level.to_i }
end end
......
...@@ -918,6 +918,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -918,6 +918,12 @@ class MergeRequest < ActiveRecord::Base
active_diff_discussions.each do |discussion| active_diff_discussions.each do |discussion|
service.execute(discussion) service.execute(discussion)
end end
if project.resolve_outdated_diff_discussions?
MergeRequests::ResolvedDiscussionNotificationService
.new(project, current_user)
.execute(self)
end
end end
def keep_around_commit def keep_around_commit
......
...@@ -37,6 +37,7 @@ class Project < ActiveRecord::Base ...@@ -37,6 +37,7 @@ class Project < ActiveRecord::Base
default_value_for :archived, false default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :resolve_outdated_diff_discussions, false
default_value_for :container_registry_enabled, gitlab_config_features.container_registry default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { current_application_settings.pick_repository_storage } default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
...@@ -144,6 +145,7 @@ class Project < ActiveRecord::Base ...@@ -144,6 +145,7 @@ class Project < ActiveRecord::Base
has_many :requesters, -> { where.not(requested_at: nil) }, has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects has_many :deploy_keys_projects
has_many :deploy_keys, through: :deploy_keys_projects has_many :deploy_keys, through: :deploy_keys_projects
......
...@@ -166,32 +166,25 @@ class Repository ...@@ -166,32 +166,25 @@ class Repository
end end
def add_branch(user, branch_name, ref) def add_branch(user, branch_name, ref)
newrev = commit(ref).try(:sha) branch = raw_repository.add_branch(branch_name, committer: user, target: ref)
return false unless newrev
Gitlab::Git::OperationService.new(user, raw_repository).add_branch(branch_name, newrev)
after_create_branch after_create_branch
find_branch(branch_name)
branch
rescue Gitlab::Git::Repository::InvalidRef
false
end end
def add_tag(user, tag_name, target, message = nil) def add_tag(user, tag_name, target, message = nil)
newrev = commit(target).try(:id) raw_repository.add_tag(tag_name, committer: user, target: target, message: message)
options = { message: message, tagger: user_to_committer(user) } if message rescue Gitlab::Git::Repository::InvalidRef
false
return false unless newrev
Gitlab::Git::OperationService.new(user, raw_repository).add_tag(tag_name, newrev, options)
find_tag(tag_name)
end end
def rm_branch(user, branch_name) def rm_branch(user, branch_name)
before_remove_branch before_remove_branch
branch = find_branch(branch_name)
Gitlab::Git::OperationService.new(user, raw_repository).rm_branch(branch) raw_repository.rm_branch(branch_name, committer: user)
after_remove_branch after_remove_branch
true true
...@@ -199,9 +192,8 @@ class Repository ...@@ -199,9 +192,8 @@ class Repository
def rm_tag(user, tag_name) def rm_tag(user, tag_name)
before_remove_tag before_remove_tag
tag = find_tag(tag_name)
Gitlab::Git::OperationService.new(user, raw_repository).rm_tag(tag) raw_repository.rm_tag(tag_name, committer: user)
after_remove_tag after_remove_tag
true true
......
...@@ -1041,6 +1041,10 @@ class User < ActiveRecord::Base ...@@ -1041,6 +1041,10 @@ class User < ActiveRecord::Base
ensure_rss_token! ensure_rss_token!
end end
def verified_email?(email)
self.email == email
end
protected protected
# override, from Devise::Validatable # override, from Devise::Validatable
......
...@@ -17,5 +17,16 @@ module Ci ...@@ -17,5 +17,16 @@ module Ci
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" "Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
end end
end end
def trigger_variables
return [] unless trigger_request
@trigger_variables ||=
if pipeline.variables.any?
pipeline.variables.map(&:to_runner_variable)
else
trigger_request.user_variables
end
end
end end
end end
# This class is deprecated because we're closing Ci::TriggerRequest.
# New class is PipelineTriggerService (app/services/ci/pipeline_trigger_service.rb)
# which is integrated with Ci::PipelineVariable instaed of Ci::TriggerRequest.
# We remove this class after we removed v1 and v3 API. This class is still being
# referred by such legacy code.
module Ci
module CreateTriggerRequestService
Result = Struct.new(:trigger_request, :pipeline)
def self.execute(project, trigger, ref, variables = nil)
trigger_request = trigger.trigger_requests.create(variables: variables)
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref)
.execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request)
Result.new(trigger_request, pipeline)
end
end
end
...@@ -10,6 +10,10 @@ module Discussions ...@@ -10,6 +10,10 @@ module Discussions
discussion.notes.each do |note| discussion.notes.each do |note|
if outdated if outdated
note.change_position = position note.change_position = position
if project.resolve_outdated_diff_discussions?
note.resolve_without_save(current_user, resolved_by_push: true)
end
else else
note.position = position note.position = position
note.change_position = nil note.change_position = nil
......
...@@ -53,7 +53,7 @@ module Projects ...@@ -53,7 +53,7 @@ module Projects
log_error("Projects::UpdatePagesService: #{message}") log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest? @status.allow_failure = !latest?
@status.description = message @status.description = message
@status.drop @status.drop(:script_failure)
super super
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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