diff options
173 files changed, 1981 insertions, 243 deletions
diff --git a/PROCESS.md b/PROCESS.md index fead93bd4cf..2f331ee9169 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -33,7 +33,7 @@ core team members will mention this person. ### Merge request coaching Several people from the [GitLab team][team] are helping community members to get -their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done). +their contributions accepted by meeting our [Definition of done][done]. What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/. @@ -64,6 +64,49 @@ Merge requests may still be merged into master during this period, but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch. By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things. +### Between the 1st and the 7th + +These types of merge requests need special consideration: + +* **Large features**: a large feature is one that is highlighted in the kick-off + and the release blogpost; typically this will have its own channel in Slack + and a dedicated team with front-end, back-end, and UX. +* **Small features**: any other feature request. + +**Large features** must be with a maintainer **by the 1st**. It's OK if they +aren't completely done, but this allows the maintainer enough time to make the +decision about whether this can make it in before the freeze. If the maintainer +doesn't think it will make it, they should inform the developers working on it +and the Product Manager responsible for the feature. + +**Small features** must be with a reviewer (not necessarily maintainer) **by the +3rd**. + +Most merge requests from the community do not have a specific release +target. However, if one does and falls into either of the above categories, it's +the reviewer's responsibility to manage the above communication and assignment +on behalf of the community member. + +### On the 7th + +Merge requests should still be complete, following the +[definition of done][done]. The single exception is documentation, and this can +only be left until after the freeze if: + +* There is a follow-up issue to add documentation. +* It is assigned to the person writing documentation for this feature, and they + are aware of it. +* It is in the correct milestone, with the ~Deliverable label. + +All Community Edition merge requests from GitLab team members merged on the +freeze date (the 7th) should have a corresponding Enterprise Edition merge +request, even if there are no conflicts. This is to reduce the size of the +subsequent EE merge, as we often merge a lot to CE on the release date. For more +information, see +[limit conflicts with EE when developing on CE][limit_ee_conflicts]. + +### Between the 7th and the 22nd + Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release) and security issues will be cherry-picked into the stable branch. Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch. @@ -158,3 +201,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http [contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria ["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements [Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review +[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done +[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html 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 80490052389..9c7acc903d1 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -226,9 +226,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 12fd0668ff8..1f95d00baf8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -349,10 +349,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 diff --git a/changelogs/unreleased/12818-ci-status-as-favicon.yml b/changelogs/unreleased/12818-ci-status-as-favicon.yml new file mode 100644 index 00000000000..70194178d90 --- /dev/null +++ b/changelogs/unreleased/12818-ci-status-as-favicon.yml @@ -0,0 +1,4 @@ +--- +title: Show CI status as Favicon on Pipelines, Job and MR pages +merge_request: 10144 +author: diff --git a/changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml b/changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml new file mode 100644 index 00000000000..e4be16d4b37 --- /dev/null +++ b/changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml @@ -0,0 +1,4 @@ +--- +title: ProjectsFinder should handle more options +merge_request: 9682 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml b/changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml new file mode 100644 index 00000000000..7584995a11f --- /dev/null +++ b/changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml @@ -0,0 +1,4 @@ +--- +title: Enable creation of deploy keys with write access via the API +merge_request: +author: diff --git a/changelogs/unreleased/30024-owner-can-t-initialize-git-repo-for-new-project-in-group.yml b/changelogs/unreleased/30024-owner-can-t-initialize-git-repo-for-new-project-in-group.yml new file mode 100644 index 00000000000..c43d2732b9a --- /dev/null +++ b/changelogs/unreleased/30024-owner-can-t-initialize-git-repo-for-new-project-in-group.yml @@ -0,0 +1,4 @@ +--- +title: Disable invalid service templates +merge_request: +author: diff --git a/changelogs/unreleased/30291-reopen-mr.yml b/changelogs/unreleased/30291-reopen-mr.yml new file mode 100644 index 00000000000..4ae3e90eeba --- /dev/null +++ b/changelogs/unreleased/30291-reopen-mr.yml @@ -0,0 +1,4 @@ +--- +title: Include reopened MRs when searching for opened ones +merge_request: 10407 +author: diff --git a/changelogs/unreleased/30493-env-deploy-tooltip.yml b/changelogs/unreleased/30493-env-deploy-tooltip.yml new file mode 100644 index 00000000000..8fadaaa7bd2 --- /dev/null +++ b/changelogs/unreleased/30493-env-deploy-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: Fixes HTML structure that was preventing the tooltip to disappear when hovering + out of the button. +merge_request: +author: diff --git a/changelogs/unreleased/add-error-empty-states.yml b/changelogs/unreleased/add-error-empty-states.yml new file mode 100644 index 00000000000..ec6c7b6dce9 --- /dev/null +++ b/changelogs/unreleased/add-error-empty-states.yml @@ -0,0 +1,4 @@ +--- +title: Introduced error/empty states for the environments performance metrics +merge_request: 10271 +author: diff --git a/changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml b/changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml new file mode 100644 index 00000000000..c7b06e45607 --- /dev/null +++ b/changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml @@ -0,0 +1,4 @@ +--- +title: Add remove_concurrent_index to database helper +merge_request: 10441 +author: blackst0ne diff --git a/changelogs/unreleased/award-emoji-button-smiley-animation.yml b/changelogs/unreleased/award-emoji-button-smiley-animation.yml new file mode 100644 index 00000000000..31903aeb040 --- /dev/null +++ b/changelogs/unreleased/award-emoji-button-smiley-animation.yml @@ -0,0 +1,4 @@ +--- +title: Added award emoji animation and improved active state +merge_request: +author: diff --git a/changelogs/unreleased/feature-gh-rake-task.yml b/changelogs/unreleased/feature-gh-rake-task.yml new file mode 100644 index 00000000000..5b1d380690c --- /dev/null +++ b/changelogs/unreleased/feature-gh-rake-task.yml @@ -0,0 +1,4 @@ +--- +title: Add rake task to import GitHub projects from the command line +merge_request: +author: diff --git a/changelogs/unreleased/fix-gh-import-status-check.yml b/changelogs/unreleased/fix-gh-import-status-check.yml new file mode 100644 index 00000000000..d04bc2954a0 --- /dev/null +++ b/changelogs/unreleased/fix-gh-import-status-check.yml @@ -0,0 +1,4 @@ +--- +title: Periodically mark projects that are stuck in importing as failed +merge_request: +author: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index e8fef0000c1..f7cae84088e 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -349,6 +349,9 @@ Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsW Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *' Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker' +Settings.cron_jobs['stuck_import_jobs_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['stuck_import_jobs_worker']['cron'] ||= '15 * * * *' +Settings.cron_jobs['stuck_import_jobs_worker']['job_class'] = 'StuckImportJobsWorker' # # GitLab Shell diff --git a/config/webpack.config.js b/config/webpack.config.js index 69d8c5640f7..dc431e4d566 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -42,6 +42,7 @@ var config = { profile: './profile/profile_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js', snippet: './snippet/snippet_bundle.js', + stl_viewer: './blob/stl_viewer.js', terminal: './terminal/terminal_bundle.js', u2f: ['vendor/u2f'], users: './users/users_bundle.js', diff --git a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb index 7a8ed99c68f..178e4bf5ed3 100644 --- a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb +++ b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb index 4bb4204cebd..081df23f394 100644 --- a/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb +++ b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb index e35af38aac3..76bb6a09639 100644 --- a/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb +++ b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb index 6ca486c63d1..48f4495b0a4 100644 --- a/db/migrate/20160620115026_add_index_on_runners_locked.rb +++ b/db/migrate/20160620115026_add_index_on_runners_locked.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddIndexOnRunnersLocked < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb index a05a4c679e3..fec17ffb7f6 100644 --- a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb +++ b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexForPipelineUserId < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb index 75a3eb15124..12e11bc3fbe 100644 --- a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb +++ b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class MergeRequestDiffRemoveUniq < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160725104452_merge_request_diff_add_index.rb b/db/migrate/20160725104452_merge_request_diff_add_index.rb index 6d04242dd25..60d81e0bdc0 100644 --- a/db/migrate/20160725104452_merge_request_diff_add_index.rb +++ b/db/migrate/20160725104452_merge_request_diff_add_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class MergeRequestDiffAddIndex < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb b/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb index 5fd51cb65f1..6d7733762c8 100644 --- a/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb +++ b/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class RemoveBuildsEnableIndexOnProjects < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb b/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb index baf2e70b127..9c1511963f7 100644 --- a/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb +++ b/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddUniqueIndexToListsLabelId < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb index 3f074723b4a..30d98a0124e 100644 --- a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb +++ b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddDeletedAtToNamespaces < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160808085602_add_index_for_build_token.rb b/db/migrate/20160808085602_add_index_for_build_token.rb index 6c5d7268e72..0446b2f2e15 100644 --- a/db/migrate/20160808085602_add_index_for_build_token.rb +++ b/db/migrate/20160808085602_add_index_for_build_token.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexForBuildToken < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160810142633_remove_redundant_indexes.rb b/db/migrate/20160810142633_remove_redundant_indexes.rb index 8641c6ffa8f..d7ab022d7bc 100644 --- a/db/migrate/20160810142633_remove_redundant_indexes.rb +++ b/db/migrate/20160810142633_remove_redundant_indexes.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class RemoveRedundantIndexes < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb index 8f693e97a58..843643c4e95 100644 --- a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb +++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddIndexToNoteDiscussionId < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb index bcad3416d04..a004a3802a2 100644 --- a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb +++ b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddIncomingEmailTokenToUsers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb index e20e693f3aa..917c2b0c521 100644 --- a/db/migrate/20160919145149_add_group_id_to_labels.rb +++ b/db/migrate/20160919145149_add_group_id_to_labels.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddGroupIdToLabels < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160920160832_add_index_to_labels_title.rb b/db/migrate/20160920160832_add_index_to_labels_title.rb index 19f7b1076a7..e38c655baee 100644 --- a/db/migrate/20160920160832_add_index_to_labels_title.rb +++ b/db/migrate/20160920160832_add_index_to_labels_title.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexToLabelsTitle < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161017125927_add_unique_index_to_labels.rb b/db/migrate/20161017125927_add_unique_index_to_labels.rb index f2b56ebfb7b..b8f6a803a0a 100644 --- a/db/migrate/20161017125927_add_unique_index_to_labels.rb +++ b/db/migrate/20161017125927_add_unique_index_to_labels.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddUniqueIndexToLabels < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb index 35ad22b6c01..b77daf12f68 100644 --- a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb +++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb b/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb index 4b1b29e1265..f263377fbc6 100644 --- a/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb +++ b/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddUniqueIndexToSubscriptions < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161106185620_add_project_import_data_project_index.rb b/db/migrate/20161106185620_add_project_import_data_project_index.rb index 94b8ddd46f5..b3746dc4f6c 100644 --- a/db/migrate/20161106185620_add_project_import_data_project_index.rb +++ b/db/migrate/20161106185620_add_project_import_data_project_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddProjectImportDataProjectIndex < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161124111395_add_index_to_parent_id.rb b/db/migrate/20161124111395_add_index_to_parent_id.rb index 73f9d92bb22..065643e058d 100644 --- a/db/migrate/20161124111395_add_index_to_parent_id.rb +++ b/db/migrate/20161124111395_add_index_to_parent_id.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddIndexToParentId < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161128142110_remove_unnecessary_indexes.rb b/db/migrate/20161128142110_remove_unnecessary_indexes.rb index 8100287ef48..699a9368eb3 100644 --- a/db/migrate/20161128142110_remove_unnecessary_indexes.rb +++ b/db/migrate/20161128142110_remove_unnecessary_indexes.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class RemoveUnnecessaryIndexes < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20161202152035_add_index_to_routes.rb b/db/migrate/20161202152035_add_index_to_routes.rb index 6d6c8906204..552b5fab68c 100644 --- a/db/migrate/20161202152035_add_index_to_routes.rb +++ b/db/migrate/20161202152035_add_index_to_routes.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddIndexToRoutes < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb index 2977917f2d1..7d39c2ae626 100644 --- a/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb +++ b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161206153751_add_path_index_to_namespace.rb b/db/migrate/20161206153751_add_path_index_to_namespace.rb index b0bac7d121e..623037e35cd 100644 --- a/db/migrate/20161206153751_add_path_index_to_namespace.rb +++ b/db/migrate/20161206153751_add_path_index_to_namespace.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddPathIndexToNamespace < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb index cc9d4974baa..9296ae36aa5 100644 --- a/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb +++ b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161206153754_add_name_index_to_namespace.rb b/db/migrate/20161206153754_add_name_index_to_namespace.rb index b3f3cb68a99..2bbd039ff27 100644 --- a/db/migrate/20161206153754_add_name_index_to_namespace.rb +++ b/db/migrate/20161206153754_add_name_index_to_namespace.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddNameIndexToNamespace < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb index 5ff0f5bae4d..15093350f12 100644 --- a/db/migrate/20161207231621_create_environment_name_unique_index.rb +++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb index ede0316e860..42a90091b87 100644 --- a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb +++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb index 53f4c6bbb18..76db5179795 100644 --- a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb +++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddLowerPathIndexToRoutes < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb b/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb index 4ea953f2b78..c006098fafd 100644 --- a/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb +++ b/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexToCiBuildsForStatusRunnerIdAndType < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb b/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb index 620befcf4d7..00aa0b311b1 100644 --- a/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb +++ b/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexToCiRunnersForIsShared < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb index 629b49436e3..f256251516a 100644 --- a/db/migrate/20170130204620_add_index_to_project_authorizations.rb +++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexToProjectAuthorizations < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers @@ -6,7 +7,9 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration disable_ddl_transaction! def up - add_concurrent_index(:project_authorizations, :project_id) + unless index_exists?(:project_authorizations, :project_id) + add_concurrent_index(:project_authorizations, :project_id) + end end def down diff --git a/db/migrate/20170131221752_add_relative_position_to_issues.rb b/db/migrate/20170131221752_add_relative_position_to_issues.rb index 1baad0893e3..fd18d8b6a60 100644 --- a/db/migrate/20170131221752_add_relative_position_to_issues.rb +++ b/db/migrate/20170131221752_add_relative_position_to_issues.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddRelativePositionToIssues < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb index 31ef458c44f..b1b0a601007 100644 --- a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb +++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb index 70fb0ef12f9..2c20f6a48ab 100644 --- a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb +++ b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb index 07d4f8af27f..c31057f2617 100644 --- a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb +++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb index 2d8329b7862..ba4976a5ce8 100644 --- a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb +++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddIndexToUserAgentDetail < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb index 8a96a784c97..884c4e569d6 100644 --- a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb +++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers DOWNTIME = false diff --git a/db/migrate/20170216141440_drop_index_for_builds_project_status.rb b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb index a2839f52d89..56ad566ca67 100644 --- a/db/migrate/20170216141440_drop_index_for_builds_project_status.rb +++ b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class DropIndexForBuildsProjectStatus < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers DOWNTIME = false diff --git a/db/migrate/20170222143500_remove_old_project_id_columns.rb b/db/migrate/20170222143500_remove_old_project_id_columns.rb index eac93e8e407..268144a2552 100644 --- a/db/migrate/20170222143500_remove_old_project_id_columns.rb +++ b/db/migrate/20170222143500_remove_old_project_id_columns.rb @@ -1,3 +1,4 @@ +# rubocop:disable RemoveIndex class RemoveOldProjectIdColumns < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers disable_ddl_transaction! diff --git a/db/migrate/20170313213916_add_index_to_user_ghost.rb b/db/migrate/20170313213916_add_index_to_user_ghost.rb index c429039c275..fe5847ed225 100644 --- a/db/migrate/20170313213916_add_index_to_user_ghost.rb +++ b/db/migrate/20170313213916_add_index_to_user_ghost.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class AddIndexToUserGhost < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170330141723_disable_invalid_service_templates2.rb b/db/migrate/20170330141723_disable_invalid_service_templates2.rb new file mode 100644 index 00000000000..8424e56d8a1 --- /dev/null +++ b/db/migrate/20170330141723_disable_invalid_service_templates2.rb @@ -0,0 +1,18 @@ +# This is the same as DisableInvalidServiceTemplates. Later migrations may have +# inadventently enabled some invalid templates again. +# +class DisableInvalidServiceTemplates2 < ActiveRecord::Migration + DOWNTIME = false + + unless defined?(Service) + class Service < ActiveRecord::Base + self.inheritance_column = nil + end + end + + def up + Service.where(template: true, active: true).each do |template| + template.update(active: false) unless template.valid? + end + end +end diff --git a/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb b/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb index 8316ee9eb9f..0237c3189a5 100644 --- a/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb +++ b/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb @@ -1,6 +1,7 @@ # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. +# rubocop:disable RemoveIndex class RemoveIndexForUsersCurrentSignInAt < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170405080720_add_import_jid_to_projects.rb b/db/migrate/20170405080720_add_import_jid_to_projects.rb new file mode 100644 index 00000000000..55b87b9d56d --- /dev/null +++ b/db/migrate/20170405080720_add_import_jid_to_projects.rb @@ -0,0 +1,9 @@ +class AddImportJidToProjects < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :projects, :import_jid, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index ccf18d07179..582f68cbee7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170402231018) do +ActiveRecord::Schema.define(version: 20170405080720) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -920,6 +920,7 @@ ActiveRecord::Schema.define(version: 20170402231018) do t.text "description_html" t.boolean "only_allow_merge_if_all_discussions_are_resolved" t.boolean "printing_merge_request_link_enabled", default: true, null: false + t.string "import_jid" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md new file mode 100644 index 00000000000..affb4d17861 --- /dev/null +++ b/doc/administration/raketasks/github_import.md @@ -0,0 +1,36 @@ +# GitHub import + +>**Note:** +> +> - [Introduced][ce-10308] in GitLab 9.1. +> - You need a personal access token in order to retrieve and import GitHub +> projects. You can get it from: https://github.com/settings/tokens +> - You also need to pass an username as the second argument to the rake task +> which will become the owner of the project. + +To import a project from the list of your GitHub projects available: + +```bash +# Omnibus installations +sudo gitlab-rake import:github[access_token,root,foo/bar] + +# Installations from source +bundle exec rake import:github[access_token,root,foo/bar] RAILS_ENV=production +``` + +In this case, `access_token` is your GitHub personal access token, `root` +is your GitLab username, and `foo/bar` is the new GitLab namespace/project that +will get created from your GitHub project. Subgroups are also possible: `foo/foo/bar`. + + +To import a specific GitHub project (named `foo/github_repo` here): + +```bash +# Omnibus installations +sudo gitlab-rake import:github[access_token,root,foo/bar,foo/github_repo] + +# Installations from source +bundle exec rake import:github[access_token,root,foo/bar,foo/github_repo] RAILS_ENV=production +``` + +[ce-10308]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10308 diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index fd8335d251e..587922d0136 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -58,10 +58,22 @@ migration was tested. ## Removing indices -If you need to remove index, please add a condition like in following example: +When removing an index make sure to use the method `remove_concurrent_index` instead +of the regular `remove_index` method. The `remove_concurrent_index` method +automatically drops concurrent indexes when using PostgreSQL, removing the +need for downtime. To use this method you must disable transactions by calling +the method `disable_ddl_transaction!` in the body of your migration class like +so: ```ruby -remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) +class MyMigration < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + def up + remove_concurrent_index :table_name, :column_name if index_exists?(:table_name, :column_name) + end +end ``` ## Adding indices diff --git a/generator_templates/active_record/migration/create_table_migration.rb b/generator_templates/active_record/migration/create_table_migration.rb index aad8626a720..59a9d37df0f 100644 --- a/generator_templates/active_record/migration/create_table_migration.rb +++ b/generator_templates/active_record/migration/create_table_migration.rb @@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration # migration requires downtime. # DOWNTIME_REASON = '' - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. # # To disable transactions uncomment the following line and remove these # comments: diff --git a/generator_templates/active_record/migration/migration.rb b/generator_templates/active_record/migration/migration.rb index 825bc8bdf61..08752b3af50 100644 --- a/generator_templates/active_record/migration/migration.rb +++ b/generator_templates/active_record/migration/migration.rb @@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration # migration requires downtime. # DOWNTIME_REASON = '' - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. # # To disable transactions uncomment the following line and remove these # comments: diff --git a/generator_templates/rails/post_deployment_migration/migration.rb b/generator_templates/rails/post_deployment_migration/migration.rb index 1a7b8d5bf35..f2dff84b618 100644 --- a/generator_templates/rails/post_deployment_migration/migration.rb +++ b/generator_templates/rails/post_deployment_migration/migration.rb @@ -6,12 +6,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration DOWNTIME = false - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. # # To disable transactions uncomment the following line and remove these # comments: diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index b888ede6fe8..8a54f7f3f05 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -47,6 +47,7 @@ module API params do requires :key, type: String, desc: 'The new deploy key' requires :title, type: String, desc: 'The name of the deploy key' + optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository" end post ":id/deploy_keys" do params[:key].strip! diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 8f3799417e3..605769eddde 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -142,7 +142,7 @@ module API end get ":id/projects" do group = find_group!(params[:id]) - projects = GroupProjectsFinder.new(group).execute(current_user) + projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute projects = filter_projects(projects) entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project present paginate(projects), with: entity, current_user: current_user diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 0fbe1669d45..766fbea53e6 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -84,7 +84,7 @@ module API end get do entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails - present_projects ProjectsFinder.new.execute(current_user), with: entity, statistics: params[:statistics] + present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity, statistics: params[:statistics] end desc 'Create new project' do diff --git a/lib/api/users.rb b/lib/api/users.rb index 530ca0b5235..992a751b37d 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -341,7 +341,7 @@ module API not_found!('User') unless user events = user.events. - merge(ProjectsFinder.new.execute(current_user)). + merge(ProjectsFinder.new(current_user: current_user).execute). references(:project). with_associations. recent diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb index c5b37622d79..9b27411ae21 100644 --- a/lib/api/v3/groups.rb +++ b/lib/api/v3/groups.rb @@ -151,7 +151,7 @@ module API end get ":id/projects" do group = find_group!(params[:id]) - projects = GroupProjectsFinder.new(group).execute(current_user) + projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute projects = filter_projects(projects) entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project present paginate(projects), with: entity, current_user: current_user diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb index b753dbab381..ba9748ada59 100644 --- a/lib/api/v3/projects.rb +++ b/lib/api/v3/projects.rb @@ -107,7 +107,7 @@ module API end get '/visible' do entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails - present_projects ProjectsFinder.new.execute(current_user), with: entity + present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity end desc 'Get a projects list for authenticated user' do diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb index 5e18cecc431..f4cda3b2eba 100644 --- a/lib/api/v3/users.rb +++ b/lib/api/v3/users.rb @@ -138,7 +138,7 @@ module API not_found!('User') unless user events = user.events. - merge(ProjectsFinder.new.execute(current_user)). + merge(ProjectsFinder.new(current_user: current_user).execute). references(:project). with_associations. recent diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index fc445ab9483..525aa920328 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -26,6 +26,30 @@ module Gitlab add_index(table_name, column_name, options) end + # Removes an existed index, concurrently when supported + # + # On PostgreSQL this method removes an index concurrently. + # + # Example: + # + # remove_concurrent_index :users, :some_column + # + # See Rails' `remove_index` for more info on the available arguments. + def remove_concurrent_index(table_name, column_name, options = {}) + if transaction_open? + raise 'remove_concurrent_index can not be run inside a transaction, ' \ + 'you can disable transactions by calling disable_ddl_transaction! ' \ + 'in the body of your migration class' + end + + if Database.postgresql? + options = options.merge({ algorithm: :concurrently }) + disable_statement_timeout + end + + remove_index(table_name, options.merge({ column: column_name })) + end + # Adds a foreign key with only minimal locking on the tables involved. # # This method only requires minimal locking when using PostgreSQL. When diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb index 11e5f1b645c..ca8d3271541 100644 --- a/lib/gitlab/sidekiq_status.rb +++ b/lib/gitlab/sidekiq_status.rb @@ -72,6 +72,8 @@ module Gitlab # job_ids - The Sidekiq job IDs to check. # # Returns an array of true or false indicating job completion. + # true = job is still running + # false = job completed def self.job_status(job_ids) keys = job_ids.map { |jid| key_for(jid) } @@ -82,6 +84,17 @@ module Gitlab end end + # Returns the JIDs that are completed + # + # job_ids - The Sidekiq job IDs to check. + # + # Returns an array of completed JIDs + def self.completed_jids(job_ids) + Sidekiq.redis do |redis| + job_ids.reject { |jid| redis.exists(key_for(jid)) } + end + end + def self.key_for(jid) STATUS_KEY % jid end diff --git a/lib/gitlab/sidekiq_status/client_middleware.rb b/lib/gitlab/sidekiq_status/client_middleware.rb index d47609f490d..00983b3284a 100644 --- a/lib/gitlab/sidekiq_status/client_middleware.rb +++ b/lib/gitlab/sidekiq_status/client_middleware.rb @@ -2,7 +2,9 @@ module Gitlab module SidekiqStatus class ClientMiddleware def call(_, job, _, _) - Gitlab::SidekiqStatus.set(job['jid']) + status_expiration = job['status_expiration'] || Gitlab::SidekiqStatus::DEFAULT_EXPIRATION + + Gitlab::SidekiqStatus.set(job['jid'], status_expiration) yield end end diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake new file mode 100644 index 00000000000..350afeb5c0b --- /dev/null +++ b/lib/tasks/import.rake @@ -0,0 +1,204 @@ +require 'benchmark' +require 'rainbow/ext/string' +require_relative '../gitlab/shell_adapter' +require_relative '../gitlab/github_import/importer' + +class NewImporter < ::Gitlab::GithubImport::Importer + def execute + # Same as ::Gitlab::GithubImport::Importer#execute, but showing some progress. + puts 'Importing repository...'.color(:aqua) + import_repository unless project.repository_exists? + + puts 'Importing labels...'.color(:aqua) + import_labels + + puts 'Importing milestones...'.color(:aqua) + import_milestones + + puts 'Importing pull requests...'.color(:aqua) + import_pull_requests + + puts 'Importing issues...'.color(:aqua) + import_issues + + puts 'Importing issue comments...'.color(:aqua) + import_comments(:issues) + + puts 'Importing pull request comments...'.color(:aqua) + import_comments(:pull_requests) + + puts 'Importing wiki...'.color(:aqua) + import_wiki + + # Gitea doesn't have a Release API yet + # See https://github.com/go-gitea/gitea/issues/330 + unless project.gitea_import? + import_releases + end + + handle_errors + + project.repository.after_import + project.import_finish + + true + end + + def import_repository + begin + raise 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url) + + gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url) + rescue => e + project.repository.before_import if project.repository_exists? + + raise "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" + end + end +end + +class GithubImport + def self.run!(*args) + new(*args).run! + end + + def initialize(token, gitlab_username, project_path, extras) + @token = token + @project_path = project_path + @current_user = User.find_by_username(gitlab_username) + @github_repo = extras.empty? ? nil : extras.first + end + + def run! + @repo = GithubRepos.new(@token, @current_user, @github_repo).choose_one! + + raise 'No repo found!' unless @repo + + show_warning! + + @project = Project.find_by_full_path(@project_path) || new_project + + import! + end + + private + + def show_warning! + puts "This will import GH #{@repo.full_name.bright} into GL #{@project_path.bright} as #{@current_user.name}" + puts "Permission checks are ignored. Press any key to continue.".color(:red) + + STDIN.getch + + puts 'Starting the import...'.color(:green) + end + + def import! + import_url = @project.import_url.gsub(/\:\/\/(.*@)?/, "://#{@token}@") + @project.update(import_url: import_url) + + @project.import_start + + timings = Benchmark.measure do + NewImporter.new(@project).execute + end + + puts "Import finished. Timings: #{timings}".color(:green) + end + + def new_project + Project.transaction do + namespace_path, _sep, name = @project_path.rpartition('/') + namespace = find_or_create_namespace(namespace_path) + + Project.create!( + import_url: "https://#{@token}@github.com/#{@repo.full_name}.git", + name: name, + path: name, + description: @repo.description, + namespace: namespace, + visibility_level: visibility_level, + import_type: 'github', + import_source: @repo.full_name, + creator: @current_user + ) + end + end + + def find_or_create_namespace(names) + return @current_user.namespace if names == @current_user.namespace_path + return @current_user.namespace unless @current_user.can_create_group? + + names = params[:target_namespace].presence || names + full_path_namespace = Namespace.find_by_full_path(names) + + return full_path_namespace if full_path_namespace + + names.split('/').inject(nil) do |parent, name| + begin + namespace = Group.create!(name: name, + path: name, + owner: @current_user, + parent: parent) + namespace.add_owner(@current_user) + + namespace + rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid + Namespace.where(parent: parent).find_by_path_or_name(name) + end + end + end + + def full_path_namespace(names) + @full_path_namespace ||= Namespace.find_by_full_path(names) + end + + def visibility_level + @repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility + end +end + +class GithubRepos + def initialize(token, current_user, github_repo) + @token = token + @current_user = current_user + @github_repo = github_repo + end + + def choose_one! + return found_github_repo if @github_repo + + repos.each do |repo| + print "ID: #{repo[:id].to_s.bright} ".color(:green) + puts "- Name: #{repo[:full_name]}".color(:green) + end + + print 'ID? '.bright + + repos.find { |repo| repo[:id] == repo_id } + end + + def found_github_repo + repos.find { |repo| repo[:full_name] == @github_repo } + end + + def repo_id + @repo_id ||= STDIN.gets.chomp.to_i + end + + def repos + @repos ||= client.repos + end + + def client + @client ||= Gitlab::GithubImport::Client.new(@token, {}) + end +end + +namespace :import do + desc 'Import a GitHub project - Example: import:github[ToKeN,root,root/blah,my/github_repo] (optional my/github_repo)' + task :github, [:token, :gitlab_username, :project_path] => :environment do |_t, args| + abort 'Project path must be: namespace(s)/project_name'.color(:red) unless args.project_path.include?('/') + + GithubImport.run!(args.token, args.gitlab_username, args.project_path, args.extras) + end +end diff --git a/package.json b/package.json index 3f64d65d57a..312e38f7407 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ "raw-loader": "^0.5.1", "select2": "3.5.2-browserify", "stats-webpack-plugin": "^0.4.3", + "three": "^0.84.0", + "three-orbit-controls": "^82.1.0", + "three-stl-loader": "^1.0.4", "timeago.js": "^2.0.5", "underscore": "^1.8.3", "visibilityjs": "^1.2.4", diff --git a/rubocop/cop/migration/add_concurrent_index.rb b/rubocop/cop/migration/add_concurrent_index.rb index 332fb7dcbd7..69852f4d580 100644 --- a/rubocop/cop/migration/add_concurrent_index.rb +++ b/rubocop/cop/migration/add_concurrent_index.rb @@ -9,7 +9,7 @@ module RuboCop include MigrationHelpers MSG = '`add_concurrent_index` is not reversible so you must manually define ' \ - 'the `up` and `down` methods in your migration class, using `remove_index` in `down`'.freeze + 'the `up` and `down` methods in your migration class, using `remove_concurrent_index` in `down`'.freeze def on_send(node) return unless in_migration?(node) diff --git a/rubocop/cop/migration/remove_concurrent_index.rb b/rubocop/cop/migration/remove_concurrent_index.rb new file mode 100644 index 00000000000..268c49865cb --- /dev/null +++ b/rubocop/cop/migration/remove_concurrent_index.rb @@ -0,0 +1,29 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if `remove_concurrent_index` is used with `up`/`down` methods + # and not `change`. + class RemoveConcurrentIndex < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = '`remove_concurrent_index` is not reversible so you must manually define ' \ + 'the `up` and `down` methods in your migration class, using `add_concurrent_index` in `down`'.freeze + + def on_send(node) + return unless in_migration?(node) + return unless node.children[1] == :remove_concurrent_index + + node.each_ancestor(:def) do |def_node| + add_offense(def_node, :name) if method_name(def_node) == :change + end + end + + def method_name(node) + node.children[0] + end + end + end + end +end diff --git a/rubocop/cop/migration/remove_index.rb b/rubocop/cop/migration/remove_index.rb new file mode 100644 index 00000000000..613b35dd00d --- /dev/null +++ b/rubocop/cop/migration/remove_index.rb @@ -0,0 +1,26 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if indexes are removed in a concurrent manner. + class RemoveIndex < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = '`remove_index` requires downtime, use `remove_concurrent_index` instead'.freeze + + def on_def(node) + return unless in_migration?(node) + + node.each_descendant(:send) do |send_node| + add_offense(send_node, :selector) if method_name(send_node) == :remove_index + end + end + + def method_name(node) + node.children[1] + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index a50a522cf9d..d580aa6857a 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -5,3 +5,5 @@ require_relative 'cop/migration/add_column_with_default' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_index' +require_relative 'cop/migration/remove_concurrent_index' +require_relative 'cop/migration/remove_index' diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index dd93b439b2b..4e140102492 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -23,5 +23,9 @@ FactoryGirl.define do factory :another_deploy_key, class: 'DeployKey' do end end + + factory :write_access_key, class: 'DeployKey' do + can_push true + end end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index c90cc06a8f5..8bfe6f4d54b 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -86,17 +86,40 @@ feature 'Group', feature: true do describe 'create a nested group' do let(:group) { create(:group, path: 'foo') } - before do - visit subgroups_group_path(group) - click_link 'New Subgroup' + context 'as admin' do + before do + visit subgroups_group_path(group) + click_link 'New Subgroup' + end + + it 'creates a nested group' do + fill_in 'Group path', with: 'bar' + click_button 'Create group' + + expect(current_path).to eq(group_path('foo/bar')) + expect(page).to have_content("Group 'bar' was successfully created.") + end end - it 'creates a nested group' do - fill_in 'Group path', with: 'bar' - click_button 'Create group' + context 'as group owner' do + let(:user) { create(:user) } - expect(current_path).to eq(group_path('foo/bar')) - expect(page).to have_content("Group 'bar' was successfully created.") + before do + group.add_owner(user) + logout + login_as(user) + + visit subgroups_group_path(group) + click_link 'New Subgroup' + end + + it 'creates a nested group' do + fill_in 'Group path', with: 'bar' + click_button 'Create group' + + expect(current_path).to eq(group_path('foo/bar')) + expect(page).to have_content("Group 'bar' was successfully created.") + end end end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 52196ce49bd..c66b9a34b86 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -71,6 +71,22 @@ feature "New project", feature: true do end end end + + context "with subgroup namespace" do + let(:group) { create(:group, :private, owner: user) } + let(:subgroup) { create(:group, parent: group) } + + before do + group.add_master(user) + visit new_project_path(namespace_id: subgroup.id) + end + + it "selects the group namespace" do + namespace = find("#project_namespace_id option[selected]") + + expect(namespace.text).to eq subgroup.full_path + end + end end context 'Import project options' do diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb index ef97b061ca7..3c7c9bdcd08 100644 --- a/spec/finders/group_projects_finder_spec.rb +++ b/spec/finders/group_projects_finder_spec.rb @@ -3,8 +3,9 @@ require 'spec_helper' describe GroupProjectsFinder do let(:group) { create(:group) } let(:current_user) { create(:user) } + let(:options) { {} } - let(:finder) { described_class.new(source_user) } + let(:finder) { described_class.new(group: group, current_user: current_user, options: options) } let!(:public_project) { create(:empty_project, :public, group: group, path: '1') } let!(:private_project) { create(:empty_project, :private, group: group, path: '2') } @@ -18,22 +19,27 @@ describe GroupProjectsFinder do shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group) end + subject { finder.execute } + describe 'with a group member current user' do - before { group.add_user(current_user, Gitlab::Access::MASTER) } + before do + group.add_master(current_user) + end context "only shared" do - subject { described_class.new(group, only_shared: true).execute(current_user) } - it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) } + let(:options) { { only_shared: true } } + + it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) } end context "only owned" do - subject { described_class.new(group, only_owned: true).execute(current_user) } - it { is_expected.to eq([private_project, public_project]) } + let(:options) { { only_owned: true } } + + it { is_expected.to match_array([private_project, public_project]) } end context "all" do - subject { described_class.new(group).execute(current_user) } - it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) } + it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) } end end @@ -44,47 +50,57 @@ describe GroupProjectsFinder do end context "only shared" do + let(:options) { { only_shared: true } } + context "without external user" do - subject { described_class.new(group, only_shared: true).execute(current_user) } - it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) } + it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) } end context "with external user" do - before { current_user.update_attributes(external: true) } - subject { described_class.new(group, only_shared: true).execute(current_user) } - it { is_expected.to eq([shared_project_2, shared_project_1]) } + before do + current_user.update_attributes(external: true) + end + + it { is_expected.to match_array([shared_project_2, shared_project_1]) } end end context "only owned" do + let(:options) { { only_owned: true } } + context "without external user" do - before { private_project.team << [current_user, Gitlab::Access::MASTER] } - subject { described_class.new(group, only_owned: true).execute(current_user) } - it { is_expected.to eq([private_project, public_project]) } + before do + private_project.team << [current_user, Gitlab::Access::MASTER] + end + + it { is_expected.to match_array([private_project, public_project]) } end context "with external user" do - before { current_user.update_attributes(external: true) } - subject { described_class.new(group, only_owned: true).execute(current_user) } - it { is_expected.to eq([public_project]) } - end + before do + current_user.update_attributes(external: true) + end - context "all" do - subject { described_class.new(group).execute(current_user) } - it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, public_project]) } + it { is_expected.to eq([public_project]) } end end + + context "all" do + it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) } + end end describe "no user" do context "only shared" do - subject { described_class.new(group, only_shared: true).execute(current_user) } - it { is_expected.to eq([shared_project_3, shared_project_1]) } + let(:options) { { only_shared: true } } + + it { is_expected.to match_array([shared_project_3, shared_project_1]) } end context "only owned" do - subject { described_class.new(group, only_owned: true).execute(current_user) } - it { is_expected.to eq([public_project]) } + let(:options) { { only_owned: true } } + + it { is_expected.to eq([public_project]) } end end end diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index e44e7434c80..148adcffe3b 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -21,38 +21,144 @@ describe ProjectsFinder do create(:empty_project, :private, name: 'D', path: 'D') end - let(:finder) { described_class.new } + let(:params) { {} } + let(:current_user) { user } + let(:project_ids_relation) { nil } + let(:finder) { described_class.new(params: params, current_user: current_user, project_ids_relation: project_ids_relation) } + + subject { finder.execute } describe 'without a user' do - subject { finder.execute } + let(:current_user) { nil } it { is_expected.to eq([public_project]) } end describe 'with a user' do - subject { finder.execute(user) } - describe 'without private projects' do - it { is_expected.to eq([public_project, internal_project]) } + it { is_expected.to match_array([public_project, internal_project]) } end describe 'with private projects' do before do - private_project.add_user(user, Gitlab::Access::MASTER) + private_project.add_master(user) end - it do - is_expected.to eq([public_project, internal_project, private_project]) - end + it { is_expected.to match_array([public_project, internal_project, private_project]) } end end describe 'with project_ids_relation' do let(:project_ids_relation) { Project.where(id: internal_project.id) } - subject { finder.execute(user, project_ids_relation) } - it { is_expected.to eq([internal_project]) } end + + describe 'filter by visibility_level' do + before do + private_project.add_master(user) + end + + context 'private' do + let(:params) { { visibility_level: Gitlab::VisibilityLevel::PRIVATE } } + + it { is_expected.to eq([private_project]) } + end + + context 'internal' do + let(:params) { { visibility_level: Gitlab::VisibilityLevel::INTERNAL } } + + it { is_expected.to eq([internal_project]) } + end + + context 'public' do + let(:params) { { visibility_level: Gitlab::VisibilityLevel::PUBLIC } } + + it { is_expected.to eq([public_project]) } + end + end + + describe 'filter by tags' do + before do + public_project.tag_list.add('foo') + public_project.save! + end + + let(:params) { { tag: 'foo' } } + + it { is_expected.to eq([public_project]) } + end + + describe 'filter by personal' do + let!(:personal_project) { create(:empty_project, namespace: user.namespace) } + let(:params) { { personal: true } } + + it { is_expected.to eq([personal_project]) } + end + + describe 'filter by search' do + let(:params) { { search: 'C' } } + + it { is_expected.to eq([public_project]) } + end + + describe 'filter by name for backward compatibility' do + let(:params) { { name: 'C' } } + + it { is_expected.to eq([public_project]) } + end + + describe 'filter by archived' do + let!(:archived_project) { create(:empty_project, :public, :archived, name: 'E', path: 'E') } + + context 'non_archived=true' do + let(:params) { { non_archived: true } } + + it { is_expected.to match_array([public_project, internal_project]) } + end + + context 'non_archived=false' do + let(:params) { { non_archived: false } } + + it { is_expected.to match_array([public_project, internal_project, archived_project]) } + end + + describe 'filter by archived for backward compatibility' do + let(:params) { { archived: false } } + + it { is_expected.to match_array([public_project, internal_project]) } + end + end + + describe 'filter by trending' do + let!(:trending_project) { create(:trending_project, project: public_project) } + let(:params) { { trending: true } } + + it { is_expected.to eq([public_project]) } + end + + describe 'filter by non_public' do + let(:params) { { non_public: true } } + before do + private_project.add_developer(current_user) + end + + it { is_expected.to eq([private_project]) } + end + + describe 'filter by viewable_starred_projects' do + let(:params) { { starred: true } } + before do + current_user.toggle_star(public_project) + end + + it { is_expected.to eq([public_project]) } + end + + describe 'sorting' do + let(:params) { { sort: 'name_asc' } } + + it { is_expected.to eq([internal_project, public_project]) } + end end end diff --git a/spec/javascripts/blob/3d_viewer/mesh_object_spec.js b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js new file mode 100644 index 00000000000..d1ebae33dab --- /dev/null +++ b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js @@ -0,0 +1,42 @@ +import { + BoxGeometry, +} from 'three/build/three.module'; +import MeshObject from '~/blob/3d_viewer/mesh_object'; + +describe('Mesh object', () => { + it('defaults to non-wireframe material', () => { + const object = new MeshObject( + new BoxGeometry(10, 10, 10), + ); + + expect(object.material.wireframe).toBeFalsy(); + }); + + it('changes to wirefame material', () => { + const object = new MeshObject( + new BoxGeometry(10, 10, 10), + ); + + object.changeMaterial('wireframe'); + + expect(object.material.wireframe).toBeTruthy(); + }); + + it('scales object down', () => { + const object = new MeshObject( + new BoxGeometry(10, 10, 10), + ); + const radius = object.geometry.boundingSphere.radius; + + expect(radius).not.toBeGreaterThan(4); + }); + + it('does not scale object down', () => { + const object = new MeshObject( + new BoxGeometry(1, 1, 1), + ); + const radius = object.geometry.boundingSphere.radius; + + expect(radius).toBeLessThan(1); + }); +}); diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js index 19a4e55a9db..d3a4d04345b 100644 --- a/spec/javascripts/blob/pdf/index_spec.js +++ b/spec/javascripts/blob/pdf/index_spec.js @@ -3,6 +3,18 @@ import testPDF from './test.pdf'; describe('PDF renderer', () => { let viewer; + let app; + + const checkLoaded = (done) => { + if (app.loading) { + setTimeout(() => { + checkLoaded(done); + }, 100); + } else { + done(); + } + }; + preloadFixtures('static/pdf_viewer.html.raw'); beforeEach(() => { @@ -21,11 +33,9 @@ describe('PDF renderer', () => { describe('successful response', () => { beforeEach((done) => { - renderPDF(); + app = renderPDF(); - setTimeout(() => { - done(); - }, 500); + checkLoaded(done); }); it('does not show loading icon', () => { @@ -50,11 +60,9 @@ describe('PDF renderer', () => { describe('error getting file', () => { beforeEach((done) => { viewer.dataset.endpoint = 'invalid/endpoint'; - renderPDF(); + app = renderPDF(); - setTimeout(() => { - done(); - }, 500); + checkLoaded(done); }); it('does not show loading icon', () => { diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 549c7af8ea8..beee6cb2969 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -75,6 +75,7 @@ describe('Build', () => { expect(url).toBe(`${BUILD_URL}.json`); expect(dataType).toBe('json'); expect(success).toEqual(jasmine.any(Function)); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); success.call(context, { trace_html: '<span>Example</span>', status: 'running' }); @@ -83,6 +84,7 @@ describe('Build', () => { it('removes the spinner', () => { const [{ success, context }] = $.ajax.calls.argsFor(0); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); success.call(context, { trace_html: '<span>Example</span>', status: 'success' }); expect($('.js-build-refresh').length).toBe(0); diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml index 483063fb889..e2dd9519898 100644 --- a/spec/javascripts/fixtures/environments/metrics.html.haml +++ b/spec/javascripts/fixtures/environments/metrics.html.haml @@ -1,12 +1,62 @@ -%div +.prometheus-container{ 'data-has-metrics': "false", 'data-doc-link': '/help/administration/monitoring/prometheus/index.md', 'data-prometheus-integration': '/root/hello-prometheus/services/prometheus/edit' } .top-area .row .col-sm-6 %h3.page-title Metrics for environment - .row - .col-sm-12 - %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } - .row - .col-sm-12 - %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
\ No newline at end of file + .prometheus-state + .js-getting-started.hidden + .row + .col-md-4.col-md-offset-4.state-svg + %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. Learn more about performance monitoring + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + %a.btn.btn-success + Configure Prometheus + .js-loading.hidden + .row + .col-md-4.col-md-offset-4.state-svg + %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 + %a.btn.btn-success + View documentation + .js-unable-to-connect.hidden + .row + .col-md-4.col-md-offset-4.state-svg + %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 Prometheus server + .row.state-button-section + .col-md-4.col-md-offset-4.text-center.state-button + %a.btn.btn-success + View documentation + .prometheus-graphs + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } + .row + .col-sm-12 + %svg.prometheus-graph{ 'graph-type' => 'memory_values' } diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 5a93d479c1f..03f3c206f44 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -310,5 +310,56 @@ require('~/lib/utils/common_utils'); }); }, 10000); }); + + describe('gl.utils.setFavicon', () => { + it('should set page favicon to provided favicon', () => { + const faviconName = 'custom_favicon'; + const fakeLink = { + setAttribute() {}, + }; + + spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); + spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { + expect(attr).toEqual('href'); + expect(val.indexOf('/assets/custom_favicon.ico') > -1).toBe(true); + }); + gl.utils.setFavicon(faviconName); + }); + }); + + describe('gl.utils.resetFavicon', () => { + it('should reset page favicon to tanuki', () => { + const fakeLink = { + setAttribute() {}, + }; + + spyOn(window.document, 'getElementById').and.callFake(() => fakeLink); + spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => { + expect(attr).toEqual('href'); + expect(val).toMatch(/favicon/); + }); + gl.utils.resetFavicon(); + }); + }); + + describe('gl.utils.setCiStatusFavicon', () => { + it('should set page favicon to CI status favicon based on provided status', () => { + const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`; + const FAVICON_PATH = 'ci_favicons/'; + const FAVICON = 'icon_status_success'; + const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub(); + const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub(); + spyOn($, 'ajax').and.callFake(function (options) { + options.success({ icon: FAVICON }); + expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH + FAVICON); + options.success(); + expect(spyResetFavicon).toHaveBeenCalled(); + options.error(); + expect(spyResetFavicon).toHaveBeenCalled(); + }); + + gl.utils.setCiStatusFavicon(BUILD_URL); + }); + }); }); })(); diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index d5193b41c33..88dae8c3e06 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -142,18 +142,21 @@ require('~/lib/utils/datetime_utility'); it('should call showCIStatus even if a notification should not be displayed', function() { var spy; spy = spyOn(this["class"], 'showCIStatus').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); }); it('should call showCIStatus when a notification should be displayed', function() { var spy; spy = spyOn(this["class"], 'showCIStatus').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(true); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); }); it('should call showCICoverage when the coverage rate is set', function() { var spy; spy = spyOn(this["class"], 'showCICoverage').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage); }); @@ -161,12 +164,14 @@ require('~/lib/utils/datetime_utility'); var spy; this.ciStatusData.coverage = null; spy = spyOn(this["class"], 'showCICoverage').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); return expect(spy).not.toHaveBeenCalled(); }); it('should not display a notification on the first check after the widget has been created', function() { var spy; spy = spyOn(window, 'notify'); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"] = new window.gl.MergeRequestWidget(this.opts); this["class"].getCIStatus(true); return expect(spy).not.toHaveBeenCalled(); @@ -174,6 +179,7 @@ require('~/lib/utils/datetime_utility'); it('should update the pipeline URL when the pipeline changes', function() { var spy; spy = spyOn(this["class"], 'updatePipelineUrls').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); this.ciStatusData.pipeline += 1; this["class"].getCIStatus(false); @@ -182,6 +188,7 @@ require('~/lib/utils/datetime_utility'); it('should update the commit URL when the sha changes', function() { var spy; spy = spyOn(this["class"], 'updateCommitUrls').and.stub(); + spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {}); this["class"].getCIStatus(false); this.ciStatusData.sha = "9b50b99a"; this["class"].getCIStatus(false); diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js index c2bcd9c0f7c..4b904fc2960 100644 --- a/spec/javascripts/monitoring/prometheus_graph_spec.js +++ b/spec/javascripts/monitoring/prometheus_graph_spec.js @@ -1,5 +1,4 @@ import 'jquery'; -import '~/lib/utils/common_utils'; import PrometheusGraph from '~/monitoring/prometheus_graph'; import { prometheusMockData } from './prometheus_mock_data'; @@ -12,6 +11,7 @@ describe('PrometheusGraph', () => { beforeEach(() => { loadFixtures(fixtureName); + $('.prometheus-container').data('has-metrics', 'true'); this.prometheusGraph = new PrometheusGraph(); const self = this; const fakeInit = (metricsResponse) => { @@ -75,3 +75,24 @@ describe('PrometheusGraph', () => { }); }); }); + +describe('PrometheusGraphs UX states', () => { + const fixtureName = 'static/environments/metrics.html.raw'; + preloadFixtures(fixtureName); + + beforeEach(() => { + loadFixtures(fixtureName); + this.prometheusGraph = new PrometheusGraph(); + }); + + it('shows a specified state', () => { + this.prometheusGraph.state = '.js-getting-started'; + this.prometheusGraph.updateState(); + const $state = $('.js-getting-started'); + expect($state).toBeDefined(); + expect($('.state-title', $state)).toBeDefined(); + expect($('.state-svg', $state)).toBeDefined(); + expect($('.state-description', $state)).toBeDefined(); + expect($('.state-button', $state)).toBeDefined(); + }); +}); diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index e007044868c..4ac79454647 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -58,6 +58,48 @@ describe Gitlab::Database::MigrationHelpers, lib: true do end end + describe '#remove_concurrent_index' do + context 'outside a transaction' do + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + + context 'using PostgreSQL' do + before do + allow(Gitlab::Database).to receive(:postgresql?).and_return(true) + allow(model).to receive(:disable_statement_timeout) + end + + it 'removes the index concurrently' do + expect(model).to receive(:remove_index). + with(:users, { algorithm: :concurrently, column: :foo }) + + model.remove_concurrent_index(:users, :foo) + end + end + + context 'using MySQL' do + it 'removes an index' do + expect(Gitlab::Database).to receive(:postgresql?).and_return(false) + + expect(model).to receive(:remove_index). + with(:users, { column: :foo }) + + model.remove_concurrent_index(:users, :foo) + end + end + end + + context 'inside a transaction' do + it 'raises RuntimeError' do + expect(model).to receive(:transaction_open?).and_return(true) + + expect { model.remove_concurrent_index(:users, :foo) }. + to raise_error(RuntimeError) + end + end + end + describe '#add_concurrent_foreign_key' do context 'inside a transaction' do it 'raises an error' do diff --git a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb index 287bf62d9bd..6307f8c16a3 100644 --- a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::SidekiqStatus::ClientMiddleware do describe '#call' do it 'tracks the job in Redis' do - expect(Gitlab::SidekiqStatus).to receive(:set).with('123') + expect(Gitlab::SidekiqStatus).to receive(:set).with('123', Gitlab::SidekiqStatus::DEFAULT_EXPIRATION) described_class.new. call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil } diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb index 56f06b61afb..496e50fbae4 100644 --- a/spec/lib/gitlab/sidekiq_status_spec.rb +++ b/spec/lib/gitlab/sidekiq_status_spec.rb @@ -73,4 +73,17 @@ describe Gitlab::SidekiqStatus do expect(key).to include('123') end end + + describe 'completed', :redis do + it 'returns the completed job' do + expect(described_class.completed_jids(%w(123))).to eq(['123']) + end + + it 'returns only the jobs completed' do + described_class.set('123') + described_class.set('456') + + expect(described_class.completed_jids(%w(123 456 789))).to eq(['789']) + end + end end diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index 09b1fda3796..0f29766db41 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -111,6 +111,20 @@ describe Blob do end end + describe '#stl?' do + it 'is falsey with image extension' do + git_blob = Gitlab::Git::Blob.new(name: 'file.png') + + expect(described_class.decorate(git_blob)).not_to be_stl + end + + it 'is truthy with STL extension' do + git_blob = Gitlab::Git::Blob.new(name: 'file.stl') + + expect(described_class.decorate(git_blob)).to be_stl + end + end + describe '#to_partial_path' do let(:project) { double(lfs_enabled?: true) } @@ -122,7 +136,8 @@ describe Blob do lfs_pointer?: false, svg?: false, text?: false, - binary?: false + binary?: false, + stl?: false ) described_class.decorate(double).tap do |blob| @@ -175,6 +190,11 @@ describe Blob do blob = stubbed_blob(text?: true, sketch?: true, binary?: true) expect(blob.to_partial_path(project)).to eq 'sketch' end + + it 'handles STLs' do + blob = stubbed_blob(text?: true, stl?: true) + expect(blob.to_partial_path(project)).to eq 'stl' + end end describe '#size_within_svg_limits?' do diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 4f4b18cf0e0..e1beac28dab 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -108,6 +108,15 @@ describe API::DeployKeys, api: true do expect(response).to have_http_status(201) end + + it 'accepts can_push parameter' do + key_attrs = attributes_for :write_access_key + + post api("/projects/#{project.id}/deploy_keys", admin), key_attrs + + expect(response).to have_http_status(201) + expect(json_response['can_push']).to eq(true) + end end describe 'DELETE /projects/:id/deploy_keys/:key_id' do diff --git a/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb new file mode 100644 index 00000000000..a714bf4e5d5 --- /dev/null +++ b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/remove_concurrent_index' + +describe RuboCop::Cop::Migration::RemoveConcurrentIndex do + include CopHelper + + subject(:cop) { described_class.new } + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when remove_concurrent_index is used inside a change method' do + inspect_source(cop, 'def change; remove_concurrent_index :table, :column; end') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + + it 'registers no offense when remove_concurrent_index is used inside an up method' do + inspect_source(cop, 'def up; remove_concurrent_index :table, :column; end') + + expect(cop.offenses.size).to eq(0) + end + end + + context 'outside of migration' do + it 'registers no offense' do + inspect_source(cop, 'def change; remove_concurrent_index :table, :column; end') + + expect(cop.offenses.size).to eq(0) + end + end +end diff --git a/spec/rubocop/cop/migration/remove_index_spec.rb b/spec/rubocop/cop/migration/remove_index_spec.rb new file mode 100644 index 00000000000..31923cb7429 --- /dev/null +++ b/spec/rubocop/cop/migration/remove_index_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/remove_index' + +describe RuboCop::Cop::Migration::RemoveIndex do + include CopHelper + + subject(:cop) { described_class.new } + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + end + + it 'registers an offense when remove_index is used' do + inspect_source(cop, 'def change; remove_index :table, :column; end') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + end + + context 'outside of migration' do + it 'registers no offense' do + inspect_source(cop, 'def change; remove_index :table, :column; end') + + expect(cop.offenses.size).to eq(0) + end + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index c22d145ca5d..03215a4624a 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -49,6 +49,7 @@ describe MergeRequests::RefreshService, services: true do context 'push to origin repo source branch' do let(:refresh_service) { service.new(@project, @user) } + before do allow(refresh_service).to receive(:execute_hooks) refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') @@ -70,6 +71,32 @@ describe MergeRequests::RefreshService, services: true do end end + context 'push to origin repo source branch when an MR was reopened' do + let(:refresh_service) { service.new(@project, @user) } + + before do + @merge_request.update(state: :reopened) + + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + end + + it 'executes hooks with update action' do + expect(refresh_service).to have_received(:execute_hooks). + with(@merge_request, 'update', @oldrev) + + expect(@merge_request.notes).not_to be_empty + expect(@merge_request).to be_open + expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey + expect(@merge_request.diff_head_sha).to eq(@newrev) + expect(@fork_merge_request).to be_open + expect(@fork_merge_request.notes).to be_empty + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end + end + context 'push to origin repo target branch' do before do service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index fbb22439f33..5a2c0671dac 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -23,10 +23,12 @@ describe RepositoryImportWorker do error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } expect_any_instance_of(Projects::ImportService).to receive(:execute). and_return({ status: :error, message: error }) + allow(subject).to receive(:jid).and_return('123') subject.perform(project.id) expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/") + expect(project.reload.import_jid).not_to be_nil end end end diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb new file mode 100644 index 00000000000..466277a5e5e --- /dev/null +++ b/spec/workers/stuck_import_jobs_worker_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe StuckImportJobsWorker do + let(:worker) { described_class.new } + let(:exclusive_lease_uuid) { SecureRandom.uuid } + + before do + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid) + end + + describe 'long running import' do + let(:project) { create(:empty_project, import_jid: '123', import_status: 'started') } + + before do + allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(['123']) + end + + it 'marks the project as failed' do + expect { worker.perform }.to change { project.reload.import_status }.to('failed') + end + end + + describe 'running import' do + let(:project) { create(:empty_project, import_jid: '123', import_status: 'started') } + + before do + allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([]) + end + + it 'does not mark the project as failed' do + worker.perform + + expect(project.reload.import_status).to eq('started') + end + end +end diff --git a/yarn.lock b/yarn.lock index 65eef75af1a..9f2b8fe3d6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4305,6 +4305,18 @@ text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" +three-orbit-controls@^82.1.0: + version "82.1.0" + resolved "https://registry.yarnpkg.com/three-orbit-controls/-/three-orbit-controls-82.1.0.tgz#11a7f33d0a20ecec98f098b37780f6537374fab4" + +three-stl-loader@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/three-stl-loader/-/three-stl-loader-1.0.4.tgz#6b3319a31e3b910aab1883d19b00c81a663c3e03" + +three@^0.84.0: + version "0.84.0" + resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918" + throttleit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" |