Commit 3b9f9aa0 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge commit 'd1b60cbc' into...

Merge commit 'd1b60cbc' into feature/gb/download-single-job-artifact-using-api

* commit 'd1b60cbc': (210 commits)
parents deaa7f54 d1b60cbc
...@@ -125,6 +125,7 @@ stages: ...@@ -125,6 +125,7 @@ stages:
- export KNAPSACK_GENERATE_REPORT=true - export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true - export CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- scripts/gitaly-test-spawn
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)' - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts: artifacts:
expire_in: 31d expire_in: 31d
...@@ -207,11 +208,10 @@ update-tests-metadata: ...@@ -207,11 +208,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: []
...@@ -226,6 +226,7 @@ flaky-examples-check: ...@@ -226,6 +226,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)
......
...@@ -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) {
......
...@@ -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 () {
......
import Cookies from 'js-cookie';
import _ from 'underscore';
import {
getCookieName,
getSelector,
hidePopover,
setupDismissButton,
mouseenter,
mouseleave,
} from './feature_highlight_helper';
export const setupFeatureHighlightPopover = (id, debounceTimeout = 300) => {
const $selector = $(getSelector(id));
const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = hidePopover.bind($selector);
const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
$selector
// Setup popover
.data('content', $popoverContent.prop('outerHTML'))
.popover({
html: true,
// Override the existing template to add custom CSS classes
template: `
<div class="popover feature-highlight-popover" role="tooltip">
<div class="arrow"></div>
<div class="popover-content"></div>
</div>
`,
})
.on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave)
.on('inserted.bs.popover', setupDismissButton)
.on('show.bs.popover', () => {
window.addEventListener('scroll', hideOnScroll);
})
.on('hide.bs.popover', () => {
window.removeEventListener('scroll', hideOnScroll);
})
// Display feature highlight
.removeAttr('disabled');
};
export const shouldHighlightFeature = (id) => {
const element = document.querySelector(getSelector(id));
const previouslyDismissed = Cookies.get(getCookieName(id)) === 'true';
return element && !previouslyDismissed;
};
export const highlightFeatures = (highlightOrder) => {
const featureId = highlightOrder.find(shouldHighlightFeature);
if (featureId) {
setupFeatureHighlightPopover(featureId);
return true;
}
return false;
};
import Cookies from 'js-cookie';
export const getCookieName = cookieId => `feature-highlighted-${cookieId}`;
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
export const showPopover = function showPopover() {
if (this.hasClass('js-popover-show')) {
return false;
}
this.popover('show');
this.addClass('disable-animation js-popover-show');
return true;
};
export const hidePopover = function hidePopover() {
if (!this.hasClass('js-popover-show')) {
return false;
}
this.popover('hide');
this.removeClass('disable-animation js-popover-show');
return true;
};
export const dismiss = function dismiss(cookieId) {
Cookies.set(getCookieName(cookieId), true);
hidePopover.call(this);
this.hide();
};
export const mouseleave = function mouseleave() {
if (!$('.popover:hover').length > 0) {
const $featureHighlight = $(this);
hidePopover.call($featureHighlight);
}
};
export const mouseenter = function mouseenter() {
const $featureHighlight = $(this);
const showedPopover = showPopover.call($featureHighlight);
if (showedPopover) {
$('.popover')
.on('mouseleave', mouseleave.bind($featureHighlight));
}
};
export const setupDismissButton = function setupDismissButton() {
const popoverId = this.getAttribute('aria-describedby');
const cookieId = this.dataset.highlight;
const $popover = $(this);
const dismissWrapper = dismiss.bind($popover, cookieId);
$(`#${popoverId} .dismiss-feature-highlight`)
.on('click', dismissWrapper);
};
import { highlightFeatures } from './feature_highlight';
import bp from '../breakpoints';
const highlightOrder = ['issue-boards'];
export default function domContentLoaded(order) {
if (bp.getBreakpointSize() === 'lg') {
highlightFeatures(order);
}
}
document.addEventListener('DOMContentLoaded', domContentLoaded.bind(this, highlightOrder));
...@@ -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
......
...@@ -102,6 +102,7 @@ import './label_manager'; ...@@ -102,6 +102,7 @@ import './label_manager';
import './labels'; import './labels';
import './labels_select'; import './labels_select';
import './layout_nav'; import './layout_nav';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader'; import LazyLoader from './lazy_loader';
import './line_highlighter'; import './line_highlighter';
import './logo'; import './logo';
...@@ -131,6 +132,7 @@ import './project_new'; ...@@ -131,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';
...@@ -248,7 +250,10 @@ $(function () { ...@@ -248,7 +250,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,
};
});
}
...@@ -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 = [];
}
}
...@@ -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>
......
...@@ -51,3 +51,4 @@ ...@@ -51,3 +51,4 @@
@import "framework/snippets"; @import "framework/snippets";
@import "framework/memory_graph"; @import "framework/memory_graph";
@import "framework/responsive-tables"; @import "framework/responsive-tables";
@import "framework/feature_highlight";
...@@ -46,6 +46,15 @@ ...@@ -46,6 +46,15 @@
} }
} }
@mixin btn-svg {
svg {
height: 15px;
width: 15px;
position: relative;
top: 2px;
}
}
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) { @mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
background-color: $light; background-color: $light;
border-color: $border-light; border-color: $border-light;
...@@ -123,6 +132,7 @@ ...@@ -123,6 +132,7 @@
.btn { .btn {
@include btn-default; @include btn-default;
@include btn-white; @include btn-white;
@include btn-svg;
color: $gl-text-color; color: $gl-text-color;
...@@ -222,13 +232,6 @@ ...@@ -222,13 +232,6 @@
} }
} }
svg {
height: 15px;
width: 15px;
position: relative;
top: 2px;
}
svg, svg,
.fa { .fa {
&:not(:last-child) { &:not(:last-child) {
......
...@@ -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 {
...@@ -822,3 +830,152 @@ ...@@ -822,3 +830,152 @@
} }
@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;
}
}
}
.feature-highlight {
position: relative;
margin-left: $gl-padding;
width: 20px;
height: 20px;
cursor: pointer;
&::before {
content: '';
display: block;
position: absolute;
top: 6px;
left: 6px;
width: 8px;
height: 8px;
background-color: $blue-500;
border-radius: 50%;
box-shadow: 0 0 0 rgba($blue-500, 0.4);
animation: pulse-highlight 2s infinite;
}
&:hover::before,
&.disable-animation::before {
animation: none;
}
&[disabled]::before {
display: none;
}
}
.is-showing-fly-out {
.feature-highlight {
display: none;
}
}
.feature-highlight-popover-content {
display: none;
hr {
margin: $gl-padding * 0.5 0;
}
.btn-link {
@include btn-svg;
svg path {
fill: currentColor;
}
}
.dismiss-feature-highlight {
padding: 0;
}
svg:first-child {
width: 100%;
background-color: $indigo-50;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
border-bottom: 1px solid darken($gray-normal, 8%);
}
}
.popover .feature-highlight-popover-content {
display: block;
}
.feature-highlight-popover {
padding: 0;
.popover-content {
padding: 0;
}
}
.feature-highlight-popover-sub-content {
padding: 9px 14px;
}
@include keyframes(pulse-highlight) {
0% {
box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
}
70% {
box-shadow: 0 0 0 10px transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
...@@ -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;
......
...@@ -2,15 +2,21 @@ ...@@ -2,15 +2,21 @@
@import 'framework/tw_bootstrap_variables'; @import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables"; @import "bootstrap/variables";
.content-wrapper.page-with-new-nav {
margin-top: $new-navbar-height;
}
header.navbar-gitlab-new { header.navbar-gitlab-new {
color: $white-light; color: $white-light;
background: linear-gradient(to right, $indigo-900, $indigo-800); background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0; border-bottom: 0;
min-height: $new-navbar-height;
.header-content { .header-content {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
padding-left: 0; padding-left: 0;
min-height: $new-navbar-height;
.title-container { .title-container {
display: -webkit-flex; display: -webkit-flex;
...@@ -38,20 +44,13 @@ header.navbar-gitlab-new { ...@@ -38,20 +44,13 @@ header.navbar-gitlab-new {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
align-items: center; align-items: center;
padding-right: $gl-padding; padding: 2px 8px;
padding-left: $gl-padding; margin: 5px 2px 5px -8px;
margin-left: -$gl-padding; border-radius: $border-radius-default;
@media (min-width: $screen-sm-min) {
padding-right: $gl-padding;
padding-left: $gl-padding;
}
svg { svg {
margin-top: -3px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
margin-right: 10px; margin-right: 8px;
} }
} }
...@@ -60,7 +59,7 @@ header.navbar-gitlab-new { ...@@ -60,7 +59,7 @@ header.navbar-gitlab-new {
svg { svg {
width: 55px; width: 55px;
height: 15px; height: 14px;
margin: 0; margin: 0;
fill: $white-light; fill: $white-light;
} }
...@@ -68,9 +67,7 @@ header.navbar-gitlab-new { ...@@ -68,9 +67,7 @@ header.navbar-gitlab-new {
&:hover, &:hover,
&:focus { &:focus {
.logo-text svg { background-color: rgba($indigo-200, .2);
fill: $tanuki-yellow;
}
} }
} }
} }
...@@ -90,6 +87,20 @@ header.navbar-gitlab-new { ...@@ -90,6 +87,20 @@ header.navbar-gitlab-new {
right: 0; right: 0;
} }
} }
&.menu-expanded {
@media (max-width: $screen-xs-max) {
.title-container,
.header-logo, {
display: none;
}
}
}
}
.dropdown-bold-header {
color: $gl-text-color-secondary;
font-size: 12px;
} }
.navbar-collapse { .navbar-collapse {
...@@ -98,14 +109,10 @@ header.navbar-gitlab-new { ...@@ -98,14 +109,10 @@ header.navbar-gitlab-new {
box-shadow: 0; box-shadow: 0;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
margin-left: -$gl-padding; margin-left: -8px;
margin-right: -10px; margin-right: -10px;
} }
.dropdown-bold-header {
color: initial;
}
.nav { .nav {
> li:not(.hidden-xs) a { > li:not(.hidden-xs) a {
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -119,7 +126,7 @@ header.navbar-gitlab-new { ...@@ -119,7 +126,7 @@ header.navbar-gitlab-new {
.container-fluid { .container-fluid {
.navbar-toggle { .navbar-toggle {
min-width: 45px; min-width: 45px;
padding: 6px $gl-padding; padding: 4px $gl-padding;
margin-right: -7px; margin-right: -7px;
font-size: 14px; font-size: 14px;
text-align: center; text-align: center;
...@@ -156,31 +163,90 @@ header.navbar-gitlab-new { ...@@ -156,31 +163,90 @@ header.navbar-gitlab-new {
} }
> a { > a {
background: none;
will-change: color; will-change: color;
margin: 4px 2px;
padding: 6px 8px;
color: $indigo-200;
height: 32px;
@media (max-width: $screen-xs-max) {
padding: 0;
}
svg {
fill: $indigo-200;
}
&.header-user-dropdown-toggle { &.header-user-dropdown-toggle {
margin-left: 2px;
.header-user-avatar { .header-user-avatar {
border-color: $indigo-200; border-color: $indigo-200;
margin-right: 0;
} }
} }
}
&:hover, .header-new-dropdown-toggle {
&:focus { margin-right: 0;
color: $white-light; }
opacity: 1;
> svg { > a:hover,
fill: $white-light; > a:focus {
} text-decoration: none;
outline: 0;
opacity: 1;
color: $white-light;
@media (min-width: $screen-sm-min) {
background-color: rgba($indigo-200, .2);
}
svg {
fill: currentColor;
}
&.header-user-dropdown-toggle { &.header-user-dropdown-toggle {
.header-user-avatar { .header-user-avatar {
border-color: $white-light; border-color: $white-light;
}
} }
} }
} }
.impersonated-user,
.impersonated-user:hover {
margin-right: 1px;
background-color: $white-light;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
svg {
fill: $indigo-900;
}
}
.impersonation-btn,
.impersonation-btn:hover {
background-color: $white-light;
margin-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
i {
color: $orange-500;
font-size: 20px;
}
}
&.active > a,
&.dropdown.open > a {
color: $indigo-900;
background-color: $white-light;
svg {
fill: currentColor;
}
}
} }
} }
} }
...@@ -188,45 +254,76 @@ header.navbar-gitlab-new { ...@@ -188,45 +254,76 @@ header.navbar-gitlab-new {
.navbar-sub-nav { .navbar-sub-nav {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
margin-bottom: 0; margin: 0 0 0 6px;
color: $indigo-200; color: $indigo-200;
> li { .dropdown-chevron {
> a:hover, position: relative;
> a:focus { top: -1px;
box-shadow: inset 0 -3px 0 rgba($indigo-200, .4); font-size: 10px;
text-decoration: none; }
outline: 0; }
color: $white-light;
}
&.active > a { .navbar-gitlab-new {
box-shadow: inset 0 -3px 0 $indigo-500; .navbar-sub-nav,
color: $white-light; .navbar-nav {
font-weight: $gl-font-weight-bold; > li {
} > a:hover,
> a:focus {
text-decoration: none;
outline: 0;
color: $white-light;
background-color: rgba($indigo-200, .2);
> a { svg {
display: block; fill: currentColor;
padding: 16px 10px; }
font-size: 13px; }
color: currentColor;
box-shadow: inset 0 0 0 transparent;
will-change: box-shadow;
transition: box-shadow 0.15s;
@media (min-width: $screen-sm-min) { &.active > a,
padding: 15px $gl-padding; &.dropdown.open > a {
font-size: 14px; color: $indigo-900;
background-color: $white-light;
svg {
fill: currentColor;
}
}
> a {
display: flex;
align-items: center;
justify-content: center;
padding: 6px 8px;
margin: 4px 2px;
font-size: 12px;
color: currentColor;
border-radius: $border-radius-default;
height: 32px;
font-weight: $gl-font-weight-bold;
svg {
fill: currentColor;
}
}
&.line-separator {
border-left: 1px solid rgba($indigo-200, .2);
margin: 8px;
} }
} }
} }
}
.dropdown-chevron { .admin-icon i {
position: relative; font-size: 18px;
top: -1px; }
font-size: 10px;
} .caret-down {
height: 11px;
width: 11px;
margin-left: 4px;
fill: currentColor;
} }
.header-user .dropdown-menu-nav, .header-user .dropdown-menu-nav,
...@@ -235,10 +332,14 @@ header.navbar-gitlab-new { ...@@ -235,10 +332,14 @@ header.navbar-gitlab-new {
} }
.search { .search {
margin: 4px 8px 0;
form { form {
height: 32px;
border: 0; border: 0;
border-radius: $border-radius-default;
background-color: rgba($indigo-200, .2); background-color: rgba($indigo-200, .2);
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, background-color ease-in-out 0.15s; transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover { &:hover {
background-color: rgba($indigo-200, .3); background-color: rgba($indigo-200, .3);
...@@ -247,31 +348,50 @@ header.navbar-gitlab-new { ...@@ -247,31 +348,50 @@ header.navbar-gitlab-new {
} }
&.search-active form { &.search-active form {
background-color: rgba($indigo-200, .3); background-color: $white-light;
box-shadow: none; box-shadow: none;
.search-input {
color: $gl-text-color;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
color: $gl-text-color-tertiary;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: $gl-text-color-tertiary;
transition: color ease-in-out 0.15s;
}
}
} }
.search-input { .search-input {
color: $white-light; color: $white-light;
background: none; background: none;
transition: color ease-in-out 0.15s;
} }
.search-input::placeholder { .search-input::placeholder {
color: rgba($indigo-200, .8); color: rgba($indigo-200, .8);
transition: color ease-in-out 0.15s;
} }
.location-badge { .location-badge {
font-size: 12px; font-size: 12px;
color: $indigo-100; color: $indigo-100;
background-color: rgba($indigo-200, .1); background-color: rgba($indigo-200, .1);
transition: color 0.15s;
will-change: color; will-change: color;
margin: -4px 4px -4px -4px; margin: -4px 4px -4px -4px;
line-height: 25px; line-height: 25px;
padding: 4px 8px; padding: 4px 8px;
border-radius: 2px 0 0 2px; border-radius: 2px 0 0 2px;
border-right: 1px solid $indigo-800; border-right: 1px solid $indigo-800;
height: 34px; height: 32px;
transition: border-color ease-in-out 0.15s;
} }
.search-input-wrap { .search-input-wrap {
...@@ -283,8 +403,9 @@ header.navbar-gitlab-new { ...@@ -283,8 +403,9 @@ header.navbar-gitlab-new {
&.search-active { &.search-active {
.location-badge { .location-badge {
color: $white-light; color: $gl-text-color;
background-color: rgba($indigo-200, .2); background-color: $nav-badge-bg;
border-color: $border-color;
} }
.search-input-wrap { .search-input-wrap {
...@@ -458,3 +579,14 @@ header.navbar-gitlab-new { ...@@ -458,3 +579,14 @@ header.navbar-gitlab-new {
} }
} }
} }
.btn-sign-in {
margin-top: 3px;
background-color: $indigo-100;
color: $indigo-900;
font-weight: $gl-font-weight-bold;
&:hover {
background-color: $white-light;
}
}
...@@ -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 {
...@@ -93,7 +93,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -93,7 +93,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 +189,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -189,7 +189,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 +453,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -453,7 +453,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 +464,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -464,7 +464,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});
} }
......
...@@ -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] }
......
...@@ -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
......
...@@ -240,7 +240,8 @@ module IssuablesHelper ...@@ -240,7 +240,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)
...@@ -296,14 +297,6 @@ module IssuablesHelper ...@@ -296,14 +297,6 @@ module IssuablesHelper
cookies[:collapsed_gutter] == 'true' cookies[:collapsed_gutter] == 'true'
end end
def issuable_state_scope(issuable)
if issuable.respond_to?(:merged?) && issuable.merged?
:merged
else
issuable.open? ? :opened : :closed
end
end
def issuable_templates(issuable) def issuable_templates(issuable)
@issuable_templates ||= @issuable_templates ||=
case issuable case issuable
......
...@@ -47,13 +47,6 @@ module IssuesHelper ...@@ -47,13 +47,6 @@ module IssuesHelper
end end
end end
def bulk_update_milestone_options
milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(Milestone::None)
options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id])
end
def milestone_options(object) def milestone_options(object)
milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed? milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed?
...@@ -93,14 +86,6 @@ module IssuesHelper ...@@ -93,14 +86,6 @@ module IssuesHelper
return 'hidden' if issue.closed? == closed return 'hidden' if issue.closed? == closed
end end
def merge_requests_sentence(merge_requests)
# Sorting based on the `!123` or `group/project!123` reference will sort
# local merge requests first.
merge_requests.map do |merge_request|
merge_request.to_reference(@project)
end.sort.to_sentence(last_word_connector: ', or ')
end
def confidential_icon(issue) def confidential_icon(issue)
icon('eye-slash') if issue.confidential? icon('eye-slash') if issue.confidential?
end end
...@@ -148,18 +133,6 @@ module IssuesHelper ...@@ -148,18 +133,6 @@ module IssuesHelper
end.to_h end.to_h
end end
def due_date_options
options = [
Issue::AnyDueDate,
Issue::NoDueDate,
Issue::DueThisWeek,
Issue::DueThisMonth,
Issue::Overdue
]
options_from_collection_for_select(options, 'name', 'title', params[:due_date])
end
def link_to_discussions_to_resolve(merge_request, single_discussion = nil) def link_to_discussions_to_resolve(merge_request, single_discussion = nil)
link_text = merge_request.to_reference link_text = merge_request.to_reference
link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion
......
...@@ -38,7 +38,7 @@ module NavHelper ...@@ -38,7 +38,7 @@ module NavHelper
end end
def layout_nav_class def layout_nav_class
return [] if show_new_nav? return 'page-with-new-nav' if show_new_nav?
class_names = [] class_names = []
class_names << 'page-with-layout-nav' if defined?(nav) && nav class_names << 'page-with-layout-nav' if defined?(nav) && nav
...@@ -50,4 +50,12 @@ module NavHelper ...@@ -50,4 +50,12 @@ module NavHelper
def nav_control_class def nav_control_class
"nav-control" if current_user "nav-control" if current_user
end end
def user_dropdown_class
class_names = []
class_names << 'header-user-dropdown-toggle'
class_names << 'impersonated-user' if session[:impersonator_id]
class_names
end
end end
...@@ -72,12 +72,6 @@ module ProjectsHelper ...@@ -72,12 +72,6 @@ module ProjectsHelper
output.html_safe output.html_safe
end end
if current_user
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
icon("chevron-down")
end
end
"#{namespace_link} / #{project_link}".html_safe "#{namespace_link} / #{project_link}".html_safe
end end
......
...@@ -305,6 +305,10 @@ module Ci ...@@ -305,6 +305,10 @@ module Ci
@stage_seeds ||= config_processor.stage_seeds(self) @stage_seeds ||= config_processor.stage_seeds(self)
end end
def has_kubernetes_active?
project.kubernetes_service&.active?
end
def has_stage_seeds? def has_stage_seeds?
stage_seeds.any? stage_seeds.any?
end end
......
...@@ -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: [])
......
...@@ -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
......
...@@ -957,13 +957,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -957,13 +957,6 @@ class MergeRequest < ActiveRecord::Base
private private
def write_ref def write_ref
target_project.repository.with_repo_branch_commit( target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path)
source_project.repository, source_branch) do |commit|
if commit
target_project.repository.write_ref(ref_path, commit.sha)
else
raise Rugged::ReferenceError, 'source repository is empty'
end
end
end end
end end
...@@ -68,7 +68,6 @@ class Project < ActiveRecord::Base ...@@ -68,7 +68,6 @@ class Project < ActiveRecord::Base
acts_as_taggable acts_as_taggable
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace attr_accessor :old_path_with_namespace
attr_accessor :template_name attr_accessor :template_name
attr_writer :pipeline_status attr_writer :pipeline_status
...@@ -145,6 +144,7 @@ class Project < ActiveRecord::Base ...@@ -145,6 +144,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
......
...@@ -20,7 +20,6 @@ class Repository ...@@ -20,7 +20,6 @@ class Repository
delegate :ref_name_for_sha, to: :raw_repository delegate :ref_name_for_sha, to: :raw_repository
CommitError = Class.new(StandardError)
CreateTreeError = Class.new(StandardError) CreateTreeError = Class.new(StandardError)
# Methods that cache data from the Git repository. # Methods that cache data from the Git repository.
...@@ -95,19 +94,6 @@ class Repository ...@@ -95,19 +94,6 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>" "#<#{self.class.name}:#{@disk_path}>"
end end
#
# Git repository can contains some hidden refs like:
# /refs/notes/*
# /refs/git-as-svn/*
# /refs/pulls/*
# This refs by default not visible in project page and not cloned to client side.
#
# This method return true if repository contains some content visible in project page.
#
def has_visible_content?
branch_count > 0
end
def commit(ref = 'HEAD') def commit(ref = 'HEAD')
return nil unless exists? return nil unless exists?
...@@ -180,32 +166,25 @@ class Repository ...@@ -180,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
GitOperationService.new(user, self).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
GitOperationService.new(user, self).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)
GitOperationService.new(user, self).rm_branch(branch) raw_repository.rm_branch(branch_name, committer: user)
after_remove_branch after_remove_branch
true true
...@@ -213,9 +192,8 @@ class Repository ...@@ -213,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)
GitOperationService.new(user, self).rm_tag(tag) raw_repository.rm_tag(tag_name, committer: user)
after_remove_tag after_remove_tag
true true
...@@ -784,16 +762,30 @@ class Repository ...@@ -784,16 +762,30 @@ class Repository
multi_action(**options) multi_action(**options)
end end
def with_branch(user, *args)
result = Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
yield start_commit
end
newrev, should_run_after_create, should_run_after_create_branch = result
after_create if should_run_after_create
after_create_branch if should_run_after_create_branch
newrev
end
# rubocop:disable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists
def multi_action( def multi_action(
user:, branch_name:, message:, actions:, user:, branch_name:, message:, actions:,
author_email: nil, author_name: nil, author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project) start_branch_name: nil, start_project: project)
GitOperationService.new(user, self).with_branch( with_branch(
user,
branch_name, branch_name,
start_branch_name: start_branch_name, start_branch_name: start_branch_name,
start_project: start_project) do |start_commit| start_repository: start_project.repository.raw_repository) do |start_commit|
index = Gitlab::Git::Index.new(raw_repository) index = Gitlab::Git::Index.new(raw_repository)
...@@ -846,7 +838,8 @@ class Repository ...@@ -846,7 +838,8 @@ class Repository
end end
def merge(user, source, merge_request, options = {}) def merge(user, source, merge_request, options = {})
GitOperationService.new(user, self).with_branch( with_branch(
user,
merge_request.target_branch) do |start_commit| merge_request.target_branch) do |start_commit|
our_commit = start_commit.sha our_commit = start_commit.sha
their_commit = source their_commit = source
...@@ -866,17 +859,18 @@ class Repository ...@@ -866,17 +859,18 @@ class Repository
merge_request.update(in_progress_merge_commit_sha: commit_id) merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id commit_id
end end
rescue Repository::CommitError # when merge_index.conflicts? rescue Gitlab::Git::CommitError # when merge_index.conflicts?
false false
end end
def revert( def revert(
user, commit, branch_name, user, commit, branch_name,
start_branch_name: nil, start_project: project) start_branch_name: nil, start_project: project)
GitOperationService.new(user, self).with_branch( with_branch(
user,
branch_name, branch_name,
start_branch_name: start_branch_name, start_branch_name: start_branch_name,
start_project: start_project) do |start_commit| start_repository: start_project.repository.raw_repository) do |start_commit|
revert_tree_id = check_revert_content(commit, start_commit.sha) revert_tree_id = check_revert_content(commit, start_commit.sha)
unless revert_tree_id unless revert_tree_id
...@@ -896,10 +890,11 @@ class Repository ...@@ -896,10 +890,11 @@ class Repository
def cherry_pick( def cherry_pick(
user, commit, branch_name, user, commit, branch_name,
start_branch_name: nil, start_project: project) start_branch_name: nil, start_project: project)
GitOperationService.new(user, self).with_branch( with_branch(
user,
branch_name, branch_name,
start_branch_name: start_branch_name, start_branch_name: start_branch_name,
start_project: start_project) do |start_commit| start_repository: start_project.repository.raw_repository) do |start_commit|
cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha) cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
unless cherry_pick_tree_id unless cherry_pick_tree_id
...@@ -921,7 +916,7 @@ class Repository ...@@ -921,7 +916,7 @@ class Repository
end end
def resolve_conflicts(user, branch_name, params) def resolve_conflicts(user, branch_name, params)
GitOperationService.new(user, self).with_branch(branch_name) do with_branch(user, branch_name) do
committer = user_to_committer(user) committer = user_to_committer(user)
create_commit(params.merge(author: committer, committer: committer)) create_commit(params.merge(author: committer, committer: committer))
...@@ -1011,25 +1006,6 @@ class Repository ...@@ -1011,25 +1006,6 @@ class Repository
run_git(args).first.lines.map(&:strip) run_git(args).first.lines.map(&:strip)
end end
def with_repo_branch_commit(start_repository, start_branch_name)
return yield nil if start_repository.empty_repo?
if start_repository == self
yield commit(start_branch_name)
else
sha = start_repository.commit(start_branch_name).sha
if branch_commit = commit(sha)
yield branch_commit
else
with_repo_tmp_commit(
start_repository, start_branch_name, sha) do |tmp_commit|
yield tmp_commit
end
end
end
end
def add_remote(name, url) def add_remote(name, url)
raw_repository.remote_add(name, url) raw_repository.remote_add(name, url)
rescue Rugged::ConfigError rescue Rugged::ConfigError
...@@ -1047,14 +1023,12 @@ class Repository ...@@ -1047,14 +1023,12 @@ class Repository
gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags) gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags)
end end
def fetch_ref(source_path, source_ref, target_ref) def fetch_source_branch(source_repository, source_branch, local_ref)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref)
message, status = run_git(args) end
# Make sure ref was created, and raise Rugged::ReferenceError when not
raise Rugged::ReferenceError, message if status != 0
target_ref def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight)
end end
def create_ref(ref, ref_path) def create_ref(ref, ref_path)
...@@ -1135,12 +1109,6 @@ class Repository ...@@ -1135,12 +1109,6 @@ class Repository
private private
def run_git(args)
circuit_breaker.perform do
Gitlab::Popen.popen([Gitlab.config.git.bin_path, *args], path_to_repo)
end
end
def blob_data_at(sha, path) def blob_data_at(sha, path)
blob = blob_at(sha, path) blob = blob_at(sha, path)
return unless blob return unless blob
...@@ -1236,16 +1204,4 @@ class Repository ...@@ -1236,16 +1204,4 @@ class Repository
.commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset) .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
.map { |c| commit(c) } .map { |c| commit(c) }
end end
def with_repo_tmp_commit(start_repository, start_branch_name, sha)
tmp_ref = fetch_ref(
start_repository.path_to_repo,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
"refs/tmp/#{SecureRandom.hex}/head"
)
yield commit(sha)
ensure
delete_refs(tmp_ref) if tmp_ref
end
end end
...@@ -644,11 +644,6 @@ class User < ActiveRecord::Base ...@@ -644,11 +644,6 @@ class User < ActiveRecord::Base
@personal_projects_count ||= personal_projects.count @personal_projects_count ||= personal_projects.count
end end
def projects_limit_percent
return 100 if projects_limit.zero?
(personal_projects.count.to_f / projects_limit) * 100
end
def recent_push(project_ids = nil) def recent_push(project_ids = nil)
# Get push events not earlier than 2 hours ago # Get push events not earlier than 2 hours ago
events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours) events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
...@@ -666,10 +661,6 @@ class User < ActiveRecord::Base ...@@ -666,10 +661,6 @@ class User < ActiveRecord::Base
end end
end end
def projects_sorted_by_activity
authorized_projects.sorted_by_activity
end
def several_namespaces? def several_namespaces?
owned_groups.any? || masters_groups.any? owned_groups.any? || masters_groups.any?
end end
...@@ -1050,6 +1041,10 @@ class User < ActiveRecord::Base ...@@ -1050,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
...@@ -17,7 +17,7 @@ module Commits ...@@ -17,7 +17,7 @@ module Commits
new_commit = create_commit! new_commit = create_commit!
success(result: new_commit) success(result: new_commit)
rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Repository::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex
error(ex.message) error(ex.message)
end end
......
...@@ -11,26 +11,8 @@ class CompareService ...@@ -11,26 +11,8 @@ class CompareService
end end
def execute(target_project, target_branch, straight: false) def execute(target_project, target_branch, straight: false)
# If compare with other project we need to fetch ref first raw_compare = target_project.repository.compare_source_branch(target_branch, start_project.repository, start_branch_name, straight: straight)
target_project.repository.with_repo_branch_commit(
start_project.repository,
start_branch_name) do |commit|
break unless commit
compare(commit.sha, target_project, target_branch, straight: straight) Compare.new(raw_compare, target_project, straight: straight) if raw_compare
end
end
private
def compare(source_sha, target_project, target_branch, straight:)
raw_compare = Gitlab::Git::Compare.new(
target_project.repository.raw_repository,
target_branch,
source_sha,
straight: straight
)
Compare.new(raw_compare, target_project, straight: straight)
end end
end end
class GitOperationService
attr_reader :committer, :repository
def initialize(committer, new_repository)
committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User)
@committer = committer
@repository = new_repository
end
def add_branch(branch_name, newrev)
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
oldrev = Gitlab::Git::BLANK_SHA
update_ref_in_hooks(ref, newrev, oldrev)
end
def rm_branch(branch)
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name
oldrev = branch.target
newrev = Gitlab::Git::BLANK_SHA
update_ref_in_hooks(ref, newrev, oldrev)
end
def add_tag(tag_name, newrev, options = {})
ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
oldrev = Gitlab::Git::BLANK_SHA
with_hooks(ref, newrev, oldrev) do |service|
# We want to pass the OID of the tag object to the hooks. For an
# annotated tag we don't know that OID until after the tag object
# (raw_tag) is created in the repository. That is why we have to
# update the value after creating the tag object. Only the
# "post-receive" hook will receive the correct value in this case.
raw_tag = repository.rugged.tags.create(tag_name, newrev, options)
service.newrev = raw_tag.target_id
end
end
def rm_tag(tag)
ref = Gitlab::Git::TAG_REF_PREFIX + tag.name
oldrev = tag.target
newrev = Gitlab::Git::BLANK_SHA
update_ref_in_hooks(ref, newrev, oldrev) do
repository.rugged.tags.delete(tag_name)
end
end
# Whenever `start_branch_name` is passed, if `branch_name` doesn't exist,
# it would be created from `start_branch_name`.
# If `start_project` is passed, and the branch doesn't exist,
# it would try to find the commits from it instead of current repository.
def with_branch(
branch_name,
start_branch_name: nil,
start_project: repository.project,
&block)
start_repository = start_project.repository
start_branch_name = nil if start_repository.empty_repo?
if start_branch_name && !start_repository.branch_exists?(start_branch_name)
raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}"
end
update_branch_with_hooks(branch_name) do
repository.with_repo_branch_commit(
start_repository,
start_branch_name || branch_name,
&block)
end
end
private
def update_branch_with_hooks(branch_name)
update_autocrlf_option
was_empty = repository.empty?
# Make commit
newrev = yield
unless newrev
raise Repository::CommitError.new('Failed to create commit')
end
branch = repository.find_branch(branch_name)
oldrev = find_oldrev_from_branch(newrev, branch)
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
update_ref_in_hooks(ref, newrev, oldrev)
# If repo was empty expire cache
repository.after_create if was_empty
repository.after_create_branch if
was_empty || Gitlab::Git.blank_ref?(oldrev)
newrev
end
def find_oldrev_from_branch(newrev, branch)
return Gitlab::Git::BLANK_SHA unless branch
oldrev = branch.target
if oldrev == repository.rugged.merge_base(newrev, branch.target)
oldrev
else
raise Repository::CommitError.new('Branch diverged')
end
end
def update_ref_in_hooks(ref, newrev, oldrev)
with_hooks(ref, newrev, oldrev) do
update_ref(ref, newrev, oldrev)
end
end
def with_hooks(ref, newrev, oldrev)
Gitlab::Git::HooksService.new.execute(
committer,
repository,
oldrev,
newrev,
ref) do |service|
yield(service)
end
end
# Gitaly note: JV: wait with migrating #update_ref until we know how to migrate its call sites.
def update_ref(ref, newrev, oldrev)
# We use 'git update-ref' because libgit2/rugged currently does not
# offer 'compare and swap' ref updates. Without compare-and-swap we can
# (and have!) accidentally reset the ref to an earlier state, clobbering
# commits. See also https://github.com/libgit2/libgit2/issues/1534.
command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
_, status = Gitlab::Popen.popen(
command,
repository.path_to_repo) do |stdin|
stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
end
unless status.zero?
raise Repository::CommitError.new(
"Could not update branch #{Gitlab::Git.branch_name(ref)}." \
" Please refresh and try again.")
end
end
def update_autocrlf_option
if repository.raw_repository.autocrlf != :input
repository.raw_repository.autocrlf = :input
end
end
end
...@@ -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
......
...@@ -22,10 +22,10 @@ ...@@ -22,10 +22,10 @@
%b Tag list: %b Tag list:
= build[:tag_list].to_a.join(", ") = build[:tag_list].to_a.join(", ")
%br %br
%b Refs only: %b Only policy:
= @jobs[build[:name].to_sym][:only].to_a.join(", ") = @jobs[build[:name].to_sym][:only].to_a.join(", ")
%br %br
%b Refs except: %b Except policy:
= @jobs[build[:name].to_sym][:except].to_a.join(", ") = @jobs[build[:name].to_sym][:except].to_a.join(", ")
%br %br
%b Environment: %b Environment:
......
<svg xmlns="http://www.w3.org/2000/svg" width="214" height="102" viewBox="0 0 214 102" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="b" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,27 C48,28.1045695 47.1045695,29 46,29 L2,29 C0.8954305,29 1.3527075e-16,28.1045695 0,27 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="a" width="102.1%" height="106.9%" x="-1%" y="-1.7%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="d" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="c" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="e" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
<path id="h" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="g" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="j" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="i" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="l" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="k" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="n" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="m" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
<path id="p" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
<filter id="o" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
</filter>
</defs>
<g fill="none" fill-rule="evenodd">
<path fill="#D6D4DE" d="M14,21 L62,21 C64.7614237,21 67,23.2385763 67,26 L67,112 C67,114.761424 64.7614237,117 62,117 L14,117 C11.2385763,117 9,114.761424 9,112 L9,26 C9,23.2385763 11.2385763,21 14,21 Z"/>
<g transform="translate(11 23)">
<path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
<path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/>
<g transform="translate(5 10)">
<use fill="black" filter="url(#a)" xlink:href="#b"/>
<use fill="#F9F9F9" xlink:href="#b"/>
</g>
<g transform="translate(5 42)">
<use fill="black" filter="url(#c)" xlink:href="#d"/>
<use fill="#FEF0E8" xlink:href="#d"/>
<path fill="#FEE1D3" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/>
<path fill="#FDC4A8" d="M9,17 L17,17 C18.1045695,17 19,17.8954305 19,19 C19,20.1045695 18.1045695,21 17,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/>
<path fill="#FC6D26" d="M24,17 L32,17 C33.1045695,17 34,17.8954305 34,19 C34,20.1045695 33.1045695,21 32,21 L24,21 C22.8954305,21 22,20.1045695 22,19 C22,17.8954305 22.8954305,17 24,17 Z"/>
</g>
</g>
<path fill="#D6D4DE" d="M148,26 L196,26 C198.761424,26 201,28.2385763 201,31 L201,117 C201,119.761424 198.761424,122 196,122 L148,122 C145.238576,122 143,119.761424 143,117 L143,31 C143,28.2385763 145.238576,26 148,26 Z"/>
<g transform="translate(145 28)">
<mask id="f" fill="white">
<use xlink:href="#e"/>
</mask>
<use fill="#FFFFFF" xlink:href="#e"/>
<path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z" mask="url(#f)"/>
<g transform="translate(5 10)">
<use fill="black" filter="url(#g)" xlink:href="#h"/>
<use fill="#F9F9F9" xlink:href="#h"/>
</g>
<g transform="translate(5 42)">
<use fill="black" filter="url(#i)" xlink:href="#j"/>
<use fill="#FEF0E8" xlink:href="#j"/>
<path fill="#FEE1D3" d="M9 8L33 8C34.1045695 8 35 8.8954305 35 10 35 11.1045695 34.1045695 12 33 12L9 12C7.8954305 12 7 11.1045695 7 10 7 8.8954305 7.8954305 8 9 8zM9 17L13 17C14.1045695 17 15 17.8954305 15 19 15 20.1045695 14.1045695 21 13 21L9 21C7.8954305 21 7 20.1045695 7 19 7 17.8954305 7.8954305 17 9 17z"/>
<path fill="#FC6D26" d="M20,17 L24,17 C25.1045695,17 26,17.8954305 26,19 C26,20.1045695 25.1045695,21 24,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/>
<path fill="#FDC4A8" d="M31,17 L35,17 C36.1045695,17 37,17.8954305 37,19 C37,20.1045695 36.1045695,21 35,21 L31,21 C29.8954305,21 29,20.1045695 29,19 C29,17.8954305 29.8954305,17 31,17 Z"/>
</g>
</g>
<path fill="#D6D4DE" d="M81,14 L129,14 C131.761424,14 134,16.2385763 134,19 L134,105 C134,107.761424 131.761424,110 129,110 L81,110 C78.2385763,110 76,107.761424 76,105 L76,19 C76,16.2385763 78.2385763,14 81,14 Z"/>
<g transform="translate(78 16)">
<path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
<g transform="translate(5 10)">
<use fill="black" filter="url(#k)" xlink:href="#l"/>
<use fill="#EFEDF8" xlink:href="#l"/>
<path fill="#E1DBF1" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/>
<path fill="#6B4FBB" d="M9,17 L13,17 C14.1045695,17 15,17.8954305 15,19 C15,20.1045695 14.1045695,21 13,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/>
<path fill="#C3B8E3" d="M20,17 L28,17 C29.1045695,17 30,17.8954305 30,19 C30,20.1045695 29.1045695,21 28,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/>
</g>
<g transform="translate(5 42)">
<use fill="black" filter="url(#m)" xlink:href="#n"/>
<use fill="#F9F9F9" xlink:href="#n"/>
</g>
<g transform="translate(5 74)">
<rect width="34" height="4" x="7" y="7" fill="#E1DBF1" rx="2"/>
<use fill="black" filter="url(#o)" xlink:href="#p"/>
<use fill="#F9F9F9" xlink:href="#p"/>
</g>
<path fill="#6B4FBB" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/>
</g>
</g>
</svg>
...@@ -42,21 +42,21 @@ ...@@ -42,21 +42,21 @@
= link_to sherlock_transactions_path, title: 'Sherlock Transactions', = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw') = icon('tachometer fw')
%li %li.user-counter
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('issues') = custom_icon('issues')
- issues_count = assigned_issuables_count(:issues) - issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count) = number_with_delimiter(issues_count)
%li %li.user-counter
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold') = custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests) - merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count) = number_with_delimiter(merge_requests_count)
%li %li.user-counter
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('check-circle fw') = custom_icon('todo_done')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count) = todos_count_format(todos_pending_count)
%li.header-user.dropdown %li.header-user.dropdown
......
...@@ -16,47 +16,35 @@ ...@@ -16,47 +16,35 @@
.navbar-collapse.collapse .navbar-collapse.collapse
%ul.nav.navbar-nav %ul.nav.navbar-nav
- if current_user
= render 'layouts/header/new_dropdown'
%li.hidden-sm.hidden-xs %li.hidden-sm.hidden-xs
= render 'layouts/search' unless current_controller?(:search) = render 'layouts/search' unless current_controller?(:search)
%li.visible-sm-inline-block.visible-xs-inline-block %li.visible-sm-inline-block.visible-xs-inline-block
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search') = icon('search')
- if current_user - if current_user
- if session[:impersonator_id] %li.user-counter
%li.impersonation
= link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret fw')
- if current_user.admin?
%li
= link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
= render 'layouts/header/new_dropdown'
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
%li
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('issues') = custom_icon('issues')
- issues_count = assigned_issuables_count(:issues) - issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count) = number_with_delimiter(issues_count)
%li %li.user-counter
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold') = custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests) - merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count) = number_with_delimiter(merge_requests_count)
%li %li.user-counter
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('check-circle fw') = custom_icon('todo_done')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count) = todos_count_format(todos_pending_count)
%li.header-user.dropdown %li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" = image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar"
= icon('chevron-down') = custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
%li.current-user %li.current-user
...@@ -68,13 +56,20 @@ ...@@ -68,13 +56,20 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li %li
= link_to "Settings", profile_path = link_to "Settings", profile_path
- if current_user
%li
= link_to "Help", help_path
%li.divider %li.divider
%li %li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
- if session[:impersonator_id]
%li.impersonation
= link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- else - else
%li %li
%div %div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
%button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' } %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
......
%li.header-new.dropdown %li.header-new.dropdown
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
- if show_new_nav? - if show_new_nav?
= icon('plus') = custom_icon('plus_square')
= icon('chevron-down') = custom_icon('caret_down')
- else - else
= icon('plus fw') = icon('plus fw')
= icon('caret-down') = custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
%ul %ul
- if @group&.persisted? - if @group&.persisted?
......
%ul.list-unstyled.navbar-sub-nav %ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "home"}) do = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do %a{ href: "#", data: { toggle: "dropdown" } }
Projects Projects
= custom_icon('caret_down')
.dropdown-menu.projects-dropdown-menu
= render "layouts/nav/projects_dropdown/show"
= nav_link(controller: ['dashboard/groups', 'explore/groups']) do = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
Groups Groups
= nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm" }) do = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
Activity Activity
%li.dropdown = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
Milestones
= nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets
%li.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } } %a{ href: "#", data: { toggle: "dropdown" } }
More More
= icon("chevron-down", class: "dropdown-chevron") = custom_icon('caret_down')
.dropdown-menu .dropdown-menu
%ul %ul
= nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm" }) do = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
Groups
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, title: 'Activity' do = link_to activity_dashboard_path, title: 'Activity' do
Activity Activity
...@@ -28,6 +43,20 @@ ...@@ -28,6 +43,20 @@
= nav_link(controller: 'dashboard/snippets') do = nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets Snippets
%li.divider
%li -# Shortcut to Dashboard > Projects
= link_to "Help", help_path, title: 'About GitLab CE' %li.hidden
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
Projects
- if current_user.admin? || Gitlab::Sherlock.enabled?
%li.line-separator.hidden-xs
- if current_user.admin?
= nav_link(controller: 'admin/dashboard') do
= link_to admin_root_path, class: 'admin-icon', title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
...@@ -5,15 +5,8 @@ ...@@ -5,15 +5,8 @@
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
Groups Groups
%li.dropdown = nav_link(controller: :snippets) do
%a{ href: "#", data: { toggle: "dropdown" } } = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
More Snippets
= icon("chevron-down", class: "dropdown-chevron") %li
.dropdown-menu = link_to "Help", help_path, title: 'About GitLab CE'
%ul
= nav_link(controller: :snippets) do
= link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
Snippets
%li.divider
%li
= link_to "Help", help_path, title: 'About GitLab CE'
...@@ -99,6 +99,20 @@ ...@@ -99,6 +99,20 @@
= link_to project_boards_path(@project), title: 'Board' do = link_to project_boards_path(@project), title: 'Board' do
%span %span
Board Board
.feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', placement: 'right', highlight: 'issue-boards' } }
.feature-highlight-popover-content
= render 'feature_highlight/issue_boards.svg'
.feature-highlight-popover-sub-content
%span= _('Use')
= link_to 'Issue Boards', project_boards_path(@project)
%span= _('to create customized software development workflows like')
%strong= _('Scrum')
%span= _('or')
%strong= _('Kanban')
%hr
%button.btn-link.dismiss-feature-highlight{ type: 'button' }
%span= _("Got it! Don't show this again")
= custom_icon('thumbs_up')
= nav_link(controller: :labels) do = nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: 'Labels' do = link_to project_labels_path(@project), title: 'Labels' do
......
- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: @project.web_url, avatar_url: @project.avatar_url } if @project&.persisted?
.projects-dropdown-container
.project-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/projects#index') do
= link_to dashboard_projects_path do
= _('Your projects')
= nav_link(path: 'projects#starred') do
= link_to starred_dashboard_projects_path do
= _('Starred projects')
= nav_link(path: 'projects#trending') do
= link_to explore_root_path do
= _('Explore projects')
.project-dropdown-content
#js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }
...@@ -14,12 +14,4 @@ ...@@ -14,12 +14,4 @@
:javascript :javascript
window.uploads_path = "#{project_uploads_path(project)}"; window.uploads_path = "#{project_uploads_path(project)}";
- content_for :header_content do
.js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects
= dropdown_title("Go to a project")
= dropdown_filter("Search your projects")
= dropdown_content
= dropdown_loading
= render template: "layouts/application" = render template: "layouts/application"
- title = capture do - title = capture do
.gpg-popover-icon.invalid This commit was signed with a different user's verified signature.
= render 'shared/icons/icon_status_notfound_borderless.svg'
%div
This commit was signed with an <strong>unverified</strong> signature.
- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] } - locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals = render partial: 'projects/commit/signature_badge', locals: locals
- title = capture do
This commit was signed with a verified signature, but the committer email
is <strong>not verified</strong> to belong to the same user.
- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'icon_status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
- if signature - if signature
- if signature.valid_signature? = render partial: "projects/commit/#{signature.verification_status}_signature_badge", locals: { signature: signature }
= render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature }
- else
= render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature }
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