diff options
Diffstat (limited to 'app')
121 files changed, 2190 insertions, 916 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 78cb3def879..8acddd6194c 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -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) { diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index 6db8b3afbef..768453b28f1 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -2,3 +2,4 @@ import 'underscore'; import './polyfills'; import './jquery'; import './bootstrap'; +import './vue'; diff --git a/app/assets/javascripts/vue_shared/common_vue.js b/app/assets/javascripts/commons/vue.js index eb2a6071fda..8b62d78c043 100644 --- a/app/assets/javascripts/vue_shared/common_vue.js +++ b/app/assets/javascripts/commons/vue.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import './vue_resource_interceptor'; if (process.env.NODE_ENV !== 'production') { Vue.config.productionTip = false; diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index c37249c060a..06ce84d7599 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -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 = []; diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index 5decfc1dc01..0863c3406bd 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -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 () { diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js new file mode 100644 index 00000000000..800ca05cd11 --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -0,0 +1,61 @@ +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; +}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js new file mode 100644 index 00000000000..9f741355cd7 --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js @@ -0,0 +1,57 @@ +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); +}; diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js new file mode 100644 index 00000000000..fd48f2e87cc --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight_options.js @@ -0,0 +1,12 @@ +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)); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 1c5ca1d3cf9..23040cd9eb8 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -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, })); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index d65bbc0d808..6f7671aa6fe 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -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 diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 6d7c7e3c930..f14458c8d41 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -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(); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 74244faa5d9..b596c4f383f 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -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" /> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 6f6da9e1463..cde2ff7ca2a 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -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" diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index c4d4647d240..a98e3d06c18 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -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"> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index d08f9cbffd4..a43dad8e601 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -1,4 +1,6 @@ <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> diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue index 32c90fda8cc..958f537d31b 100644 --- a/app/assets/javascripts/monitoring/components/graph_group.vue +++ b/app/assets/javascripts/monitoring/components/graph_group.vue @@ -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> diff --git a/app/assets/javascripts/monitoring/components/graph_row.vue b/app/assets/javascripts/monitoring/components/graph_row.vue deleted file mode 100644 index bdb9149c3b4..00000000000 --- a/app/assets/javascripts/monitoring/components/graph_row.vue +++ /dev/null @@ -1,41 +0,0 @@ -<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> diff --git a/app/assets/javascripts/monitoring/components/monitoring_paths.vue b/app/assets/javascripts/monitoring/components/monitoring_paths.vue new file mode 100644 index 00000000000..043f1bf66bb --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_paths.vue @@ -0,0 +1,40 @@ +<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> diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js index 8e62fa63f13..345a0b37a76 100644 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -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({ diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 737c964f12e..7592af5878e 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -1,46 +1,36 @@ 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; diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js index 62cd19c86e1..ee3c45efacc 100644 --- a/app/assets/javascripts/monitoring/utils/measurements.js +++ b/app/assets/javascripts/monitoring/utils/measurements.js @@ -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, diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js new file mode 100644 index 00000000000..05d551e917c --- /dev/null +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -0,0 +1,80 @@ +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, + }; + }); +} diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index b38a6abc8d1..a09270d6d24 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -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(); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index d7e3ab42f00..fe6602259e2 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -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) { diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 1b4ed6be90a..fb01390f91c 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -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'); diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue new file mode 100644 index 00000000000..7606605be32 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/app.vue @@ -0,0 +1,157 @@ +<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> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue new file mode 100644 index 00000000000..093554cd0bc --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue @@ -0,0 +1,57 @@ +<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> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue new file mode 100644 index 00000000000..fe5179de206 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue @@ -0,0 +1,96 @@ +<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> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue new file mode 100644 index 00000000000..fa5efef2919 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue @@ -0,0 +1,63 @@ +<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> diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue new file mode 100644 index 00000000000..b71997234e5 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -0,0 +1,64 @@ +<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> diff --git a/app/assets/javascripts/projects_dropdown/constants.js b/app/assets/javascripts/projects_dropdown/constants.js new file mode 100644 index 00000000000..8937097184c --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/constants.js @@ -0,0 +1,10 @@ +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'; diff --git a/app/assets/javascripts/projects_dropdown/event_hub.js b/app/assets/javascripts/projects_dropdown/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js new file mode 100644 index 00000000000..2660da3c558 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/index.js @@ -0,0 +1,68 @@ +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, + }, + }); + }, + }); +}); diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js new file mode 100644 index 00000000000..fad956b4c26 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js @@ -0,0 +1,132 @@ +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); + } +} diff --git a/app/assets/javascripts/projects_dropdown/store/projects_store.js b/app/assets/javascripts/projects_dropdown/store/projects_store.js new file mode 100644 index 00000000000..ffefbe693f4 --- /dev/null +++ b/app/assets/javascripts/projects_dropdown/store/projects_store.js @@ -0,0 +1,33 @@ +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 = []; + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js index c05a76a3b4a..aaca42e3ebc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js @@ -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> diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 0edd820743f..7cf2e029cf6 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -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> diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index b2b3297e880..c0524bf6aa3 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -51,3 +51,4 @@ @import "framework/snippets"; @import "framework/memory_graph"; @import "framework/responsive-tables"; +@import "framework/feature_highlight"; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index b4a6b214e98..82350c36df0 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -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) { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 68a51c5a461..a85051642dd 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -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; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index fad991f2c49..6b21def33a6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -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; + } + } +} diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss new file mode 100644 index 00000000000..ebae473df50 --- /dev/null +++ b/app/assets/stylesheets/framework/feature_highlight.scss @@ -0,0 +1,94 @@ +.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; + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 35bd97980e2..b00a2d053e2 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -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); diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index a39927eb0df..6c14e8b97e0 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -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, diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 01fffa717e9..88b08998dfd 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -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; diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index b711bd12c73..4deb7431284 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -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; + } +} diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index f624b130e19..90b0a543c5c 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -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}); } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index e7c830cbc69..9362d80d4e6 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -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; - } -} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 6523376ccc3..9f2cb979518 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -617,6 +617,8 @@ } .issuable-actions { + @include new-style-dropdown; + padding-top: 10px; @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 0213e7aa9d9..e8ca5cedaee 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -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 { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 8932cff22a8..5d7c85b16ef 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -23,6 +23,8 @@ .new-note, .note-edit-form { .note-form-actions { + @include new-style-dropdown; + position: relative; margin: $gl-padding 0 0; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 19caefa1961..dd600a27545 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -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; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 8d73246223d..615020ca856 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -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; diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index a34a82b7ba6..23909bd2d39 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -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 diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 0d4266f0899..dc9e6f71152 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -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 diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index e3fa3736808..5095d7fd445 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -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] } diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index c8dd2275730..9848497f258 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -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 diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index aa9cef6b08c..d2275139c42 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -14,6 +14,7 @@ # search: string # label_name: string # sort: string +# my_reaction_emoji: string # class IssuesFinder < IssuableFinder CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 771da3d441d..d0687d28c21 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -16,6 +16,7 @@ # label_name: string # sort: string # non_archived: boolean +# my_reaction_emoji: string # class MergeRequestsFinder < IssuableFinder def klass diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index d81ba2c06eb..717abf2082d 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -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 diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 853ce827061..3d0fdce6a43 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -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 diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index b63b3b70903..73b3386fe9c 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -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 diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 0bf94fd30db..02fe82ea872 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -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 diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index ca9a350ea79..35d14b6e297 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -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 diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index c58ce5c3717..2c860598281 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -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 diff --git a/app/models/commit.rb b/app/models/commit.rb index c943365016f..ba3845df867 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -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 diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 842c6e5cb50..f3888528940 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -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? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 3731b7c8577..681c3241dbb 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -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: []) diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 3df60ddc950..1633acd4fa9 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -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 diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 50fb35c77ec..454c90d5fc4 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -1,9 +1,21 @@ 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 diff --git a/app/models/group.rb b/app/models/group.rb index 190b27cf66b..e746e4a12c9 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -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 diff --git a/app/models/member.rb b/app/models/member.rb index ee2cb13697b..cbbd58f2eaf 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -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 diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7a817eedec2..724fb4ccef1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index b9247fb535a..3d89dabd96f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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 diff --git a/app/models/repository.rb b/app/models/repository.rb index 5474c8eeb68..035f85a0b46 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 68ec93a3ec5..c5b5f09722f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb index c495c3f39bb..255475e1fe6 100644 --- a/app/presenters/ci/build_presenter.rb +++ b/app/presenters/ci/build_presenter.rb @@ -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 diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb deleted file mode 100644 index b2aa457bbd5..00000000000 --- a/app/services/ci/create_trigger_request_service.rb +++ /dev/null @@ -1,19 +0,0 @@ -# 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 diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index dbd0b9ef43a..f96f2931508 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -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 diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index a5ae4927412..53f16a236d2 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -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 diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb deleted file mode 100644 index 6b7a56e6922..00000000000 --- a/app/services/git_operation_service.rb +++ /dev/null @@ -1,159 +0,0 @@ -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 diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index f6b83a2f621..d34903c9989 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -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 diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml index c91602fcff7..30bf1384b22 100644 --- a/app/views/ci/lints/_create.html.haml +++ b/app/views/ci/lints/_create.html.haml @@ -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: diff --git a/app/views/feature_highlight/_issue_boards.svg b/app/views/feature_highlight/_issue_boards.svg new file mode 100644 index 00000000000..1522c9d51c9 --- /dev/null +++ b/app/views/feature_highlight/_issue_boards.svg @@ -0,0 +1,98 @@ +<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> diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 1d875f81041..0d6760e7b8f 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -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 diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml index c84d7053cd6..61b71c091be 100644 --- a/app/views/layouts/header/_new.html.haml +++ b/app/views/layouts/header/_new.html.haml @@ -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 diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index 9da739b0974..9cf2739b368 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -1,11 +1,11 @@ %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? diff --git a/app/views/layouts/nav/_new_dashboard.html.haml b/app/views/layouts/nav/_new_dashboard.html.haml index cfdfcbebc9f..8a39c4d775f 100644 --- a/app/views/layouts/nav/_new_dashboard.html.haml +++ b/app/views/layouts/nav/_new_dashboard.html.haml @@ -1,23 +1,38 @@ %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') diff --git a/app/views/layouts/nav/_new_explore.html.haml b/app/views/layouts/nav/_new_explore.html.haml index 40385f251e3..cd1c39f3226 100644 --- a/app/views/layouts/nav/_new_explore.html.haml +++ b/app/views/layouts/nav/_new_explore.html.haml @@ -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' diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index f5361c7af0c..760c4c97c33 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -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 diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml new file mode 100644 index 00000000000..a7370180bf6 --- /dev/null +++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml @@ -0,0 +1,15 @@ +- 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 } } diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 54d56e9b873..d6db85ee87a 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -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" diff --git a/app/views/projects/commit/_invalid_signature_badge.html.haml b/app/views/projects/commit/_invalid_signature_badge.html.haml deleted file mode 100644 index 3a73aae9d95..00000000000 --- a/app/views/projects/commit/_invalid_signature_badge.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- 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. - -- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] } - -= render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_other_user_signature_badge.html.haml b/app/views/projects/commit/_other_user_signature_badge.html.haml new file mode 100644 index 00000000000..80eca96f7ce --- /dev/null +++ b/app/views/projects/commit/_other_user_signature_badge.html.haml @@ -0,0 +1,6 @@ +- title = capture do + This commit was signed with a different user's verified signature. + +- 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 diff --git a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml new file mode 100644 index 00000000000..e737de48e22 --- /dev/null +++ b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml @@ -0,0 +1,7 @@ +- 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 diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml index 60fa52557ef..145bc629380 100644 --- a/app/views/projects/commit/_signature.html.haml +++ b/app/views/projects/commit/_signature.html.haml @@ -1,5 +1,2 @@ - 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 } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index d06b29db838..edff018ba6d 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -1,17 +1,27 @@ -- css_classes = commit_signature_badge_classes(css_classes) +- signature = local_assigns.fetch(:signature) +- title = local_assigns.fetch(:title) +- label = local_assigns.fetch(:label) +- css_class = local_assigns.fetch(:css_class) +- icon = local_assigns.fetch(:icon) +- show_user = local_assigns.fetch(:show_user, false) + +- css_classes = commit_signature_badge_classes(css_class) - title = capture do .gpg-popover-status - = title + .gpg-popover-icon{ class: css_class } + = render "shared/icons/#{icon}.svg" + %div + = title - content = capture do - .clearfix - = content + - if show_user + .clearfix + = render partial: 'projects/commit/signature_badge_user', locals: { signature: signature } GPG Key ID: %span.monospace= signature.gpg_key_primary_keyid - = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') %button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } diff --git a/app/views/projects/commit/_signature_badge_user.html.haml b/app/views/projects/commit/_signature_badge_user.html.haml new file mode 100644 index 00000000000..b20198e76db --- /dev/null +++ b/app/views/projects/commit/_signature_badge_user.html.haml @@ -0,0 +1,21 @@ +- gpg_key = signature.gpg_key +- user = gpg_key&.user +- user_name = signature.gpg_key_user_name +- user_email = signature.gpg_key_user_email + +- if user + = link_to user_path(user), class: 'gpg-popover-user-link' do + %div + = user_avatar_without_link(user: user, size: 32) + + %div + %strong= user.name + %div= user.to_reference +- else + = mail_to user_email do + %div + = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32) + + %div + %strong= user_name + %div= user_email diff --git a/app/views/projects/commit/_unknown_key_signature_badge.html.haml b/app/views/projects/commit/_unknown_key_signature_badge.html.haml new file mode 100644 index 00000000000..75c5cf57bcc --- /dev/null +++ b/app/views/projects/commit/_unknown_key_signature_badge.html.haml @@ -0,0 +1 @@ += render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature } diff --git a/app/views/projects/commit/_unverified_key_signature_badge.html.haml b/app/views/projects/commit/_unverified_key_signature_badge.html.haml new file mode 100644 index 00000000000..75c5cf57bcc --- /dev/null +++ b/app/views/projects/commit/_unverified_key_signature_badge.html.haml @@ -0,0 +1 @@ += render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature } diff --git a/app/views/projects/commit/_unverified_signature_badge.html.haml b/app/views/projects/commit/_unverified_signature_badge.html.haml new file mode 100644 index 00000000000..1af58027b83 --- /dev/null +++ b/app/views/projects/commit/_unverified_signature_badge.html.haml @@ -0,0 +1,6 @@ +- title = capture do + This commit was signed with an <strong>unverified</strong> signature. + +- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless' } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml deleted file mode 100644 index db1a41bbf64..00000000000 --- a/app/views/projects/commit/_valid_signature_badge.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -- title = capture do - .gpg-popover-icon.valid - = render 'shared/icons/icon_status_success_borderless.svg' - %div - This commit was signed with a <strong>verified</strong> signature. - -- content = capture do - - gpg_key = signature.gpg_key - - user = gpg_key&.user - - user_name = signature.gpg_key_user_name - - user_email = signature.gpg_key_user_email - - - if user - = link_to user_path(user), class: 'gpg-popover-user-link' do - %div - = user_avatar_without_link(user: user, size: 32) - - %div - %strong= gpg_key.user.name - %div @#{gpg_key.user.username} - - else - = mail_to user_email do - %div - = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32) - - %div - %strong= user_name - %div= user_email - -- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] } - -= render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/commit/_verified_signature_badge.html.haml b/app/views/projects/commit/_verified_signature_badge.html.haml new file mode 100644 index 00000000000..423beba2120 --- /dev/null +++ b/app/views/projects/commit/_verified_signature_badge.html.haml @@ -0,0 +1,7 @@ +- title = capture do + This commit was signed with a <strong>verified</strong> signature and the + committer email is verified to belong to the same user. + +- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'icon_status_success_borderless', show_user: true } + += render partial: 'projects/commit/signature_badge', locals: locals diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index 34d5a3e1831..6fb5aa45166 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -4,4 +4,4 @@ = render 'shared/empty_states/issues' - if @issues.present? - = paginate @issues, theme: "gitlab" + = paginate @issues, theme: "gitlab", total_pages: @total_pages diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index fd7ff176c5e..04b4ed95a2d 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -37,8 +37,7 @@ %ul - if can_update_issue %li= link_to 'Edit', edit_project_issue_path(@project, @issue) - / TODO: simplify condition back #36860 - - if @issue.author && current_user != @issue.author + - unless current_user == @issue.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) - if can_update_issue %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue' diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml index f5d5bc7eda9..43e23bb2200 100644 --- a/app/views/projects/jobs/_sidebar.html.haml +++ b/app/views/projects/jobs/_sidebar.html.haml @@ -46,14 +46,14 @@ %span.build-light-text Token: #{@build.trigger_request.trigger.short_token} - - if @build.trigger_request.variables + - if @build.trigger_variables.any? %p %button.btn.group.btn-group-justified.reveal-variables Reveal Variables %dl.js-build-variables.trigger-build-variables.hide - - @build.trigger_request.variables.each do |key, value| - %dt.js-build-variable.trigger-build-variable= key - %dd.js-build-value.trigger-build-value= value + - @build.trigger_variables.each do |trigger_variable| + %dt.js-build-variable.trigger-build-variable= trigger_variable[:key] + %dd.js-build-value.trigger-build-value= trigger_variable[:value] %div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") } %p diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index 4e97f74dd6a..bd6f1c05949 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -5,4 +5,4 @@ = render 'shared/empty_states/merge_requests' - if @merge_requests.present? - = paginate @merge_requests, theme: "gitlab" + = paginate @merge_requests, theme: "gitlab", total_pages: @total_pages diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index b04f5efe1f9..fb07141d2ac 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -31,7 +31,7 @@ %template{ 'v-if' => 'isResolved' } = render 'shared/icons/icon_status_success_solid.svg' %template{ 'v-else' => '' } - = render 'shared/icons/icon_status_success.svg' + = render 'shared/icons/icon_resolve_discussion.svg' - if current_user - if note.emoji_awardable? diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index 255d7ef38e0..d407e187df0 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -60,8 +60,21 @@ = f.check_box :public_builds %strong Public pipelines .help-block - Allow everyone to access pipelines for public and internal projects + Allow public access to pipelines and job details, including output logs and artifacts = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank' + .bs-callout.bs-callout-info + %p If enabled: + %ul + %li + For public projects, anyone can view pipelines and access job details (output logs and artifacts) + %li + For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts) + %li + For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts) + %p + If disabled, the access level will depend on the user's + permissions in the project. + %hr .form-group .checkbox diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg index 10e6c49ae9f..0ef9de5fed6 100644 --- a/app/views/shared/_logo.svg +++ b/app/views/shared/_logo.svg @@ -1,4 +1,4 @@ -<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36"> +<svg width="24" height="24" class="tanuki-logo" viewBox="0 0 36 36"> <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/> <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/> <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/> diff --git a/app/views/shared/icons/_caret_down.svg b/app/views/shared/icons/_caret_down.svg new file mode 100644 index 00000000000..fd80fd0f651 --- /dev/null +++ b/app/views/shared/icons/_caret_down.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="caret-down" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></svg> diff --git a/app/views/shared/icons/_icon_resolve_discussion.svg b/app/views/shared/icons/_icon_resolve_discussion.svg new file mode 100644 index 00000000000..845562e9320 --- /dev/null +++ b/app/views/shared/icons/_icon_resolve_discussion.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg> diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg index 845562e9320..eed5006bebe 100755 --- a/app/views/shared/icons/_icon_status_success.svg +++ b/app/views/shared/icons/_icon_status_success.svg @@ -1 +1 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg> diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg index 5468545da2e..0f5be6e2bc8 100644 --- a/app/views/shared/icons/_mr_bold.svg +++ b/app/views/shared/icons/_mr_bold.svg @@ -1,2 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg> - +<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg> diff --git a/app/views/shared/icons/_plus_square.svg b/app/views/shared/icons/_plus_square.svg new file mode 100644 index 00000000000..7263d924f1f --- /dev/null +++ b/app/views/shared/icons/_plus_square.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 7V4c0-.552-.448-1-1-1s-1 .448-1 1v3H4c-.552 0-1 .448-1 1s.448 1 1 1h3v3c0 .552.448 1 1 1s1-.448 1-1V9h3c.552 0 1-.448 1-1s-.448-1-1-1H9zM3 0h10c1.657 0 3 1.343 3 3v10c0 1.657-1.343 3-3 3H3c-1.657 0-3-1.343-3-3V3c0-1.657 1.343-3 3-3z"/></svg> diff --git a/app/views/shared/icons/_thumbs_up.svg b/app/views/shared/icons/_thumbs_up.svg new file mode 100644 index 00000000000..7267462418e --- /dev/null +++ b/app/views/shared/icons/_thumbs_up.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.104 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.486.5l.138.137a1 1 0 0 1 .28.87L8.33 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></svg> diff --git a/app/views/shared/icons/_todo_done.svg b/app/views/shared/icons/_todo_done.svg new file mode 100644 index 00000000000..156dfa11df1 --- /dev/null +++ b/app/views/shared/icons/_todo_done.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0l-2.83-2.83a1 1 0 0 1 1.415-1.413l2.123 2.12zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></svg> diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index cb706d80f23..f16bc8dd430 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -9,7 +9,6 @@ class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" - elsif can_update && !is_current_user = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable -- elsif issuable.author - / TODO: change back to else #36860 +- else = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse' diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml index d8144a39b23..a38cd319e3c 100644 --- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml +++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml @@ -37,15 +37,13 @@ %li.divider.droplab-item-ignore - / TODO: remove condition #36860 - - if issuable.author - %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), - button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } } - %button.btn.btn-transparent - = icon('check', class: 'icon') - .description - %strong.title Report abuse - %p.text - Report - = display_issuable_type.pluralize - that are abusive, inappropriate or spam. + %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), + button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } } + %button.btn.btn-transparent + = icon('check', class: 'icon') + .description + %strong.title Report abuse + %p.text + Report + = display_issuable_type.pluralize + that are abusive, inappropriate or spam. diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb index f34dff2d656..9b5ff17aafa 100644 --- a/app/workers/create_gpg_signature_worker.rb +++ b/app/workers/create_gpg_signature_worker.rb @@ -6,7 +6,11 @@ class CreateGpgSignatureWorker project = Project.find_by(id: project_id) return unless project + commit = project.commit(commit_sha) + + return unless commit + # This calculates and caches the signature in the database - Gitlab::Gpg::Commit.new(project, commit_sha).signature + Gitlab::Gpg::Commit.new(commit).signature end end diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb index 8b0cfcc8af8..269776a1f62 100644 --- a/app/workers/stuck_ci_jobs_worker.rb +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -53,7 +53,7 @@ class StuckCiJobsWorker def drop_build(type, build, status, timeout) Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})" Gitlab::OptimisticLocking.retry_lock(build, 3) do |b| - b.drop + b.drop(:stuck_or_timeout_failure) end end end |