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:
- export KNAPSACK_GENERATE_REPORT=true
- export CACHE_CLASSES=true
- 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)'
artifacts:
expire_in: 31d
......@@ -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 $FLAKY_RSPEC_SUITE_REPORT_PATH'
- 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:
<<: *dedicated-runner
<<: *except-docs
image: ruby:2.3-alpine
services: []
before_script: []
......@@ -226,6 +226,7 @@ flaky-examples-check:
- branches
except:
- master
- /(^docs[\/-].*|.*-docs$)/
artifacts:
expire_in: 30d
paths:
......
......@@ -181,7 +181,7 @@ gem 'connection_pool', '~> 2.0'
gem 'hipchat', '~> 1.5.0'
# JIRA integration
gem 'jira-ruby', '~> 1.1.2'
gem 'jira-ruby', '~> 1.4'
# Flowdock integration
gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
......@@ -397,7 +397,7 @@ group :ed25519 do
end
# 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
......
......@@ -275,7 +275,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.31.0)
gitaly-proto (0.32.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -404,8 +404,9 @@ GEM
cause
json
ipaddress (0.8.3)
jira-ruby (1.1.2)
jira-ruby (1.4.1)
activesupport
multipart-post
oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
jquery-rails (4.1.1)
......@@ -1020,7 +1021,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.31.0)
gitaly-proto (~> 0.32.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
......@@ -1042,7 +1043,7 @@ DEPENDENCIES
html2text
httparty (~> 0.13.3)
influxdb (~> 0.2)
jira-ruby (~> 1.1.2)
jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0)
json-schema (~> 2.6.2)
......
......@@ -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)
[![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)
[![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)
......
......@@ -5,7 +5,7 @@ const Api = {
groupPath: '/api/:version/groups/:id.json',
namespacesPath: '/api/:version/namespaces.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',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
......@@ -58,6 +58,7 @@ const Api = {
const defaults = {
search: query,
per_page: 20,
simple: true,
};
if (gon.current_user_id) {
......
......@@ -2,3 +2,4 @@ import 'underscore';
import './polyfills';
import './jquery';
import './bootstrap';
import './vue';
import Vue from 'vue';
import './vue_resource_interceptor';
if (process.env.NODE_ENV !== 'production') {
Vue.config.productionTip = false;
......
......@@ -21,11 +21,13 @@ const DiffNoteAvatars = Vue.extend({
},
template: `
<div class="diff-comment-avatar-holders"
:class="discussionClassName"
v-show="notesCount !== 0">
<div v-if="!isVisible">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image
v-for="note in notesSubset"
:key="note.id"
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="clickedAvatar($event)"
:img-src="note.authorAvatar"
......@@ -68,7 +70,8 @@ const DiffNoteAvatars = Vue.extend({
});
});
},
destroyed() {
beforeDestroy() {
this.addNoCommentClass();
$(document).off('toggle.comments');
},
watch: {
......@@ -85,6 +88,9 @@ const DiffNoteAvatars = Vue.extend({
},
},
computed: {
discussionClassName() {
return `js-diff-avatars-${this.discussionId}`;
},
notesSubset() {
let notes = [];
......
......@@ -32,6 +32,10 @@ $(() => {
const tmpApp = new tmp().$mount();
$(this).replaceWith(tmpApp.$el);
$(tmpApp.$el).one('remove.vue', () => {
tmpApp.$destroy();
tmpApp.$el.remove();
});
});
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 {
.map(tokenKey => ({
icon: `fa-${tokenKey.icon}`,
hint: tokenKey.key,
tag: `<${tokenKey.tag}>`,
tag: `:${tokenKey.tag}`,
type: tokenKey.type,
}));
......
......@@ -637,11 +637,15 @@ GitLabDropdown = (function() {
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
if (value) { value = value.toString().replace(/'/g, '\\\''); }
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
selected = true;
if (value) {
value = value.toString().replace(/'/g, '\\\'');
field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
if (field.length) {
selected = true;
}
} else {
field = this.dropdown.parent().find(`input[name='${fieldName}']`);
selected = !field.length;
}
}
// Set URL
......
......@@ -102,6 +102,7 @@ import './label_manager';
import './labels';
import './labels_select';
import './layout_nav';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
......@@ -131,6 +132,7 @@ import './project_new';
import './project_select';
import './project_show';
import './project_variables';
import './projects_dropdown';
import './projects_list';
import './syntax_highlight';
import './render_math';
......@@ -248,7 +250,10 @@ $(function () {
// Initialize popovers
$body.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 () {
return $(this).parents('form').submit();
......
......@@ -4,7 +4,7 @@
import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue';
import GraphRow from './graph_row.vue';
import Graph from './graph.vue';
import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
......@@ -32,8 +32,8 @@
},
components: {
Graph,
GraphGroup,
GraphRow,
EmptyState,
},
......@@ -127,10 +127,10 @@
:key="index"
:name="groupData.group"
>
<graph-row
v-for="(row, index) in groupData.metrics"
<graph
v-for="(graphData, index) in groupData.metrics"
:key="index"
:row-data="row"
:graph-data="graphData"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
/>
......
......@@ -3,11 +3,12 @@
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
import monitoringPaths from './monitoring_paths.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils';
import { timeScaleFormat } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left;
......@@ -18,10 +19,6 @@
type: Object,
required: true,
},
classType: {
type: String,
required: true,
},
updateAspectRatio: {
type: Boolean,
required: true,
......@@ -36,32 +33,29 @@
data() {
return {
baseGraphHeight: 450,
baseGraphWidth: 600,
graphHeight: 450,
graphWidth: 600,
graphHeightOffset: 120,
xScale: {},
yScale: {},
margin: {},
data: [],
unitOfDisplay: '',
areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1',
yAxisLabel: '',
legendTitle: '',
reducedDeploymentData: [],
area: '',
line: '',
measurements: measurements.large,
currentData: {
time: new Date(),
value: 0,
},
currentYCoordinate: 0,
currentDataIndex: 0,
currentXCoordinate: 0,
currentFlagPosition: 0,
metricUsage: '',
showFlag: false,
showDeployInfo: true,
timeSeries: [],
};
},
......@@ -69,16 +63,17 @@
GraphLegend,
GraphFlag,
GraphDeployment,
monitoringPaths,
},
computed: {
outterViewBox() {
return `0 0 ${this.graphWidth} ${this.graphHeight}`;
return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
},
innerViewBox() {
if ((this.graphWidth - 150) > 0) {
return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`;
if ((this.baseGraphWidth - 150) > 0) {
return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
}
return '0 0 0 0';
},
......@@ -89,7 +84,7 @@
paddingBottomRootSvg() {
return {
paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`,
paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
};
},
},
......@@ -104,17 +99,16 @@
this.margin = measurements.small.margin;
this.measurements = measurements.small;
}
this.data = query.result[0].values;
this.unitOfDisplay = query.unit || '';
this.yAxisLabel = this.graphData.y_label || 'Values';
this.legendTitle = query.label || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth -
this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
if (this.data !== undefined) {
this.renderAxesPaths();
this.formatDeployments();
}
this.baseGraphHeight = this.graphHeight;
this.baseGraphWidth = this.graphWidth;
this.renderAxesPaths();
this.formatDeployments();
},
handleMouseOverGraph(e) {
......@@ -123,16 +117,17 @@
point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x = point.x += 7;
const timeValueOverlay = this.xScale.invert(point.x);
const overlayIndex = bisectDate(this.data, timeValueOverlay, 1);
const d0 = this.data[overlayIndex - 1];
const d1 = this.data[overlayIndex];
const firstTimeSeries = this.timeSeries[0];
const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
const d0 = firstTimeSeries.values[overlayIndex - 1];
const d1 = firstTimeSeries.values[overlayIndex];
if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
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);
this.currentYCoordinate = this.yScale(this.currentData.value);
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103;
......@@ -145,17 +140,25 @@
} else {
this.showFlag = true;
}
this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`;
},
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()
.range([0, this.graphWidth]);
this.yScale = d3.scale.linear()
const axisYScale = d3.scale.linear()
.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()
.scale(axisXScale)
......@@ -164,7 +167,7 @@
.orient('bottom');
const yAxis = d3.svg.axis()
.scale(this.yScale)
.scale(axisYScale)
.ticks(measurements.yTicks)
.orient('left');
......@@ -180,25 +183,6 @@
.attr('class', 'axis-tick');
} // Avoid adding the class to the first tick, to prevent coloring
}); // 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 @@
},
};
</script>
<template>
<div
:class="classType">
<h5
class="text-center graph-title">
{{graphData.title}}
<div class="prometheus-graph">
<h5 class="text-center graph-title">
{{graphData.title}}
</h5>
<div
class="prometheus-svg-container"
......@@ -245,30 +228,25 @@
:graph-height="graphHeight"
:margin="margin"
:measurements="measurements"
:area-color-rgb="areaColorRgb"
:legend-title="legendTitle"
:y-axis-label="yAxisLabel"
:metric-usage="metricUsage"
:time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
/>
<svg
class="graph-data"
:viewBox="innerViewBox"
ref="graphData">
<path
class="metric-area"
:d="area"
:fill="areaColorRgb"
transform="translate(-5, 20)">
</path>
<path
class="metric-line"
:d="line"
:stroke="lineColorRgb"
fill="none"
stroke-width="2"
transform="translate(-5, 20)">
</path>
<graph-deployment
<monitoring-paths
v-for="(path, index) in timeSeries"
:key="index"
:generated-line-path="path.linePath"
:generated-area-path="path.areaPath"
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
<monitoring-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
......@@ -277,7 +255,6 @@
<graph-flag
v-if="showFlag"
:current-x-coordinate="currentXCoordinate"
:current-y-coordinate="currentYCoordinate"
:current-data="currentData"
:current-flag-position="currentFlagPosition"
:graph-height="graphHeight"
......
......@@ -7,10 +7,6 @@
type: Number,
required: true,
},
currentYCoordinate: {
type: Number,
required: true,
},
currentFlagPosition: {
type: Number,
required: true,
......@@ -60,16 +56,7 @@
:y2="calculatedHeight"
transform="translate(-5, 20)">
</line>
<circle
class="circle-metric"
:fill="circleColorRgb"
stroke="#000"
:cx="currentXCoordinate"
:cy="currentYCoordinate"
r="5"
transform="translate(-5, 20)">
</circle>
<svg
<svg
class="rect-text-metric"
:x="currentFlagPosition"
y="0">
......
<script>
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
export default {
props: {
graphWidth: {
......@@ -17,10 +19,6 @@
type: Object,
required: true,
},
areaColorRgb: {
type: String,
required: true,
},
legendTitle: {
type: String,
required: true,
......@@ -29,15 +27,25 @@
type: String,
required: true,
},
metricUsage: {
timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String,
required: true,
},
currentDataIndex: {
type: Number,
required: true,
},
},
data() {
return {
yLabelWidth: 0,
yLabelHeight: 0,
seriesXPosition: 0,
metricUsageXPosition: 0,
};
},
computed: {
......@@ -63,10 +71,28 @@
yPosition() {
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() {
this.$nextTick(() => {
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.yLabelHeight = bbox.height + 5;
});
......@@ -121,24 +147,33 @@
dy=".35em">
Time
</text>
<rect
:fill="areaColorRgb"
:width="measurements.legends.width"
:height="measurements.legends.height"
x="20"
:y="graphHeight - measurements.legendOffset">
</rect>
<text
class="text-metric-title"
x="50"
:y="graphHeight - 25">
{{legendTitle}}
</text>
<text
class="text-metric-usage"
x="50"
:y="graphHeight - 10">
{{metricUsage}}
</text>
<g class="legend-group"
v-for="(series, index) in timeSeries"
:key="index"
:transform="translateLegendGroup(index)">
<rect
:fill="series.areaColor"
:width="measurements.legends.width"
:height="measurements.legends.height"
x="20"
:y="graphHeight - measurements.legendOffset">
</rect>
<text
v-if="timeSeries.length > 1"
class="legend-metric-title"
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30">
{{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}}
</text>
<text
v-else
class="legend-metric-title"
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30">
{{legendTitle}} {{formatMetricUsage(series)}}
</text>
</g>
</g>
</template>
......@@ -14,7 +14,7 @@ export default {
<div class="panel-heading">
<h4>{{name}}</h4>
</div>
<div class="panel-body">
<div class="panel-body prometheus-graph-group">
<slot />
</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 = {
formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
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) {
deploymentDataArray.push({
......
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() {
this.groups = [];
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 = []) {
this.groups = groups.map((group) => {
const currentGroup = group;
currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value();
currentGroup.metrics = this.createArrayRows(currentGroup.metrics);
return currentGroup;
});
this.groups = groups.map(group => ({
...group,
metrics: normalizeMetrics(sortMetrics(group.metrics)),
}));
}
storeDeploymentData(deploymentData = []) {
......@@ -48,14 +38,6 @@ class MonitoringStore {
}
getMetricsCount() {
let metricsCount = 0;
this.groups.forEach((group) => {
group.metrics.forEach((metric) => {
metricsCount = metricsCount += metric.length;
});
});
return metricsCount;
return this.groups.reduce((count, group) => count + group.metrics.length, 0);
}
}
export default MonitoringStore;
......@@ -7,15 +7,15 @@ export default {
left: 40,
},
legends: {
width: 15,
height: 25,
width: 10,
height: 3,
},
backgroundLegend: {
width: 30,
height: 50,
},
axisLabelLineOffset: -20,
legendOffset: 35,
legendOffset: 33,
},
large: { // This covers both md and lg screen sizes
margin: {
......@@ -25,15 +25,15 @@ export default {
left: 80,
},
legends: {
width: 20,
height: 30,
width: 15,
height: 3,
},
backgroundLegend: {
width: 30,
height: 150,
},
axisLabelLineOffset: 20,
legendOffset: 38,
legendOffset: 36,
},
xTicks: 8,
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 {
}
renderDiscussionAvatar(diffAvatarContainer, noteEntity) {
var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
if (!avatarHolder.length) {
......@@ -475,10 +474,6 @@ export default class Notes {
gl.diffNotesCompileComponents();
}
if (commentButton.length) {
commentButton.remove();
}
}
/**
......@@ -767,6 +762,7 @@ export default class Notes {
var $note, $notes;
$note = $(el);
$notes = $note.closest('.discussion-notes');
const discussionId = $('.notes', $notes).data('discussion-id');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteElId]) {
......@@ -783,6 +779,8 @@ export default class Notes {
// "Discussions" tab
$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
if (notesTr.find('.discussion-notes').length > 1) {
$notes.remove();
......
......@@ -53,10 +53,6 @@ import Cookies from 'js-cookie';
return _this.changeProject($(e.currentTarget).val());
};
})(this));
return $('.js-projects-dropdown-toggle').on('click', function(e) {
e.preventDefault();
return $('.js-projects-dropdown').select2('open');
});
};
Project.prototype.changeProject = function(url) {
......
......@@ -5,48 +5,6 @@ import ProjectSelectComboButton from './project_select_combo_button';
(function() {
this.ProjectSelect = (function() {
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) {
var placeholder;
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 {
class="btn btn-small inline">
Check out branch
</a>
<span class="dropdown inline prepend-left-10">
<span class="dropdown prepend-left-10">
<a
class="btn btn-xs dropdown-toggle"
class="btn btn-small inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
<i
class="fa fa-download"
aria-hidden="true" />
aria-hidden="true">
</i>
<i
class="fa fa-caret-down"
aria-hidden="true" />
aria-hidden="true">
</i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li>
......
......@@ -9,6 +9,11 @@ export default {
type: String,
required: true,
},
sizeClass: {
type: String,
required: false,
default: 's40',
},
},
computed: {
/**
......@@ -38,7 +43,8 @@ export default {
<template>
<div
class="avatar s40 identicon"
class="avatar identicon"
:class="sizeClass"
:style="identiconStyles">
{{identiconTitle}}
</div>
......
......@@ -51,3 +51,4 @@
@import "framework/snippets";
@import "framework/memory_graph";
@import "framework/responsive-tables";
@import "framework/feature_highlight";
......@@ -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) {
background-color: $light;
border-color: $border-light;
......@@ -123,6 +132,7 @@
.btn {
@include btn-default;
@include btn-white;
@include btn-svg;
color: $gl-text-color;
......@@ -222,13 +232,6 @@
}
}
svg {
height: 15px;
width: 15px;
position: relative;
top: 2px;
}
svg,
.fa {
&:not(:last-child) {
......
......@@ -21,6 +21,7 @@
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
......
......@@ -737,6 +737,8 @@
@mixin new-style-dropdown($selector: '') {
#{$selector}.dropdown-menu,
#{$selector}.dropdown-menu-nav {
margin-bottom: 24px;
li {
display: block;
padding: 0 1px;
......@@ -764,11 +766,12 @@
box-shadow: none;
padding: 8px 16px;
text-align: left;
white-space: normal;
width: 100%;
// make sure the text color is not overriden
&.text-danger {
@extend .text-danger;
color: $brand-danger;
}
&.is-focused,
......@@ -777,6 +780,11 @@
&:focus {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
}
&.is-active {
......@@ -822,3 +830,152 @@
}
@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 {
top: -3px;
font-size: 10px;
}
}
.user-counter {
svg {
position: relative;
top: 2px;
height: 17px;
// hack to get SVG to line up with FA icons
height: 16px;
width: 23px;
fill: currentColor;
}
......@@ -325,12 +324,12 @@ header {
li {
.badge {
position: inherit;
top: -8px;
font-weight: $gl-font-weight-normal;
margin-left: -11px;
margin-left: -6px;
font-size: 11px;
color: $white-light;
padding: 1px 5px 2px;
padding: 0 5px;
line-height: 12px;
border-radius: 7px;
box-shadow: 0 1px 0 rgba($gl-header-color, .2);
......
......@@ -267,14 +267,26 @@
// TODO: change global style
.ajax-project-dropdown,
.ajax-users-dropdown,
body[data-page="projects:edit"] #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="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 {
&.select2-drop {
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
color: $gl-text-color;
}
&.select2-drop-above {
border-top: none;
margin-top: -4px;
}
.select2-results {
.select2-no-results,
.select2-searching,
......
......@@ -177,13 +177,14 @@ $row-hover: $blue-25;
$row-hover-border: $blue-100;
$progress-color: #c0392b;
$header-height: 50px;
$new-navbar-height: 40px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
$container-text-max-width: 540px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
$border-radius-default: 3px;
$border-radius-default: 4px;
$settings-icon-size: 18px;
$provider-btn-not-active-color: $blue-500;
$link-underline-blue: $blue-500;
......
......@@ -2,15 +2,21 @@
@import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables";
.content-wrapper.page-with-new-nav {
margin-top: $new-navbar-height;
}
header.navbar-gitlab-new {
color: $white-light;
background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0;
min-height: $new-navbar-height;
.header-content {
display: -webkit-flex;
display: flex;
padding-left: 0;
min-height: $new-navbar-height;
.title-container {
display: -webkit-flex;
......@@ -38,20 +44,13 @@ header.navbar-gitlab-new {
display: -webkit-flex;
display: flex;
align-items: center;
padding-right: $gl-padding;
padding-left: $gl-padding;
margin-left: -$gl-padding;
@media (min-width: $screen-sm-min) {
padding-right: $gl-padding;
padding-left: $gl-padding;
}
padding: 2px 8px;
margin: 5px 2px 5px -8px;
border-radius: $border-radius-default;
svg {
margin-top: -3px;
@media (min-width: $screen-sm-min) {
margin-right: 10px;
margin-right: 8px;
}
}
......@@ -60,7 +59,7 @@ header.navbar-gitlab-new {
svg {
width: 55px;
height: 15px;
height: 14px;
margin: 0;
fill: $white-light;
}
......@@ -68,9 +67,7 @@ header.navbar-gitlab-new {
&:hover,
&:focus {
.logo-text svg {
fill: $tanuki-yellow;
}
background-color: rgba($indigo-200, .2);
}
}
}
......@@ -90,6 +87,20 @@ header.navbar-gitlab-new {
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 {
......@@ -98,14 +109,10 @@ header.navbar-gitlab-new {
box-shadow: 0;
@media (max-width: $screen-xs-max) {
margin-left: -$gl-padding;
margin-left: -8px;
margin-right: -10px;
}
.dropdown-bold-header {
color: initial;
}
.nav {
> li:not(.hidden-xs) a {
@media (max-width: $screen-xs-max) {
......@@ -119,7 +126,7 @@ header.navbar-gitlab-new {
.container-fluid {
.navbar-toggle {
min-width: 45px;
padding: 6px $gl-padding;
padding: 4px $gl-padding;
margin-right: -7px;
font-size: 14px;
text-align: center;
......@@ -156,31 +163,90 @@ header.navbar-gitlab-new {
}
> a {
background: none;
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 {
margin-left: 2px;
.header-user-avatar {
border-color: $indigo-200;
margin-right: 0;
}
}
}
&:hover,
&:focus {
color: $white-light;
opacity: 1;
.header-new-dropdown-toggle {
margin-right: 0;
}
> svg {
fill: $white-light;
}
> a:hover,
> 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-avatar {
border-color: $white-light;
}
&.header-user-dropdown-toggle {
.header-user-avatar {
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 {
.navbar-sub-nav {
display: -webkit-flex;
display: flex;
margin-bottom: 0;
margin: 0 0 0 6px;
color: $indigo-200;
> li {
> a:hover,
> a:focus {
box-shadow: inset 0 -3px 0 rgba($indigo-200, .4);
text-decoration: none;
outline: 0;
color: $white-light;
}
.dropdown-chevron {
position: relative;
top: -1px;
font-size: 10px;
}
}
&.active > a {
box-shadow: inset 0 -3px 0 $indigo-500;
color: $white-light;
font-weight: $gl-font-weight-bold;
}
.navbar-gitlab-new {
.navbar-sub-nav,
.navbar-nav {
> li {
> a:hover,
> a:focus {
text-decoration: none;
outline: 0;
color: $white-light;
background-color: rgba($indigo-200, .2);
> a {
display: block;
padding: 16px 10px;
font-size: 13px;
color: currentColor;
box-shadow: inset 0 0 0 transparent;
will-change: box-shadow;
transition: box-shadow 0.15s;
svg {
fill: currentColor;
}
}
@media (min-width: $screen-sm-min) {
padding: 15px $gl-padding;
font-size: 14px;
&.active > a,
&.dropdown.open > a {
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 {
position: relative;
top: -1px;
font-size: 10px;
}
.admin-icon i {
font-size: 18px;
}
.caret-down {
height: 11px;
width: 11px;
margin-left: 4px;
fill: currentColor;
}
.header-user .dropdown-menu-nav,
......@@ -235,10 +332,14 @@ header.navbar-gitlab-new {
}
.search {
margin: 4px 8px 0;
form {
height: 32px;
border: 0;
border-radius: $border-radius-default;
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 {
background-color: rgba($indigo-200, .3);
......@@ -247,31 +348,50 @@ header.navbar-gitlab-new {
}
&.search-active form {
background-color: rgba($indigo-200, .3);
background-color: $white-light;
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 {
color: $white-light;
background: none;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
color: rgba($indigo-200, .8);
transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
color: $indigo-100;
background-color: rgba($indigo-200, .1);
transition: color 0.15s;
will-change: color;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
border-right: 1px solid $indigo-800;
height: 34px;
height: 32px;
transition: border-color ease-in-out 0.15s;
}
.search-input-wrap {
......@@ -283,8 +403,9 @@ header.navbar-gitlab-new {
&.search-active {
.location-badge {
color: $white-light;
background-color: rgba($indigo-200, .2);
color: $gl-text-color;
background-color: $nav-badge-bg;
border-color: $border-color;
}
.search-input-wrap {
......@@ -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;
// Override position: absolute
.right-sidebar {
position: fixed;
height: calc(100% - #{$header-height});
height: calc(100% - #{$new-navbar-height});
}
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
......@@ -93,7 +93,7 @@ $new-sidebar-collapsed-width: 50px;
z-index: 400;
width: $new-sidebar-width;
transition: left $sidebar-transition-duration;
top: $header-height;
top: $new-navbar-height;
bottom: 0;
left: 0;
background-color: $gray-normal;
......@@ -189,7 +189,7 @@ $new-sidebar-collapsed-width: 50px;
}
.with-performance-bar .nav-sidebar {
top: $header-height + $performance-bar-height;
top: $new-navbar-height + $performance-bar-height;
}
.sidebar-sub-level-items {
......@@ -453,7 +453,7 @@ $new-sidebar-collapsed-width: 50px;
// Make issue boards full-height now that sub-nav is gone
.boards-list {
height: calc(100vh - #{$header-height});
height: calc(100vh - #{$new-navbar-height});
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
......@@ -464,7 +464,7 @@ $new-sidebar-collapsed-width: 50px;
}
.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 @@
}
.metric-area {
opacity: 0.8;
opacity: 0.25;
}
.prometheus-graph-overlay {
......@@ -227,6 +227,26 @@
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 {
position: relative;
height: 0;
......@@ -251,8 +271,14 @@
font-weight: $gl-font-weight-bold;
}
.label-axis-text,
.text-metric-usage {
.label-axis-text {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 10px;
}
.text-metric-usage,
.legend-metric-title {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 12px;
......@@ -291,9 +317,3 @@
}
}
}
.prometheus-row {
h5 {
font-size: 16px;
}
}
......@@ -617,6 +617,8 @@
}
.issuable-actions {
@include new-style-dropdown;
padding-top: 10px;
@media (min-width: $screen-sm-min) {
......
......@@ -143,8 +143,12 @@ ul.related-merge-requests > li {
}
}
.issue-form .select2-container {
width: 250px !important;
.issue-form {
@include new-style-dropdown;
.select2-container {
width: 250px !important;
}
}
.issues-footer {
......
......@@ -23,6 +23,8 @@
.new-note,
.note-edit-form {
.note-form-actions {
@include new-style-dropdown;
position: relative;
margin: $gl-padding 0 0;
}
......
......@@ -800,8 +800,10 @@ pre.light-well {
}
}
.new_protected_branch,
.new-protected-branch,
.new-protected-tag {
@include new-style-dropdown;
label {
margin-top: 6px;
font-weight: $gl-font-weight-normal;
......@@ -821,19 +823,9 @@ pre.light-well {
.protected-branches-list,
.protected-tags-list {
margin-bottom: 30px;
a {
color: $gl-text-color;
&:hover {
color: $gl-link-color;
}
@include new-style-dropdown;
&.is-active {
font-weight: $gl-font-weight-bold;
}
}
margin-bottom: 30px;
.settings-message {
margin: 0;
......
......@@ -190,6 +190,8 @@ input[type="checkbox"]:hover {
}
.search-holder {
@include new-style-dropdown;
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: flex;
......
......@@ -36,6 +36,34 @@ module IssuableCollections
@merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder)
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)
finder_class.new(current_user, filter_params)
end
......
......@@ -27,10 +27,9 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = issues_collection
@issues = @issues.page(params[:page])
@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 redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true))
end
return if redirect_out_of_range(@issues, @total_pages)
if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
......
......@@ -18,10 +18,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(merge_request_diff: :merge_request)
@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 redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true))
end
return if redirect_out_of_range(@merge_requests, @total_pages)
if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
......
......@@ -61,6 +61,10 @@ class IssuableFinder
execute.find_by(*params)
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
# 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
......
......@@ -14,6 +14,7 @@
# search: string
# label_name: string
# sort: string
# my_reaction_emoji: string
#
class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
......
......@@ -16,6 +16,7 @@
# label_name: string
# sort: string
# non_archived: boolean
# my_reaction_emoji: string
#
class MergeRequestsFinder < IssuableFinder
def klass
......
......@@ -240,7 +240,8 @@ module IssuablesHelper
def issuables_count_for_state(issuable_type, state)
finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend
finder.count_by_state[state]
Gitlab::IssuablesCountForState.new(finder)[state]
end
def close_issuable_url(issuable)
......@@ -296,14 +297,6 @@ module IssuablesHelper
cookies[:collapsed_gutter] == 'true'
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)
@issuable_templates ||=
case issuable
......
......@@ -47,13 +47,6 @@ module IssuesHelper
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)
milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed?
......@@ -93,14 +86,6 @@ module IssuesHelper
return 'hidden' if issue.closed? == closed
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)
icon('eye-slash') if issue.confidential?
end
......@@ -148,18 +133,6 @@ module IssuesHelper
end.to_h
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)
link_text = merge_request.to_reference
link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion
......
......@@ -38,7 +38,7 @@ module NavHelper
end
def layout_nav_class
return [] if show_new_nav?
return 'page-with-new-nav' if show_new_nav?
class_names = []
class_names << 'page-with-layout-nav' if defined?(nav) && nav
......@@ -50,4 +50,12 @@ module NavHelper
def nav_control_class
"nav-control" if current_user
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
......@@ -72,12 +72,6 @@ module ProjectsHelper
output.html_safe
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
end
......
......@@ -305,6 +305,10 @@ module Ci
@stage_seeds ||= config_processor.stage_seeds(self)
end
def has_kubernetes_active?
project.kubernetes_service&.active?
end
def has_stage_seeds?
stage_seeds.any?
end
......
......@@ -6,6 +6,10 @@ module Ci
belongs_to :pipeline, foreign_key: :commit_id
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
def user_variables
......
......@@ -405,6 +405,6 @@ class Commit
end
def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self)
@gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end
end
......@@ -38,6 +38,14 @@ class CommitStatus < ActiveRecord::Base
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
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
event :process do
transition [:skipped, :manual] => :created
......@@ -79,6 +87,11 @@ class CommitStatus < ActiveRecord::Base
commit_status.finished_at = Time.now
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|
next if transition.loopback?
......
......@@ -6,6 +6,7 @@
#
module Issuable
extend ActiveSupport::Concern
include Gitlab::SQL::Pattern
include CacheMarkdownField
include Participable
include Mentionable
......@@ -122,7 +123,9 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def search(query)
where(arel_table[:title].matches("%#{query}%"))
title = to_fuzzy_arel(:title, query)
where(title)
end
# Searches for records with a matching title or description.
......@@ -133,10 +136,10 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def full_search(query)
t = arel_table
pattern = "%#{query}%"
title = to_fuzzy_arel(:title, query)
description = to_fuzzy_arel(:description, query)
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
where(title&.or(description))
end
def sort(method, excluded_labels: [])
......
......@@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base
def verified_user_infos
user_infos.select do |user_info|
user_info[:email] == user.email
user.verified_email?(user_info[:email])
end
end
......@@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base
user_infos.map do |user_info|
[
user_info[:email],
user_info[:email] == user.email
user.verified_email?(user_info[:email])
]
end.to_h
end
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
def update_invalid_gpg_signatures
......@@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base
end
def revoke
GpgSignature.where(gpg_key: self, valid_signature: true).update_all(
gpg_key_id: nil,
valid_signature: false,
updated_at: Time.zone.now
)
GpgSignature
.where(gpg_key: self)
.where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
.update_all(
gpg_key_id: nil,
verification_status: GpgSignature.verification_statuses[:unknown_key],
updated_at: Time.zone.now
)
destroy
end
......
class GpgSignature < ActiveRecord::Base
include ShaAttribute
include IgnorableColumn
ignore_column :valid_signature
sha_attribute :commit_sha
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 :gpg_key
......@@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base
end
def gpg_commit
Gitlab::Gpg::Commit.new(project, commit_sha)
Gitlab::Gpg::Commit.new(commit)
end
end
......@@ -16,6 +16,7 @@ class Group < Namespace
source: :user
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 :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......
......@@ -126,20 +126,11 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
end
def add_user(source, user, access_level, current_user: nil, expires_at: nil)
user = retrieve_user(user)
def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil)
# `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)
# `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)
member.attributes = {
......@@ -165,17 +156,15 @@ class Member < ActiveRecord::Base
def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present?
# Collect all user ids into separate array
# 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)
emails, users, existing_members = parse_users_list(source, users)
self.transaction do
users.map do |user|
(emails + users).map! do |user|
add_user(
source,
user,
access_level,
existing_members: existing_members,
current_user: current_user,
expires_at: expires_at
)
......@@ -189,6 +178,31 @@ class Member < ActiveRecord::Base
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.
# These can be the User objects directly, their IDs, their emails, or new emails to be invited.
def retrieve_user(user)
......@@ -197,6 +211,20 @@ class Member < ActiveRecord::Base
User.find_by(id: user) || User.find_by(email: user) || user
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)
access_levels.fetch(access_level) { access_level.to_i }
end
......
......@@ -957,13 +957,6 @@ class MergeRequest < ActiveRecord::Base
private
def write_ref
target_project.repository.with_repo_branch_commit(
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
target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path)
end
end
......@@ -68,7 +68,6 @@ class Project < ActiveRecord::Base
acts_as_taggable
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
attr_accessor :template_name
attr_writer :pipeline_status
......@@ -145,6 +144,7 @@ class Project < ActiveRecord::Base
has_many :requesters, -> { where.not(requested_at: nil) },
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, through: :deploy_keys_projects
......
......@@ -20,7 +20,6 @@ class Repository
delegate :ref_name_for_sha, to: :raw_repository
CommitError = Class.new(StandardError)
CreateTreeError = Class.new(StandardError)
# Methods that cache data from the Git repository.
......@@ -95,19 +94,6 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>"
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')
return nil unless exists?
......@@ -180,32 +166,25 @@ class Repository
end
def add_branch(user, branch_name, ref)
newrev = commit(ref).try(:sha)
return false unless newrev
GitOperationService.new(user, self).add_branch(branch_name, newrev)
branch = raw_repository.add_branch(branch_name, committer: user, target: ref)
after_create_branch
find_branch(branch_name)
branch
rescue Gitlab::Git::Repository::InvalidRef
false
end
def add_tag(user, tag_name, target, message = nil)
newrev = commit(target).try(:id)
options = { message: message, tagger: user_to_committer(user) } if message
return false unless newrev
GitOperationService.new(user, self).add_tag(tag_name, newrev, options)
find_tag(tag_name)
raw_repository.add_tag(tag_name, committer: user, target: target, message: message)
rescue Gitlab::Git::Repository::InvalidRef
false
end
def rm_branch(user, branch_name)
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
true
......@@ -213,9 +192,8 @@ class Repository
def rm_tag(user, tag_name)
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
true
......@@ -784,16 +762,30 @@ class Repository
multi_action(**options)
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
def multi_action(
user:, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
GitOperationService.new(user, self).with_branch(
with_branch(
user,
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)
......@@ -846,7 +838,8 @@ class Repository
end
def merge(user, source, merge_request, options = {})
GitOperationService.new(user, self).with_branch(
with_branch(
user,
merge_request.target_branch) do |start_commit|
our_commit = start_commit.sha
their_commit = source
......@@ -866,17 +859,18 @@ class Repository
merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id
end
rescue Repository::CommitError # when merge_index.conflicts?
rescue Gitlab::Git::CommitError # when merge_index.conflicts?
false
end
def revert(
user, commit, branch_name,
start_branch_name: nil, start_project: project)
GitOperationService.new(user, self).with_branch(
with_branch(
user,
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)
unless revert_tree_id
......@@ -896,10 +890,11 @@ class Repository
def cherry_pick(
user, commit, branch_name,
start_branch_name: nil, start_project: project)
GitOperationService.new(user, self).with_branch(
with_branch(
user,
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)
unless cherry_pick_tree_id
......@@ -921,7 +916,7 @@ class Repository
end
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)
create_commit(params.merge(author: committer, committer: committer))
......@@ -1011,25 +1006,6 @@ class Repository
run_git(args).first.lines.map(&:strip)
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)
raw_repository.remote_add(name, url)
rescue Rugged::ConfigError
......@@ -1047,14 +1023,12 @@ class Repository
gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags)
end
def fetch_ref(source_path, source_ref, target_ref)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
message, status = run_git(args)
# Make sure ref was created, and raise Rugged::ReferenceError when not
raise Rugged::ReferenceError, message if status != 0
def fetch_source_branch(source_repository, source_branch, local_ref)
raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref)
end
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
def create_ref(ref, ref_path)
......@@ -1135,12 +1109,6 @@ class Repository
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)
blob = blob_at(sha, path)
return unless blob
......@@ -1236,16 +1204,4 @@ class Repository
.commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
.map { |c| commit(c) }
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
......@@ -644,11 +644,6 @@ class User < ActiveRecord::Base
@personal_projects_count ||= personal_projects.count
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)
# Get push events not earlier than 2 hours ago
events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
......@@ -666,10 +661,6 @@ class User < ActiveRecord::Base
end
end
def projects_sorted_by_activity
authorized_projects.sorted_by_activity
end
def several_namespaces?
owned_groups.any? || masters_groups.any?
end
......@@ -1050,6 +1041,10 @@ class User < ActiveRecord::Base
ensure_rss_token!
end
def verified_email?(email)
self.email == email
end
protected
# override, from Devise::Validatable
......
......@@ -17,5 +17,16 @@ module Ci
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
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
# 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
new_commit = create_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)
end
......
......@@ -11,26 +11,8 @@ class CompareService
end
def execute(target_project, target_branch, straight: false)
# If compare with other project we need to fetch ref first
target_project.repository.with_repo_branch_commit(
start_project.repository,
start_branch_name) do |commit|
break unless commit
raw_compare = target_project.repository.compare_source_branch(target_branch, start_project.repository, start_branch_name, straight: straight)
compare(commit.sha, target_project, target_branch, straight: straight)
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)
Compare.new(raw_compare, target_project, straight: straight) if raw_compare
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
log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest?
@status.description = message
@status.drop
@status.drop(:script_failure)
super
end
......
......@@ -22,10 +22,10 @@
%b Tag list:
= build[:tag_list].to_a.join(", ")
%br
%b Refs only:
%b Only policy:
= @jobs[build[:name].to_sym][:only].to_a.join(", ")
%br
%b Refs except:
%b Except policy:
= @jobs[build[:name].to_sym][:except].to_a.join(", ")
%br
%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 @@
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= 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
= custom_icon('issues')
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= 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
= custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= 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
= icon('check-circle fw')
= custom_icon('todo_done')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
%li.header-user.dropdown
......
......@@ -16,47 +16,35 @@
.navbar-collapse.collapse
%ul.nav.navbar-nav
- if current_user
= render 'layouts/header/new_dropdown'
%li.hidden-sm.hidden-xs
= render 'layouts/search' unless current_controller?(:search)
%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
= icon('search')
- if current_user
- if session[:impersonator_id]
%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
%li.user-counter
= 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')
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= 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
= custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= 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
= icon('check-circle fw')
= custom_icon('todo_done')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
= todos_count_format(todos_pending_count)
%li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
= icon('chevron-down')
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 23), width: 23, height: 23, class: "header-user-avatar"
= custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
%li.current-user
......@@ -68,13 +56,20 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
- if current_user
%li
= link_to "Help", help_path
%li.divider
%li
= 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
%li
%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' }
%span.sr-only Toggle navigation
......
%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
- if show_new_nav?
= icon('plus')
= icon('chevron-down')
= custom_icon('plus_square')
= custom_icon('caret_down')
- else
= icon('plus fw')
= icon('caret-down')
= custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
- if @group&.persisted?
......
%ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "home"}) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
%a{ href: "#", data: { toggle: "dropdown" } }
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
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
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" } }
More
= icon("chevron-down", class: "dropdown-chevron")
= custom_icon('caret_down')
.dropdown-menu
%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
Activity
......@@ -28,6 +43,20 @@
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets
%li.divider
%li
= link_to "Help", help_path, title: 'About GitLab CE'
-# Shortcut to Dashboard > Projects
%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 @@
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
Groups
%li.dropdown
%a{ href: "#", data: { toggle: "dropdown" } }
More
= icon("chevron-down", class: "dropdown-chevron")
.dropdown-menu
%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'
= nav_link(controller: :snippets) do
= link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
Snippets
%li
= link_to "Help", help_path, title: 'About GitLab CE'
......@@ -99,6 +99,20 @@
= link_to project_boards_path(@project), title: 'Board' do
%span
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
= 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 @@
:javascript
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"
- title = capture do
.gpg-popover-icon.invalid
= render 'shared/icons/icon_status_notfound_borderless.svg'
%div
This commit was signed with an <strong>unverified</strong> signature.
This commit was signed with a different user's verified 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
- 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.valid_signature?
= render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature }
- else
= render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature }
= render partial: "projects/commit/#{signature.verification_status}_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