diff options
Diffstat (limited to 'app')
69 files changed, 778 insertions, 148 deletions
diff --git a/app/assets/images/ci_favicons/icon_status_canceled.ico b/app/assets/images/ci_favicons/icon_status_canceled.ico Binary files differnew file mode 100755 index 00000000000..5a19458f2a2 --- /dev/null +++ b/app/assets/images/ci_favicons/icon_status_canceled.ico diff --git a/app/assets/images/ci_favicons/icon_status_created.ico b/app/assets/images/ci_favicons/icon_status_created.ico Binary files differnew file mode 100755 index 00000000000..4dca9640cb3 --- /dev/null +++ b/app/assets/images/ci_favicons/icon_status_created.ico diff --git a/app/assets/images/ci_favicons/icon_status_failed.ico b/app/assets/images/ci_favicons/icon_status_failed.ico Binary files differnew file mode 100755 index 00000000000..c961ff9a69b --- /dev/null +++ b/app/assets/images/ci_favicons/icon_status_failed.ico diff --git a/app/assets/images/ci_favicons/icon_status_manual.ico b/app/assets/images/ci_favicons/icon_status_manual.ico Binary files differnew file mode 100755 index 00000000000..5fbbc99ea7c --- /dev/null +++ b/app/assets/images/ci_favicons/icon_status_manual.ico diff --git a/app/assets/images/ci_favicons/icon_status_not_found.ico b/app/assets/images/ci_favicons/icon_status_not_found.ico Binary files differnew file mode 100755 index 00000000000..21afa9c72e6 --- /dev/null +++ b/app/assets/images/ci_favicons/icon_status_not_found.ico diff --git a/app/assets/images/ci_favicons/icon_status_pending.ico b/app/assets/images/ci_favicons/icon_status_pending.ico Binary files differnew file mode 100755 index 00000000000..8be32dab85a --- /dev/null +++ b/app/assets/images/ci_favicons/icon_status_pending.ico diff --git a/app/assets/images/ci_favicons/icon_status_running.ico b/app/assets/images/ci_favicons/icon_status_running.ico Binary files differnew file mode 100755 index 00000000000..f328ff1a5ed --- /dev/null +++ b/app/assets/images/ci_favicons/icon_status_running.ico diff --git a/app/assets/images/ci_favicons/icon_status_skipped.ico b/app/assets/images/ci_favicons/icon_status_skipped.ico Binary files differnew file mode 100755 index 00000000000..b4394e1b4af --- /dev/null +++ b/app/assets/images/ci_favicons/icon_status_skipped.ico diff --git a/app/assets/images/ci_favicons/icon_status_success.ico b/app/assets/images/ci_favicons/icon_status_success.ico Binary files differnew file mode 100755 index 00000000000..4f436c95242 --- /dev/null +++ b/app/assets/images/ci_favicons/icon_status_success.ico diff --git a/app/assets/images/ci_favicons/icon_status_warning.ico b/app/assets/images/ci_favicons/icon_status_warning.ico Binary files differnew file mode 100755 index 00000000000..805cc20cdec --- /dev/null +++ b/app/assets/images/ci_favicons/icon_status_warning.ico diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 4f63c7988f5..67106e85a37 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -263,7 +263,8 @@ AwardsHandler.prototype.addAward = function addAward( this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); return typeof callback === 'function' ? callback() : undefined; }); - return $('.emoji-menu').removeClass('is-visible'); + $('.emoji-menu').removeClass('is-visible'); + $('.js-add-award.is-active').removeClass('is-active'); }; AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar( diff --git a/app/assets/javascripts/blob/3d_viewer/index.js b/app/assets/javascripts/blob/3d_viewer/index.js new file mode 100644 index 00000000000..68d4ddad551 --- /dev/null +++ b/app/assets/javascripts/blob/3d_viewer/index.js @@ -0,0 +1,147 @@ +import * as THREE from 'three/build/three.module'; +import STLLoaderClass from 'three-stl-loader'; +import OrbitControlsClass from 'three-orbit-controls'; +import MeshObject from './mesh_object'; + +const STLLoader = STLLoaderClass(THREE); +const OrbitControls = OrbitControlsClass(THREE); + +export default class Renderer { + constructor(container) { + this.renderWrapper = this.render.bind(this); + this.objects = []; + + this.container = container; + this.width = this.container.offsetWidth; + this.height = 500; + + this.loader = new STLLoader(); + + this.fov = 45; + this.camera = new THREE.PerspectiveCamera( + this.fov, + this.width / this.height, + 1, + 1000, + ); + + this.scene = new THREE.Scene(); + + this.scene.add(this.camera); + + // Setup the viewer + this.setupRenderer(); + this.setupGrid(); + this.setupLight(); + + // Setup OrbitControls + this.controls = new OrbitControls( + this.camera, + this.renderer.domElement, + ); + this.controls.minDistance = 5; + this.controls.maxDistance = 30; + this.controls.enableKeys = false; + + this.loadFile(); + } + + setupRenderer() { + this.renderer = new THREE.WebGLRenderer({ + antialias: true, + }); + + this.renderer.setClearColor(0xFFFFFF); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.renderer.setSize( + this.width, + this.height, + ); + } + + setupLight() { + // Point light illuminates the object + const pointLight = new THREE.PointLight( + 0xFFFFFF, + 2, + 0, + ); + + pointLight.castShadow = true; + + this.camera.add(pointLight); + + // Ambient light illuminates the scene + const ambientLight = new THREE.AmbientLight( + 0xFFFFFF, + 1, + ); + this.scene.add(ambientLight); + } + + setupGrid() { + this.grid = new THREE.GridHelper( + 20, + 20, + 0x000000, + 0x000000, + ); + + this.scene.add(this.grid); + } + + loadFile() { + this.loader.load(this.container.dataset.endpoint, (geo) => { + const obj = new MeshObject(geo); + + this.objects.push(obj); + this.scene.add(obj); + + this.start(); + this.setDefaultCameraPosition(); + }); + } + + start() { + // Empty the container first + this.container.innerHTML = ''; + + // Add to DOM + this.container.appendChild(this.renderer.domElement); + + // Make controls visible + this.container.parentNode.classList.remove('is-stl-loading'); + + this.render(); + } + + render() { + this.renderer.render( + this.scene, + this.camera, + ); + + requestAnimationFrame(this.renderWrapper); + } + + changeObjectMaterials(type) { + this.objects.forEach((obj) => { + obj.changeMaterial(type); + }); + } + + setDefaultCameraPosition() { + const obj = this.objects[0]; + const radius = (obj.geometry.boundingSphere.radius / 1.5); + const dist = radius / (Math.sin((this.fov * (Math.PI / 180)) / 2)); + + this.camera.position.set( + 0, + dist + 1, + dist, + ); + + this.camera.lookAt(this.grid); + this.controls.update(); + } +} diff --git a/app/assets/javascripts/blob/3d_viewer/mesh_object.js b/app/assets/javascripts/blob/3d_viewer/mesh_object.js new file mode 100644 index 00000000000..96758884abf --- /dev/null +++ b/app/assets/javascripts/blob/3d_viewer/mesh_object.js @@ -0,0 +1,49 @@ +import { + Matrix4, + MeshLambertMaterial, + Mesh, +} from 'three/build/three.module'; + +const defaultColor = 0xE24329; +const materials = { + default: new MeshLambertMaterial({ + color: defaultColor, + }), + wireframe: new MeshLambertMaterial({ + color: defaultColor, + wireframe: true, + }), +}; + +export default class MeshObject extends Mesh { + constructor(geo) { + super( + geo, + materials.default, + ); + + this.geometry.computeBoundingSphere(); + + this.rotation.set(-Math.PI / 2, 0, 0); + + if (this.geometry.boundingSphere.radius > 4) { + const scale = 4 / this.geometry.boundingSphere.radius; + + this.geometry.applyMatrix( + new Matrix4().makeScale( + scale, + scale, + scale, + ), + ); + this.geometry.computeBoundingSphere(); + + this.position.x = -this.geometry.boundingSphere.center.x; + this.position.z = this.geometry.boundingSphere.center.y; + } + } + + changeMaterial(type) { + this.material = materials[type]; + } +} diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js index 5b79717d1e1..a74c2db9a61 100644 --- a/app/assets/javascripts/blob/pdf/index.js +++ b/app/assets/javascripts/blob/pdf/index.js @@ -10,7 +10,7 @@ Vue.use(PDFLab, { export default () => { const el = document.getElementById('js-pdf-viewer'); - new Vue({ + return new Vue({ el, data() { return { diff --git a/app/assets/javascripts/blob/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js new file mode 100644 index 00000000000..f611c4fe640 --- /dev/null +++ b/app/assets/javascripts/blob/stl_viewer.js @@ -0,0 +1,19 @@ +import Renderer from './3d_viewer'; + +document.addEventListener('DOMContentLoaded', () => { + const viewer = new Renderer(document.getElementById('js-stl-viewer')); + + [].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => { + el.addEventListener('click', (e) => { + const target = e.target; + + e.preventDefault(); + + document.querySelector('.js-material-changer.active').classList.remove('active'); + target.classList.add('active'); + target.blur(); + + viewer.changeObjectMaterials(target.dataset.type); + }); + }); +}); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 6efd26ccc37..fe54ecffdfe 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -88,6 +88,7 @@ window.Build = (function() { dataType: 'json', success: function(buildData) { $('.js-build-output').html(buildData.trace_html); + gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); if (window.location.hash === DOWN_BUILD_TRACE) { $("html,body").scrollTop(this.$buildTrace.height()); } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 2e7c9bc2689..d1c07891675 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -227,9 +227,11 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'projects:pipelines:builds': case 'projects:pipelines:show': const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; + const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`; new gl.Pipelines({ initTabs: true, + pipelineStatusUrl, tabsOptions: { action: controllerAction, defaultAction: 'pipelines', diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js index 4bb7920bb5e..1418e8d86ee 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js +++ b/app/assets/javascripts/environments/components/environment_actions.js @@ -75,6 +75,7 @@ export default { class="fa fa-spinner fa-spin" aria-hidden="true"/> </span> + </button> <ul class="dropdown-menu dropdown-menu-align-right"> <li v-for="action in actions"> @@ -91,7 +92,6 @@ export default { </button> </li> </ul> - </button> </div> `, }; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 46b80c04e20..e1e6ca25446 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -2,6 +2,8 @@ (function() { (function(w) { var base; + const faviconEl = document.getElementById('favicon'); + const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; w.gl || (w.gl = {}); (base = w.gl).utils || (base.utils = {}); w.gl.utils.isInGroupsPage = function() { @@ -361,5 +363,34 @@ fn(next, stop); }); }; + + w.gl.utils.setFavicon = (iconName) => { + if (faviconEl && iconName) { + faviconEl.setAttribute('href', `/assets/${iconName}.ico`); + } + }; + + w.gl.utils.resetFavicon = () => { + if (faviconEl) { + faviconEl.setAttribute('href', originalFavicon); + } + }; + + w.gl.utils.setCiStatusFavicon = (pageUrl) => { + $.ajax({ + url: pageUrl, + dataType: 'json', + success: function(data) { + if (data && data.icon) { + gl.utils.setFavicon(`ci_favicons/${data.icon}`); + } else { + gl.utils.resetFavicon(); + } + }, + error: function() { + gl.utils.resetFavicon(); + } + }); + }; })(window); }).call(window); diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index 0e2af3df071..b0254b17dd2 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -38,11 +38,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; function MergeRequestWidget(opts) { // Initialize MergeRequestWidget behavior // - // check_enable - Boolean, whether to check automerge status - // merge_check_url - String, URL to use to check automerge status + // check_enable - Boolean, whether to check automerge status + // merge_check_url - String, URL to use to check automerge status // ci_status_url - String, URL to use to check CI status + // pipeline_status_url - String, URL to use to get CI status for Favicon // this.opts = opts; + this.opts.pipeline_status_url = `${this.opts.pipeline_status_url}.json`; this.$widgetBody = $('.mr-widget-body'); $('#modal_merge_info').modal({ show: false @@ -159,6 +161,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; _this.status = data.status; _this.hasCi = data.has_ci; _this.updateMergeButton(_this.status, _this.hasCi); + gl.utils.setCiStatusFavicon(_this.opts.pipeline_status_url); if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (data.status !== _this.opts.ci_status || data.sha !== _this.opts.ci_sha || diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index a6ffa0f59de..d82a4eb9642 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -6,7 +6,10 @@ import statusCodes from '~/lib/utils/http_status'; import { formatRelevantDigits } from '~/lib/utils/number_utils'; import '../flash'; +const prometheusContainer = '.prometheus-container'; +const prometheusParentGraphContainer = '.prometheus-graphs'; const prometheusGraphsContainer = '.prometheus-graph'; +const prometheusStatesContainer = '.prometheus-state'; const metricsEndpoint = 'metrics.json'; const timeFormat = d3.time.format('%H:%M'); const dayFormat = d3.time.format('%b %e, %a'); @@ -14,19 +17,30 @@ const bisectDate = d3.bisector(d => d.time).left; const extraAddedWidthParent = 100; class PrometheusGraph { - constructor() { - this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; - this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; - const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + - extraAddedWidthParent; - this.originalWidth = parentContainerWidth; - this.originalHeight = 330; - this.width = parentContainerWidth - this.margin.left - this.margin.right; - this.height = this.originalHeight - this.margin.top - this.margin.bottom; - this.backOffRequestCounter = 0; - this.configureGraph(); - this.init(); + const $prometheusContainer = $(prometheusContainer); + const hasMetrics = $prometheusContainer.data('has-metrics'); + this.docLink = $prometheusContainer.data('doc-link'); + this.integrationLink = $prometheusContainer.data('prometheus-integration'); + + $(document).ajaxError(() => {}); + + if (hasMetrics) { + this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; + this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; + const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + + extraAddedWidthParent; + this.originalWidth = parentContainerWidth; + this.originalHeight = 330; + this.width = parentContainerWidth - this.margin.left - this.margin.right; + this.height = this.originalHeight - this.margin.top - this.margin.bottom; + this.backOffRequestCounter = 0; + this.configureGraph(); + this.init(); + } else { + this.state = '.js-getting-started'; + this.updateState(); + } } createGraph() { @@ -40,8 +54,19 @@ class PrometheusGraph { init() { this.getData().then((metricsResponse) => { - if (Object.keys(metricsResponse).length === 0) { - new Flash('Empty metrics', 'alert'); + let enoughData = true; + Object.keys(metricsResponse.metrics).forEach((key) => { + let currentKey; + if (key === 'cpu_values' || key === 'memory_values') { + currentKey = metricsResponse.metrics[key]; + if (Object.keys(currentKey).length === 0) { + enoughData = false; + } + } + }); + if (!enoughData) { + this.state = '.js-loading'; + this.updateState(); } else { this.transformData(metricsResponse); this.createGraph(); @@ -345,14 +370,17 @@ class PrometheusGraph { } return resp.metrics; }) - .catch(() => new Flash('An error occurred while fetching metrics.', 'alert')); + .catch(() => { + this.state = '.js-unable-to-connect'; + this.updateState(); + }); } transformData(metricsResponse) { Object.keys(metricsResponse.metrics).forEach((key) => { if (key === 'cpu_values' || key === 'memory_values') { const metricValues = (metricsResponse.metrics[key])[0]; - if (typeof metricValues !== 'undefined') { + if (metricValues !== undefined) { this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ time: new Date(metric[0] * 1000), value: metric[1], @@ -361,6 +389,13 @@ class PrometheusGraph { } }); } + + updateState() { + const $statesContainer = $(prometheusStatesContainer); + $(prometheusParentGraphContainer).hide(); + $(`${this.state}`, $statesContainer).removeClass('hidden'); + $(prometheusStatesContainer).show(); + } } export default PrometheusGraph; diff --git a/app/assets/javascripts/pipelines.js b/app/assets/javascripts/pipelines.js index 9203abefbbc..4252b615887 100644 --- a/app/assets/javascripts/pipelines.js +++ b/app/assets/javascripts/pipelines.js @@ -9,6 +9,10 @@ require('./lib/utils/bootstrap_linked_tabs'); new global.LinkedTabs(options.tabsOptions); } + if (options.pipelineStatusUrl) { + gl.utils.setCiStatusFavicon(options.pipelineStatusUrl); + } + this.addMarginToBuildColumns(); } diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 1ae144fb471..b849cc2d853 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -91,7 +91,7 @@ .award-menu-holder { display: inline-block; - position: relative; + position: absolute; .tooltip { white-space: nowrap; @@ -117,11 +117,41 @@ &.active, &:hover, - &:active { + &:active, + &.is-active { background-color: $row-hover; border-color: $row-hover-border; box-shadow: none; outline: 0; + + .award-control-icon svg { + background: $award-emoji-positive-add-bg; + + path { + fill: $award-emoji-positive-add-lines; + } + } + + .award-control-icon-neutral { + opacity: 0; + } + + .award-control-icon-positive { + opacity: 1; + transform: scale(1.15); + } + } + + &.is-active { + .award-control-icon-positive { + opacity: 0; + transform: scale(1); + } + + .award-control-icon-super-positive { + opacity: 1; + transform: scale(1); + } } &.btn { @@ -162,9 +192,33 @@ color: $border-gray-normal; margin-top: 1px; padding: 0 2px; + + svg { + margin-bottom: 1px; + height: 18px; + width: 18px; + border-radius: 50%; + + path { + fill: $border-gray-normal; + } + } + } + + .award-control-icon-positive, + .award-control-icon-super-positive { + position: absolute; + left: 7px; + bottom: 9px; + opacity: 0; + @include transition(opacity, transform); } .award-control-text { vertical-align: middle; } } + +.note-awards .award-control-icon-positive { + left: 6px; +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index ffece53a093..ddea1cf540b 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -275,3 +275,9 @@ span.idiff { } } } + +.is-stl-loading { + .stl-controls { + display: none; + } +} diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 8cd49280e1c..7098203321d 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -16,6 +16,8 @@ body.modal-open { overflow: hidden; } -.modal .modal-dialog { - width: 860px; +@media (min-width: $screen-md-min) { + .modal-dialog { + width: 860px; + } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 97794a47df8..712eb7caf33 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -293,6 +293,8 @@ $badge-color: $gl-text-color-secondary; * Award emoji */ $award-emoji-menu-shadow: rgba(0,0,0,.175); +$award-emoji-positive-add-bg: #fed159; +$award-emoji-positive-add-lines: #bb9c13; /* * Search Box diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 6faa3794c83..72e7d42858d 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -233,6 +233,15 @@ stroke-width: 1; } +.prometheus-state { + margin-top: 10px; + display: none; + + .state-button-section { + margin-top: 10px; + } +} + .environments-actions { .external-url, .monitoring-url, diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 566dcc64802..2f946ab2f59 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -329,8 +329,6 @@ } #modal_merge_info .modal-dialog { - width: 600px; - .dark { margin-right: 40px; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 57cf8e136e2..603ef461ffe 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -398,13 +398,50 @@ ul.notes { font-size: 17px; } - &:hover { + svg { + height: 16px; + width: 16px; + fill: $gray-darkest; + vertical-align: text-top; + } + + .award-control-icon-positive, + .award-control-icon-super-positive { + position: absolute; + margin-left: -20px; + opacity: 0; + } + + &:hover, + &.is-active { .danger-highlight { color: $gl-text-red; } .link-highlight { color: $gl-link-color; + + svg { + fill: $gl-link-color; + } + } + + .award-control-icon-neutral { + opacity: 0; + } + + .award-control-icon-positive { + opacity: 1; + } + } + + &.is-active { + .award-control-icon-positive { + opacity: 0; + } + + .award-control-icon-super-positive { + opacity: 1; } } } @@ -508,7 +545,6 @@ ul.notes { } .line-resolve-all-container { - .btn-group { margin-left: -4px; } @@ -537,7 +573,6 @@ ul.notes { fill: $gray-darkest; } } - } .line-resolve-all { diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 703c5fc8869..8c6dd392865 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -230,6 +230,14 @@ font-size: 0; } + .fade-right { + right: 0; + } + + .fade-left { + left: 0; + } + @media (max-width: $screen-xs-max) { .cover-block { padding-top: 20px; diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss index b97a29cd1a0..fe22d186af1 100644 --- a/app/assets/stylesheets/pages/settings_ci_cd.scss +++ b/app/assets/stylesheets/pages/settings_ci_cd.scss @@ -6,6 +6,8 @@ } .trigger-actions { + white-space: nowrap; + .btn { margin-left: 10px; } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index fc4da4c495f..f3916622b6f 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -145,8 +145,6 @@ margin: 0; } -#modal-remove-blob > .modal-dialog { width: 850px; } - .blob-upload-dropzone-previews { text-align: center; border: 2px; diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index daecfc832bf..a1975c0e341 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -3,6 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController before_action :group, only: [:show, :transfer] def index + params[:sort] ||= 'latest_activity_desc' @projects = Project.with_statistics @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb deleted file mode 100644 index 6014112256a..00000000000 --- a/app/controllers/concerns/filter_projects.rb +++ /dev/null @@ -1,17 +0,0 @@ -# == FilterProjects -# -# Controller concern to handle projects filtering -# * by name -# * by archived state -# -module FilterProjects - extend ActiveSupport::Concern - - def filter_projects(projects) - projects = projects.search(params[:name]) if params[:name].present? - projects = projects.non_archived if params[:archived].blank? - projects = projects.personal(current_user) if params[:personal].present? && current_user - - projects - end -end diff --git a/app/controllers/concerns/params_backward_compatibility.rb b/app/controllers/concerns/params_backward_compatibility.rb new file mode 100644 index 00000000000..b0e3d9c7b34 --- /dev/null +++ b/app/controllers/concerns/params_backward_compatibility.rb @@ -0,0 +1,7 @@ +module ParamsBackwardCompatibility + private + + def set_non_archived_param + params[:non_archived] = params[:archived].blank? + end +end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index be00d765f73..5a1efcab1a3 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -1,10 +1,11 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController - include FilterProjects + include ParamsBackwardCompatibility + + before_action :set_non_archived_param + before_action :default_sorting def index - @projects = load_projects(current_user.authorized_projects) - @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.page(params[:page]) + @projects = load_projects(params.merge(non_public: true)).page(params[:page]) respond_to do |format| format.html { @last_push = current_user.recent_push } @@ -21,10 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def starred - @projects = load_projects(current_user.viewable_starred_projects) - @projects = @projects.includes(:forked_from_project, :tags) - @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.page(params[:page]) + @projects = load_projects(params.merge(starred: true)). + includes(:forked_from_project, :tags).page(params[:page]) @last_push = current_user.recent_push @groups = [] @@ -41,14 +40,18 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController private - def load_projects(base_scope) - projects = base_scope.sorted_by_activity.includes(:route, namespace: :route) + def default_sorting + params[:sort] ||= 'latest_activity_desc' + @sort = params[:sort] + end - filter_projects(projects) + def load_projects(finder_params) + ProjectsFinder.new(params: finder_params, current_user: current_user). + execute.includes(:route, namespace: :route) end def load_events - @events = Event.in_projects(load_projects(current_user.authorized_projects)) + @events = Event.in_projects(load_projects(params.merge(non_public: true))) @events = event_filter.apply_filter(@events).with_associations @events = @events.limit(20).offset(params[:offset] || 0) end diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 6167f9bd335..8f1870759e4 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -1,14 +1,12 @@ class Explore::ProjectsController < Explore::ApplicationController - include FilterProjects + include ParamsBackwardCompatibility + + before_action :set_non_archived_param def index - @projects = load_projects - @tags = @projects.tags_on(:tags) - @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? - @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? - @projects = filter_projects(@projects) - @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.includes(:namespace).page(params[:page]) + params[:sort] ||= 'latest_activity_desc' + @sort = params[:sort] + @projects = load_projects.page(params[:page]) respond_to do |format| format.html @@ -21,10 +19,9 @@ class Explore::ProjectsController < Explore::ApplicationController end def trending - @projects = load_projects(Project.trending) - @projects = filter_projects(@projects) - @projects = @projects.sort(@sort = params[:sort]) - @projects = @projects.page(params[:page]) + params[:trending] = true + @sort = params[:sort] + @projects = load_projects.page(params[:page]) respond_to do |format| format.html @@ -37,10 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController end def starred - @projects = load_projects - @projects = filter_projects(@projects) - @projects = @projects.reorder('star_count DESC') - @projects = @projects.page(params[:page]) + @projects = load_projects.reorder('star_count DESC').page(params[:page]) respond_to do |format| format.html @@ -52,10 +46,10 @@ class Explore::ProjectsController < Explore::ApplicationController end end - protected + private - def load_projects(base_scope = nil) - base_scope ||= ProjectsFinder.new.execute(current_user) - base_scope.includes(:route, namespace: :route) + def load_projects + ProjectsFinder.new(current_user: current_user, params: params). + execute.includes(:route, namespace: :route) end end diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 8b69c18d689..29ffaeb19c1 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -27,7 +27,7 @@ class Groups::ApplicationController < ApplicationController end def group_projects - @projects ||= GroupProjectsFinder.new(group).execute(current_user) + @projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute end def authorize_admin_group! diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 05f9ee1ee90..78c9f1f7004 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,7 +1,7 @@ class GroupsController < Groups::ApplicationController - include FilterProjects include IssuesAction include MergeRequestsAction + include ParamsBackwardCompatibility respond_to :html @@ -105,15 +105,16 @@ class GroupsController < Groups::ApplicationController protected def setup_projects + set_non_archived_param + params[:sort] ||= 'latest_activity_desc' + @sort = params[:sort] + options = {} options[:only_owned] = true if params[:shared] == '0' options[:only_shared] = true if params[:shared] == '1' - @projects = GroupProjectsFinder.new(group, options).execute(current_user) + @projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user).execute @projects = @projects.includes(:namespace) - @projects = @projects.sorted_by_activity - @projects = filter_projects(@projects) - @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]) if params[:name].blank? end diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index ba46e2528e6..1eb3800e49d 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -9,7 +9,7 @@ class Projects::ForksController < Projects::ApplicationController def index base_query = project.forks.includes(:creator) - @forks = base_query.merge(ProjectsFinder.new.execute(current_user)) + @forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute) @total_forks_count = base_query.size @private_forks_count = @total_forks_count - @forks.size @public_forks_count = @total_forks_count - @private_forks_count diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2683614d2e8..a452bbba422 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -140,6 +140,6 @@ class UsersController < ApplicationController end def projects_for_current_user - ProjectsFinder.new.execute(current_user) + ProjectsFinder.new(current_user: current_user).execute end end diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index 3b9a421b118..f043c38c6f9 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -1,42 +1,63 @@ -class GroupProjectsFinder < UnionFinder - def initialize(group, options = {}) +# GroupProjectsFinder +# +# Used to filter Projects by set of params +# +# Arguments: +# current_user - which user use +# project_ids_relation: int[] - project ids to use +# group +# options: +# only_owned: boolean +# only_shared: boolean +# params: +# sort: string +# visibility_level: int +# tags: string[] +# personal: boolean +# search: string +# non_archived: boolean +# +class GroupProjectsFinder < ProjectsFinder + attr_reader :group, :options + + def initialize(group:, params: {}, options: {}, current_user: nil, project_ids_relation: nil) + super(params: params, current_user: current_user, project_ids_relation: project_ids_relation) @group = group @options = options end - def execute(current_user = nil) - segments = group_projects(current_user) - find_union(segments, Project) - end - private - def group_projects(current_user) - only_owned = @options.fetch(:only_owned, false) - only_shared = @options.fetch(:only_shared, false) + def init_collection + only_owned = options.fetch(:only_owned, false) + only_shared = options.fetch(:only_shared, false) projects = [] if current_user - if @group.users.include?(current_user) - projects << @group.projects unless only_shared - projects << @group.shared_projects unless only_owned + if group.users.include?(current_user) + projects << group.projects unless only_shared + projects << group.shared_projects unless only_owned else unless only_shared - projects << @group.projects.visible_to_user(current_user) - projects << @group.projects.public_to_user(current_user) + projects << group.projects.visible_to_user(current_user) + projects << group.projects.public_to_user(current_user) end unless only_owned - projects << @group.shared_projects.visible_to_user(current_user) - projects << @group.shared_projects.public_to_user(current_user) + projects << group.shared_projects.visible_to_user(current_user) + projects << group.shared_projects.public_to_user(current_user) end end else - projects << @group.projects.public_only unless only_shared - projects << @group.shared_projects.public_only unless only_owned + projects << group.projects.public_only unless only_shared + projects << group.shared_projects.public_only unless only_owned end projects end + + def union(items) + find_union(items, Project) + end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f7ebb1807d7..4cc42b88a2a 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -116,9 +116,9 @@ class IssuableFinder if current_user && params[:authorized_only].presence && !current_user_related? current_user.authorized_projects elsif group - GroupProjectsFinder.new(group).execute(current_user) + GroupProjectsFinder.new(group: group, current_user: current_user).execute else - projects_finder.execute(current_user, item_project_ids(items)) + ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute end @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) @@ -405,8 +405,4 @@ class IssuableFinder def current_user_related? params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' end - - def projects_finder - @projects_finder ||= ProjectsFinder.new - end end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index e52083f86e4..042d792dada 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -83,7 +83,7 @@ class LabelsFinder < UnionFinder def projects return @projects if defined?(@projects) - @projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user) + @projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute @projects = @projects.in_namespace(params[:group_id]) if group? @projects = @projects.where(id: params[:project_ids]) if projects? @projects = @projects.reorder(nil) diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 18ec45f300d..f6d8226bf3f 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -1,19 +1,93 @@ +# ProjectsFinder +# +# Used to filter Projects by set of params +# +# Arguments: +# current_user - which user use +# project_ids_relation: int[] - project ids to use +# params: +# trending: boolean +# non_public: boolean +# starred: boolean +# sort: string +# visibility_level: int +# tags: string[] +# personal: boolean +# search: string +# non_archived: boolean +# class ProjectsFinder < UnionFinder - def execute(current_user = nil, project_ids_relation = nil) - segments = all_projects(current_user) - segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation + attr_accessor :params + attr_reader :current_user, :project_ids_relation - find_union(segments, Project).with_route + def initialize(params: {}, current_user: nil, project_ids_relation: nil) + @params = params + @current_user = current_user + @project_ids_relation = project_ids_relation + end + + def execute + items = init_collection + items = by_ids(items) + items = union(items) + items = by_personal(items) + items = by_visibilty_level(items) + items = by_tags(items) + items = by_search(items) + items = by_archived(items) + sort(items) end private - def all_projects(current_user) + def init_collection projects = [] - projects << current_user.authorized_projects if current_user - projects << Project.unscoped.public_to_user(current_user) + if params[:trending].present? + projects << Project.trending + elsif params[:starred].present? && current_user + projects << current_user.viewable_starred_projects + else + projects << current_user.authorized_projects if current_user + projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present? + end projects end + + def by_ids(items) + project_ids_relation ? items.map { |item| item.where(id: project_ids_relation) } : items + end + + def union(items) + find_union(items, Project).with_route + end + + def by_personal(items) + (params[:personal].present? && current_user) ? items.personal(current_user) : items + end + + def by_visibilty_level(items) + params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items + end + + def by_tags(items) + params[:tag].present? ? items.tagged_with(params[:tag]) : items + end + + def by_search(items) + params[:search] ||= params[:name] + params[:search].present? ? items.search(params[:search]) : items + end + + def sort(items) + params[:sort].present? ? items.sort(params[:sort]) : items + end + + def by_archived(projects) + # Back-compatibility with the places where `params[:archived]` can be set explicitly to `false` + params[:non_archived] = !Gitlab::Utils.to_boolean(params[:archived]) if params.key?(:archived) + + params[:non_archived] ? projects.non_archived : projects + end end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index b7f091f334d..dc13386184e 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -95,7 +95,7 @@ class TodosFinder def projects(items) item_project_ids = items.reorder(nil).select(:project_id) - ProjectsFinder.new.execute(current_user, item_project_ids) + ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids).execute end def type? diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 5c89cbea3fc..3a5d1b97c36 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -25,8 +25,8 @@ module SortingHelper def projects_sort_options_hash options = { sort_value_name => sort_title_name, - sort_value_recently_updated => sort_title_recently_updated, - sort_value_oldest_updated => sort_title_oldest_updated, + sort_value_latest_activity => sort_title_latest_activity, + sort_value_oldest_activity => sort_title_oldest_activity, sort_value_recently_created => sort_title_recently_created, sort_value_oldest_created => sort_title_oldest_created } @@ -78,6 +78,14 @@ module SortingHelper 'Last updated' end + def sort_title_oldest_activity + 'Oldest updated' + end + + def sort_title_latest_activity + 'Last updated' + end + def sort_title_oldest_created 'Oldest created' end @@ -198,6 +206,14 @@ module SortingHelper 'updated_desc' end + def sort_value_oldest_activity + 'latest_activity_asc' + end + + def sort_value_latest_activity + 'latest_activity_desc' + end + def sort_value_oldest_created 'created_asc' end diff --git a/app/models/blob.rb b/app/models/blob.rb index f82126f8e65..801d3442803 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -58,6 +58,10 @@ class Blob < SimpleDelegator binary? && extname.downcase.delete('.') == 'sketch' end + def stl? + extname.downcase.delete('.') == 'stl' + end + def size_within_svg_limits? size <= MAXIMUM_SVG_SIZE end @@ -81,6 +85,8 @@ class Blob < SimpleDelegator 'notebook' elsif sketch? 'sketch' + elsif stl? + 'stl' elsif text? 'text' else diff --git a/app/models/project.rb b/app/models/project.rb index 2c631050042..7f2fc64977d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -350,10 +350,15 @@ class Project < ActiveRecord::Base end def sort(method) - if method == 'storage_size_desc' + case method.to_s + when 'storage_size_desc' # storage_size is a joined column so we need to # pass a string to avoid AR adding the table name reorder('project_statistics.storage_size DESC, projects.id DESC') + when 'latest_activity_desc' + reorder(last_activity_at: :desc) + when 'latest_activity_asc' + reorder(last_activity_at: :asc) else order_by(method) end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 4cc21696eb6..cb58c115d54 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -12,7 +12,7 @@ class GroupPolicy < BasePolicy can_read ||= globally_viewable can_read ||= member can_read ||= @user.admin? - can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any? + can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? can! :read_group if can_read # Only group masters and group owners can create new projects @@ -41,6 +41,6 @@ class GroupPolicy < BasePolicy return true if @subject.internal? && !@user.external? return true if @subject.users.include?(@user) - GroupProjectsFinder.new(@subject).execute(@user).any? + GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any? end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 5a53b973059..582d5c47b66 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -39,7 +39,7 @@ module MergeRequests private # Returns all origin and fork merge requests from `@project` satisfying passed arguments. - def merge_requests_for(source_branch, mr_states: [:opened]) + def merge_requests_for(source_branch, mr_states: [:opened, :reopened]) MergeRequest .with_state(mr_states) .where(source_branch: source_branch, source_project_id: @project.id) diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index c1549df5ac6..8409b592b72 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -8,7 +8,7 @@ module Search def execute group = Group.find_by(id: params[:group_id]) if params[:group_id].present? - projects = ProjectsFinder.new.execute(current_user) + projects = ProjectsFinder.new(current_user: current_user).execute if group projects = projects.inside_path(group.full_path) diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 5aae410a63f..3ca45fbf751 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -13,5 +13,7 @@ %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', 'aria-label': 'Add emoji', data: { title: 'Add emoji', placement: "bottom" } } - = icon('smile-o', class: "award-control-icon award-control-icon-normal") + %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') + %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley') + %span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile') = icon('spinner spin', class: "award-control-icon award-control-icon-loading") diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index 1bbd4602ecf..8843d4e7c84 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -1,4 +1,4 @@ -- publicish_project_count = ProjectsFinder.new.execute(current_user).count +- publicish_project_count = ProjectsFinder.new(current_user: current_user).execute.count .blank-state.blank-state-welcome %h2.blank-state-welcome-title Welcome to GitLab diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index f6d8bb08a64..a611481a0a4 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -23,7 +23,7 @@ %title= page_title(site_name) %meta{ name: "description", content: page_description } - = favicon_link_tag favicon + = favicon_link_tag favicon, id: 'favicon' = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" diff --git a/app/views/projects/blob/_stl.html.haml b/app/views/projects/blob/_stl.html.haml new file mode 100644 index 00000000000..a9332a0eeb6 --- /dev/null +++ b/app/views/projects/blob/_stl.html.haml @@ -0,0 +1,12 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('stl_viewer') + +.file-content.is-stl-loading + .text-center#js-stl-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } } + = icon('spinner spin 2x', class: 'prepend-top-default append-bottom-default', 'aria-hidden' => 'true', 'aria-label' => 'Loading') + .text-center.prepend-top-default.append-bottom-default.stl-controls + .btn-group + %button.btn.btn-default.btn-sm.js-material-changer{ data: { type: 'wireframe' } } + Wireframe + %button.btn.btn-default.btn-sm.active.js-material-changer{ data: { type: 'default' } } + Solid diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml index e27281d6917..b4102fcf103 100644 --- a/app/views/projects/environments/_metrics_button.html.haml +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -1,6 +1,6 @@ - environment = local_assigns.fetch(:environment) -- return unless environment.has_metrics? && can?(current_user, :read_environment, environment) +- return unless can?(current_user, :read_environment, environment) = link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do = icon('area-chart') diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index 92dc58cd38d..2e54af698aa 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -5,7 +5,7 @@ = page_specific_javascript_bundle_tag('monitoring') = render "projects/pipelines/head" -%div{ class: container_class } +.prometheus-container{ class: container_class, 'data-has-metrics': "#{@environment.has_metrics?}" } .top-area .row .col-sm-6 @@ -16,13 +16,68 @@ .col-sm-6 .nav-controls = render 'projects/deployments/actions', deployment: @environment.last_deployment - .row - .col-sm-12 - %h4 - CPU utilization - %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } - .row - .col-sm-12 - %h4 - Memory usage - %svg.prometheus-graph{ 'graph-type' => 'memory_values' } + .prometheus-state + .js-getting-started.hidden + .row + .col-md-4.col-md-offset-4.state-svg + = render "shared/empty_states/monitoring/getting_started.svg" + .row + .col-md-6.col-md-offset-3 + %h4.text-center.state-title + Get started with performance monitoring + .row + .col-md-6.col-md-offset-3 + .description-text.text-center.state-description + Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments. + = link_to help_page_path('administration/monitoring/prometheus/index.md') do + Learn more about performance monitoring + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + = link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'), class: 'btn btn-success' do + Configure Prometheus + .js-loading.hidden + .row + .col-md-4.col-md-offset-4.state-svg + = render "shared/empty_states/monitoring/loading.svg" + .row + .col-md-6.col-md-offset-3 + %h4.text-center.state-title + Waiting for performance data + .row + .col-md-6.col-md-offset-3 + .description-text.text-center.state-description + Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available. + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + = link_to help_page_path('administration/monitoring/prometheus/index.md'), class: 'btn btn-success' do + View documentation + .js-unable-to-connect.hidden + .row + .col-md-4.col-md-offset-4.state-svg + = render "shared/empty_states/monitoring/unable_to_connect.svg" + .row + .col-md-6.col-md-offset-3 + %h4.text-center.state-title + Unable to connect to Prometheus server + .row + .col-md-6.col-md-offset-3 + .description-text.text-center.state-description + Ensure connectivity is available from the GitLab server to the + = link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus') do + Prometheus server + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + = link_to help_page_path('administration/monitoring/prometheus/index.md'), class:'btn btn-success' do + View documentation + + .prometheus-graphs + .row + .col-sm-12 + %h4 + CPU utilization + %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } + .row + .col-sm-12 + %h4 + Memory usage + %svg.prometheus-graph{ 'graph-type' => 'memory_values' } diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 0b0fb7854c2..c716b69b35b 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -12,6 +12,7 @@ merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", check_enable: #{@merge_request.unchecked? ? "true" : "false"}, ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + pipeline_status_url: "#{pipeline_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 6c0e6d48d6c..18afa811bad 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -59,7 +59,9 @@ - if note.emoji_awardable? = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do = icon('spinner spin') - = icon('smile-o', class: 'link-highlight') + %span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') + %span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley') + %span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile') - if note_editable = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do diff --git a/app/views/shared/empty_states/monitoring/_getting_started.svg b/app/views/shared/empty_states/monitoring/_getting_started.svg new file mode 100644 index 00000000000..db7a1c2e708 --- /dev/null +++ b/app/views/shared/empty_states/monitoring/_getting_started.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="0" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="2" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="4" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="1" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="5" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="matrix(.99619.08716-.08716.99619 19.08-16.813)" rx="10"/><g transform="matrix(.96593.25882-.25882.96593 227.1 57.47)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g transform="translate(24.368 36.951)"><path fill="#d2caea" fill-rule="nonzero" d="m71.785 44.2c.761.296 1.625.099 2.184-.496l35.956-38.34c.756-.806.715-2.071-.091-2.827-.806-.756-2.071-.715-2.827.091l-35.03 37.36-41.888-16.285c-.749-.291-1.6-.106-2.16.471l-26.368 27.16c-.769.793-.751 2.059.042 2.828.793.769 2.059.751 2.828-.042l25.444-26.21 41.911 16.294"/><g fill="#fff"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="matrix(.99619-.08716.08716.99619-12.703 10.717)" rx="10"/><g transform="matrix(.99619.08716-.08716.99619 126.61 137.8)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="m84.67 28.41c18.225 0 33 15.07 33 33.651h-33v-33.651" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="m78.67 66.41h30c1.105 0 2 .895 2 2 0 18.778-15.222 34-34 34-18.778 0-34-15.222-34-34 0-18.778 15.222-34 34-34 1.105 0 2 .895 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28h-29.934c-1.105 0-2-.895-2-2v-29.934c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="matrix(.99619-.08716.08716.99619 30 88.03)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g transform="translate(42 34)"><path fill="#fef0ea" d="m0 13.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391v49.609h-12v-49.609"/><path fill="#fb722e" d="m66 21.406c0-.777.628-1.406 1.4-1.406h9.2c.773 0 1.4.624 1.4 1.406v41.594h-12v-41.594"/><path fill="#6b4fbb" d="m22 1.404c0-.776.628-1.404 1.4-1.404h9.2c.773 0 1.4.624 1.4 1.404v61.6h-12v-61.6"/><path fill="#d2caea" d="m44 39.4c0-.772.628-1.398 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602h-12v-23.602"/></g></g><g fill="#fee8dc"><path d="m6.226 94.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" transform="matrix(.70711.70711-.70711.70711 66.33 22.317)"/><path d="m312.78 53.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 126.1-206.88)"/></g><path fill="#e1dcf1" d="m124.78 12.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711 31.05 90.51)"/><path fill="#d2caea" d="m374.78 244.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711-59.779 335.24)"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/empty_states/monitoring/_loading.svg b/app/views/shared/empty_states/monitoring/_loading.svg new file mode 100644 index 00000000000..6bbd7a6c5b9 --- /dev/null +++ b/app/views/shared/empty_states/monitoring/_loading.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="C" width="161" height="100" x="92" y="181" rx="10"/><rect id="E" width="151" height="32" x="20" rx="10"/><rect id="G" width="191" height="62" y="10" rx="10"/><circle id="I" cx="23" cy="41" r="9"/><circle id="4" cx="36.5" cy="36.5" r="36.5"/><circle id="8" cx="262.5" cy="169.5" r="15.5"/><circle id="A" cx="79.5" cy="169.5" r="15.5"/><circle id="K" cx="45" cy="41" r="9"/><circle id="0" cx="30.5" cy="30.5" r="30.5"/><circle id="2" cx="18" cy="34" r="3"/><ellipse id="6" cx="43.5" cy="43.5" rx="43.5" ry="43.5"/><mask id="H" width="191" height="62" x="0" y="0" fill="#fff"><use xlink:href="#G"/></mask><mask id="J" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#I"/></mask><mask id="D" width="161" height="100" x="0" y="0" fill="#fff"><use xlink:href="#C"/></mask><mask id="F" width="151" height="32" x="0" y="0" fill="#fff"><use xlink:href="#E"/></mask><mask id="9" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#8"/></mask><mask id="1" width="61" height="61" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="B" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#A"/></mask><mask id="3" width="6" height="6" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="7" width="87" height="87" x="0" y="0" fill="#fff"><use xlink:href="#6"/></mask><mask id="L" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#K"/></mask><mask id="5" width="73" height="73" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(28 2)"><g transform="translate(133 87)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><path stroke="#d2caea" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="m19 32l2-9 5 17 4-12 4 5 6-10 3 5"/><g fill="#fff" stroke="#fb722e"><use stroke-width="4" mask="url(#3)" xlink:href="#2"/><circle cx="44" cy="30" r="2" stroke-width="2"/></g></g><g transform="translate(188 29)"><circle cx="36.5" cy="41.5" r="36.5" fill="#f9f9f9"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><rect width="27" height="4" x="23" y="27" fill="#d2caea" rx="2"/><rect width="10.5" height="4" x="23" y="27" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="36" fill="#d2caea" rx="2"/><rect width="19" height="4" x="23" y="36" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="45" fill="#d2caea" rx="2"/><rect width="7" height="4" x="23" y="45" fill="#6b4fbb" rx="2"/></g><path fill="#eee" fill-rule="nonzero" d="m247 292v1c0 5.519-4.469 9.993-10.01 9.993h-125.99c-5.177 0-9.436-3.927-9.954-8.96 1.348.998 2.957 1.666 4.705 1.883 1.027 1.835 2.992 3.077 5.248 3.077h125.99c2.485 0 4.611-1.497 5.526-3.637 1.796-.675 3.347-1.852 4.48-3.359m1.947-8.962c-.518 5.03-4.774 8.958-9.95 8.958h-131.99c-4.929 0-9.03-3.563-9.851-8.25 1.382.767 2.964 1.216 4.649 1.248 1.037 1.794 2.978 3 5.202 3h131.99c2.255 0 4.219-1.241 5.245-3.076 1.748-.216 3.356-.883 4.705-1.882"/><g transform="translate(79)"><ellipse cx="43.5" cy="47.5" fill="#f9f9f9" rx="43.5" ry="43.5"/><g fill="#fff"><g stroke="#eee"><use stroke-width="8" mask="url(#7)" xlink:href="#6"/><path stroke-width="4" d="m18.595 49c2.515 11.44 12.71 20 24.905 20 14.08 0 25.5-11.417 25.5-25.5 0-12.195-8.56-22.391-20-24.905v15.959c3 1.848 5 5.164 5 8.946 0 5.799-4.701 10.5-10.5 10.5-3.782 0-7.098-2-8.946-5h-15.959" stroke-linejoin="round"/></g><path stroke="#d2caea" stroke-width="4" d="m18 44c-.003-.166-.005-.333-.005-.5 0-14.08 11.417-25.5 25.5-25.5.167 0 .334.002.5.005v15.01c-.166-.008-.332-.012-.5-.012-5.799 0-10.5 4.701-10.5 10.5 0 .168.004.334.012.5h-15.01" stroke-linejoin="round"/></g></g><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#9)" xlink:href="#8"/><use mask="url(#B)" xlink:href="#A"/><use mask="url(#D)" xlink:href="#C"/></g><g fill="#eee"><rect width="15" height="2" x="226" y="247" rx="1"/><rect width="15" height="2" x="226" y="242" rx="1"/><rect width="15" height="2" x="226" y="252" rx="1"/></g><rect width="10" height="52" x="118" y="196" fill="#d2caea" rx="2"/><rect width="10" height="47" x="154" y="196" fill="#6b4fbb" rx="2"/><rect width="10" height="37" x="190" y="196" fill="#d2caea" rx="2"/><g fill="#fee8dc"><rect width="10" height="52" x="132" y="185" rx="2"/><rect width="10" height="38" x="168" y="185" rx="2"/></g><rect width="10" height="58" x="204" y="185" fill="#fb722e" rx="2"/><g transform="translate(76 128)"><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#F)" xlink:href="#E"/><use mask="url(#H)" xlink:href="#G"/></g><g fill="#d2caea"><rect width="16" height="4" x="156" y="35" rx="2"/><rect width="16" height="4" x="156" y="43" rx="2"/></g><g fill="#fff" stroke-width="8"><use stroke="#fee8dc" mask="url(#J)" xlink:href="#I"/><use stroke="#fb722e" mask="url(#L)" xlink:href="#K"/></g></g><g fill="#fb722e"><path d="m6.226 220.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" opacity=".2" transform="matrix(.70711.70711-.70711.70711 155.43 59.22)"/><path d="m256.23 9.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" opacity=".2" transform="matrix(.70711.70711-.70711.70711 79.45-179.36)"/></g><path fill="#fee8dc" d="m312.78 150.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 194.69-178.47)"/><path fill="#6b4fbb" d="m43.778 80.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" opacity=".2" transform="matrix(.70711-.70711.70711.70711-40.761 53.15)"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/empty_states/monitoring/_unable_to_connect.svg b/app/views/shared/empty_states/monitoring/_unable_to_connect.svg new file mode 100644 index 00000000000..62537d87d5d --- /dev/null +++ b/app/views/shared/empty_states/monitoring/_unable_to_connect.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><use id="0" xlink:href="#E"/><use id="2" xlink:href="#E"/><use id="4" xlink:href="#E"/><path id="6" d="m74 93h26v47h-26z"/><path id="8" d="m74 93h26v47h-26z"/><rect id="A" width="65" height="14" x="55" y="135" rx="4"/><rect id="C" width="175" height="118" rx="10"/><rect id="E" width="159" rx="10" height="56"/><rect id="F" width="160" y="2" rx="10" height="56" fill="#f9f9f9"/><mask id="B" width="65" height="14" x="0" y="0" fill="#fff"><use xlink:href="#A"/></mask><mask id="9" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#8"/></mask><mask id="D" width="175" height="118" x="0" y="0" fill="#fff"><use xlink:href="#C"/></mask><mask id="7" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#6"/></mask><mask id="3" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="1" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="5" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(1 65)"><g transform="translate(244)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g fill-rule="nonzero"><path fill="#fb722e" d="m134 31c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m117 31c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6m-17-4c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><g fill="#d2caea"><rect width="50" height="4" x="19" y="20" rx="2"/><rect width="50" height="4" x="19" y="34" rx="2"/></g><g transform="translate(0 59)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#fee8dc" d="m134 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fb722e" d="m117 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m100 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><rect width="50" height="4" x="19" y="19" fill="#d2caea" rx="2" id="G"/><rect width="50" height="4" x="19" y="33" fill="#d2caea" rx="2" id="H"/></g><g transform="translate(0 118)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g fill-rule="nonzero"><path fill="#fb722e" d="m134 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m117 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6m-17-4c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><use xlink:href="#G"/><use xlink:href="#H"/></g></g><g transform="translate(163 55)"><g fill="#eee"><rect width="29" height="4" y="29" rx="2"/><rect width="28" height="4" x="55" y="29" rx="2"/></g><g transform="translate(16)"><circle cx="30" cy="30" r="24" fill="#fef0ea"/><g fill="#fb722e"><circle cx="30.5" cy="30.5" r="30.5" opacity=".1"/><circle cx="30.5" cy="30.5" r="19.5" opacity=".1"/></g><circle cx="30.5" cy="30.5" r="13.5" fill="#fff"/><path fill="#fb722e" d="m32.621 30.5l2.481-2.481c.586-.586.58-1.529-.006-2.115-.59-.59-1.533-.589-2.115-.006l-2.481 2.481-2.481-2.481c-.586-.586-1.529-.58-2.115.006-.59.59-.589 1.533-.006 2.115l2.481 2.481-2.481 2.481c-.586.586-.58 1.529.006 2.115.59.59 1.533.589 2.115.006l2.481-2.481 2.481 2.481c.586.586 1.529.58 2.115-.006.59-.59.589-1.533.006-2.115l-2.481-2.481"/></g></g><g transform="translate(0 13)"><rect width="65" height="14" x="55" y="137" fill="#f9f9f9" rx="4"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#7)" xlink:href="#6"/><rect width="175" height="118" y="3" fill="#f9f9f9" rx="10"/><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#9)" xlink:href="#8"/><use mask="url(#B)" xlink:href="#A"/><use mask="url(#D)" xlink:href="#C"/></g><g fill-rule="nonzero"><path fill="#eee" d="m163 105v-93h-152v93h152m-156-93.01c0-2.204 1.797-3.99 3.995-3.99h152.01c2.206 0 3.995 1.796 3.995 3.99v93.02c0 2.204-1.797 3.99-3.995 3.99h-152.01c-2.206 0-3.995-1.796-3.995-3.99v-93.02"/><path fill="#d2caea" d="m86 92c-11.598 0-21-9.402-21-21 0-11.598 9.402-21 21-21 11.598 0 21 9.402 21 21 0 11.598-9.402 21-21 21m0-4c9.389 0 17-7.611 17-17 0-9.389-7.611-17-17-17-9.389 0-17 7.611-17 17 0 9.389 7.611 17 17 17"/></g><path fill="#6b4fbb" d="m83 63c0-1.659 1.347-3 3-3 1.657 0 3 1.342 3 3v7.993c0 1.659-1.347 3-3 3-1.657 0-3-1.342-3-3v-7.993m3 18.997c-1.657 0-3-1.343-3-3 0-1.657 1.343-3 3-3 1.657 0 3 1.343 3 3 0 1.657-1.343 3-3 3"/><g fill="#eee"><rect width="134" height="4" x="20" y="30" rx="2"/><rect width="14" height="4" x="20" y="20" rx="2"/><circle cx="87" cy="21" r="5"/></g></g></g></svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_emoji_slightly_smiling_face.svg b/app/views/shared/icons/_emoji_slightly_smiling_face.svg new file mode 100644 index 00000000000..56dbad91554 --- /dev/null +++ b/app/views/shared/icons/_emoji_slightly_smiling_face.svg @@ -0,0 +1 @@ +<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445c.195.625.556 1.131 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg> diff --git a/app/views/shared/icons/_emoji_smile.svg b/app/views/shared/icons/_emoji_smile.svg new file mode 100644 index 00000000000..ce645fee46f --- /dev/null +++ b/app/views/shared/icons/_emoji_smile.svg @@ -0,0 +1 @@ +<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg> diff --git a/app/views/shared/icons/_emoji_smiley.svg b/app/views/shared/icons/_emoji_smiley.svg new file mode 100644 index 00000000000..ddfae50e566 --- /dev/null +++ b/app/views/shared/icons/_emoji_smiley.svg @@ -0,0 +1 @@ +<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="nonzero"/></svg> diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index 2d25b8aad62..8939aeb6c3a 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -1,4 +1,4 @@ -- @sort ||= sort_value_recently_updated +- @sort ||= sort_value_latest_activity .dropdown - toggle_text = projects_sort_options_hash[@sort] = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 68a6fd76e70..b33ba2ed7c1 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -2,6 +2,8 @@ class RepositoryImportWorker include Sidekiq::Worker include DedicatedSidekiqQueue + sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_EXPIRATION + attr_accessor :project, :current_user def perform(project_id) @@ -12,7 +14,7 @@ class RepositoryImportWorker import_url: @project.import_url, path: @project.path_with_namespace) - project.update_column(:import_error, nil) + project.update_columns(import_jid: self.jid, import_error: nil) result = Projects::ImportService.new(project, current_user).execute diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb new file mode 100644 index 00000000000..bfc5e667bb6 --- /dev/null +++ b/app/workers/stuck_import_jobs_worker.rb @@ -0,0 +1,37 @@ +class StuckImportJobsWorker + include Sidekiq::Worker + include CronjobQueue + + IMPORT_EXPIRATION = 15.hours.to_i + + def perform + stuck_projects.find_in_batches(batch_size: 500) do |group| + jids = group.map(&:import_jid) + + # Find the jobs that aren't currently running or that exceeded the threshold. + completed_jids = Gitlab::SidekiqStatus.completed_jids(jids) + + if completed_jids.any? + completed_ids = group.select { |project| completed_jids.include?(project.import_jid) }.map(&:id) + + fail_batch!(completed_jids, completed_ids) + end + end + end + + private + + def stuck_projects + Project.select('id, import_jid').with_import_status(:started).where.not(import_jid: nil) + end + + def fail_batch!(completed_jids, completed_ids) + Project.where(id: completed_ids).update_all(import_status: 'failed', import_error: error_message) + + Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.join(', ')}") + end + + def error_message + "Import timed out. Import took longer than #{IMPORT_EXPIRATION} seconds" + end +end |