Commit 26a672da authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '18471-restrict-tag-pushes-protected-tags' of...

Merge branch '18471-restrict-tag-pushes-protected-tags' of gitlab.com:gitlab-org/gitlab-ce into 18471-restrict-tag-pushes-protected-tags
parents 3dc8c312 55811ac9
...@@ -33,7 +33,7 @@ core team members will mention this person. ...@@ -33,7 +33,7 @@ core team members will mention this person.
### Merge request coaching ### Merge request coaching
Several people from the [GitLab team][team] are helping community members to get 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/. 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, ...@@ -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. 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. 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) 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. 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. 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 ...@@ -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 [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 ["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 [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
...@@ -263,7 +263,8 @@ AwardsHandler.prototype.addAward = function addAward( ...@@ -263,7 +263,8 @@ AwardsHandler.prototype.addAward = function addAward(
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
return typeof callback === 'function' ? callback() : undefined; 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( AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar(
......
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();
}
}
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];
}
}
...@@ -10,7 +10,7 @@ Vue.use(PDFLab, { ...@@ -10,7 +10,7 @@ Vue.use(PDFLab, {
export default () => { export default () => {
const el = document.getElementById('js-pdf-viewer'); const el = document.getElementById('js-pdf-viewer');
new Vue({ return new Vue({
el, el,
data() { data() {
return { return {
......
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);
});
});
});
...@@ -88,6 +88,7 @@ window.Build = (function() { ...@@ -88,6 +88,7 @@ window.Build = (function() {
dataType: 'json', dataType: 'json',
success: function(buildData) { success: function(buildData) {
$('.js-build-output').html(buildData.trace_html); $('.js-build-output').html(buildData.trace_html);
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (window.location.hash === DOWN_BUILD_TRACE) { if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height()); $("html,body").scrollTop(this.$buildTrace.height());
} }
......
...@@ -227,9 +227,11 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -227,9 +227,11 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
case 'projects:pipelines:show': case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
new gl.Pipelines({ new gl.Pipelines({
initTabs: true, initTabs: true,
pipelineStatusUrl,
tabsOptions: { tabsOptions: {
action: controllerAction, action: controllerAction,
defaultAction: 'pipelines', defaultAction: 'pipelines',
......
...@@ -75,6 +75,7 @@ export default { ...@@ -75,6 +75,7 @@ export default {
class="fa fa-spinner fa-spin" class="fa fa-spinner fa-spin"
aria-hidden="true"/> aria-hidden="true"/>
</span> </span>
</button>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions"> <li v-for="action in actions">
...@@ -91,7 +92,6 @@ export default { ...@@ -91,7 +92,6 @@ export default {
</button> </button>
</li> </li>
</ul> </ul>
</button>
</div> </div>
`, `,
}; };
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
(function() { (function() {
(function(w) { (function(w) {
var base; var base;
const faviconEl = document.getElementById('favicon');
const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
w.gl || (w.gl = {}); w.gl || (w.gl = {});
(base = w.gl).utils || (base.utils = {}); (base = w.gl).utils || (base.utils = {});
w.gl.utils.isInGroupsPage = function() { w.gl.utils.isInGroupsPage = function() {
...@@ -361,5 +363,34 @@ ...@@ -361,5 +363,34 @@
fn(next, stop); 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); })(window);
}).call(window); }).call(window);
...@@ -41,8 +41,10 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -41,8 +41,10 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
// check_enable - Boolean, whether to check automerge status // check_enable - Boolean, whether to check automerge status
// merge_check_url - String, URL to use 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 // 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 = opts;
this.opts.pipeline_status_url = `${this.opts.pipeline_status_url}.json`;
this.$widgetBody = $('.mr-widget-body'); this.$widgetBody = $('.mr-widget-body');
$('#modal_merge_info').modal({ $('#modal_merge_info').modal({
show: false show: false
...@@ -159,6 +161,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -159,6 +161,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.status = data.status; _this.status = data.status;
_this.hasCi = data.has_ci; _this.hasCi = data.has_ci;
_this.updateMergeButton(_this.status, _this.hasCi); _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.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (data.status !== _this.opts.ci_status || if (data.status !== _this.opts.ci_status ||
data.sha !== _this.opts.ci_sha || data.sha !== _this.opts.ci_sha ||
......
...@@ -6,7 +6,10 @@ import statusCodes from '~/lib/utils/http_status'; ...@@ -6,7 +6,10 @@ import statusCodes from '~/lib/utils/http_status';
import { formatRelevantDigits } from '~/lib/utils/number_utils'; import { formatRelevantDigits } from '~/lib/utils/number_utils';
import '../flash'; import '../flash';
const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph'; const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json'; const metricsEndpoint = 'metrics.json';
const timeFormat = d3.time.format('%H:%M'); const timeFormat = d3.time.format('%H:%M');
const dayFormat = d3.time.format('%b %e, %a'); const dayFormat = d3.time.format('%b %e, %a');
...@@ -14,8 +17,15 @@ const bisectDate = d3.bisector(d => d.time).left; ...@@ -14,8 +17,15 @@ const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100; const extraAddedWidthParent = 100;
class PrometheusGraph { class PrometheusGraph {
constructor() { constructor() {
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.margin = { top: 80, right: 180, bottom: 80, left: 100 };
this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
...@@ -27,6 +37,10 @@ class PrometheusGraph { ...@@ -27,6 +37,10 @@ class PrometheusGraph {
this.backOffRequestCounter = 0; this.backOffRequestCounter = 0;
this.configureGraph(); this.configureGraph();
this.init(); this.init();
} else {
this.state = '.js-getting-started';
this.updateState();
}
} }
createGraph() { createGraph() {
...@@ -40,8 +54,19 @@ class PrometheusGraph { ...@@ -40,8 +54,19 @@ class PrometheusGraph {
init() { init() {
this.getData().then((metricsResponse) => { this.getData().then((metricsResponse) => {
if (Object.keys(metricsResponse).length === 0) { let enoughData = true;
new Flash('Empty metrics', 'alert'); 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 { } else {
this.transformData(metricsResponse); this.transformData(metricsResponse);
this.createGraph(); this.createGraph();
...@@ -345,14 +370,17 @@ class PrometheusGraph { ...@@ -345,14 +370,17 @@ class PrometheusGraph {
} }
return resp.metrics; return resp.metrics;
}) })
.catch(() => new Flash('An error occurred while fetching metrics.', 'alert')); .catch(() => {
this.state = '.js-unable-to-connect';
this.updateState();
});
} }
transformData(metricsResponse) { transformData(metricsResponse) {
Object.keys(metricsResponse.metrics).forEach((key) => { Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') { if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0]; const metricValues = (metricsResponse.metrics[key])[0];
if (typeof metricValues !== 'undefined') { if (metricValues !== undefined) {
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
time: new Date(metric[0] * 1000), time: new Date(metric[0] * 1000),
value: metric[1], value: metric[1],
...@@ -361,6 +389,13 @@ class PrometheusGraph { ...@@ -361,6 +389,13 @@ class PrometheusGraph {
} }
}); });
} }
updateState() {
const $statesContainer = $(prometheusStatesContainer);
$(prometheusParentGraphContainer).hide();
$(`${this.state}`, $statesContainer).removeClass('hidden');
$(prometheusStatesContainer).show();
}
} }
export default PrometheusGraph; export default PrometheusGraph;
...@@ -9,6 +9,10 @@ require('./lib/utils/bootstrap_linked_tabs'); ...@@ -9,6 +9,10 @@ require('./lib/utils/bootstrap_linked_tabs');
new global.LinkedTabs(options.tabsOptions); new global.LinkedTabs(options.tabsOptions);
} }
if (options.pipelineStatusUrl) {
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
}
this.addMarginToBuildColumns(); this.addMarginToBuildColumns();
} }
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
.award-menu-holder { .award-menu-holder {
display: inline-block; display: inline-block;
position: relative; position: absolute;
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
...@@ -117,11 +117,41 @@ ...@@ -117,11 +117,41 @@
&.active, &.active,
&:hover, &:hover,
&:active { &:active,
&.is-active {
background-color: $row-hover; background-color: $row-hover;
border-color: $row-hover-border; border-color: $row-hover-border;
box-shadow: none; box-shadow: none;
outline: 0; 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 { &.btn {
...@@ -162,9 +192,33 @@ ...@@ -162,9 +192,33 @@
color: $border-gray-normal; color: $border-gray-normal;
margin-top: 1px; margin-top: 1px;
padding: 0 2px; 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 { .award-control-text {
vertical-align: middle; vertical-align: middle;
} }
} }
.note-awards .award-control-icon-positive {
left: 6px;
}
...@@ -275,3 +275,9 @@ span.idiff { ...@@ -275,3 +275,9 @@ span.idiff {
} }
} }
} }
.is-stl-loading {
.stl-controls {
display: none;
}
}
...@@ -16,6 +16,8 @@ body.modal-open { ...@@ -16,6 +16,8 @@ body.modal-open {
overflow: hidden; overflow: hidden;
} }
.modal .modal-dialog { @media (min-width: $screen-md-min) {
.modal-dialog {
width: 860px; width: 860px;
}
} }
...@@ -293,6 +293,8 @@ $badge-color: $gl-text-color-secondary; ...@@ -293,6 +293,8 @@ $badge-color: $gl-text-color-secondary;
* Award emoji * Award emoji
*/ */
$award-emoji-menu-shadow: rgba(0,0,0,.175); $award-emoji-menu-shadow: rgba(0,0,0,.175);
$award-emoji-positive-add-bg: #fed159;
$award-emoji-positive-add-lines: #bb9c13;
/* /*
* Search Box * Search Box
......
...@@ -233,6 +233,15 @@ ...@@ -233,6 +233,15 @@
stroke-width: 1; stroke-width: 1;
} }
.prometheus-state {
margin-top: 10px;
display: none;
.state-button-section {
margin-top: 10px;
}
}
.environments-actions { .environments-actions {
.external-url, .external-url,
.monitoring-url, .monitoring-url,
......
...@@ -329,8 +329,6 @@ ...@@ -329,8 +329,6 @@
} }
#modal_merge_info .modal-dialog { #modal_merge_info .modal-dialog {
width: 600px;
.dark { .dark {
margin-right: 40px; margin-right: 40px;
} }
......
...@@ -398,13 +398,50 @@ ul.notes { ...@@ -398,13 +398,50 @@ ul.notes {
font-size: 17px; 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 { .danger-highlight {
color: $gl-text-red; color: $gl-text-red;
} }
.link-highlight { .link-highlight {
color: $gl-link-color; 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 { ...@@ -508,7 +545,6 @@ ul.notes {
} }
.line-resolve-all-container { .line-resolve-all-container {
.btn-group { .btn-group {
margin-left: -4px; margin-left: -4px;
} }
...@@ -537,7 +573,6 @@ ul.notes { ...@@ -537,7 +573,6 @@ ul.notes {
fill: $gray-darkest; fill: $gray-darkest;
} }
} }
} }
.line-resolve-all { .line-resolve-all {
......
...@@ -230,6 +230,14 @@ ...@@ -230,6 +230,14 @@
font-size: 0; font-size: 0;
} }
.fade-right {
right: 0;
}
.fade-left {
left: 0;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.cover-block { .cover-block {
padding-top: 20px; padding-top: 20px;
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
} }
.trigger-actions { .trigger-actions {
white-space: nowrap;
.btn { .btn {
margin-left: 10px; margin-left: 10px;
} }
......
...@@ -145,8 +145,6 @@ ...@@ -145,8 +145,6 @@
margin: 0; margin: 0;
} }
#modal-remove-blob > .modal-dialog { width: 850px; }
.blob-upload-dropzone-previews { .blob-upload-dropzone-previews {
text-align: center; text-align: center;
border: 2px; border: 2px;
......
...@@ -3,6 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -3,6 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer] before_action :group, only: [:show, :transfer]
def index def index
params[:sort] ||= 'latest_activity_desc'
@projects = Project.with_statistics @projects = Project.with_statistics
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? @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? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
......
# == 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
module ParamsBackwardCompatibility
private
def set_non_archived_param
params[:non_archived] = params[:archived].blank?
end
end
class Dashboard::ProjectsController < Dashboard::ApplicationController class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects include ParamsBackwardCompatibility
before_action :set_non_archived_param
before_action :default_sorting
def index def index
@projects = load_projects(current_user.authorized_projects) @projects = load_projects(params.merge(non_public: true)).page(params[:page])
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
format.html { @last_push = current_user.recent_push } format.html { @last_push = current_user.recent_push }
...@@ -21,10 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -21,10 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end end
def starred def starred
@projects = load_projects(current_user.viewable_starred_projects) @projects = load_projects(params.merge(starred: true)).
@projects = @projects.includes(:forked_from_project, :tags) includes(:forked_from_project, :tags).page(params[:page])
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@last_push = current_user.recent_push @last_push = current_user.recent_push
@groups = [] @groups = []
...@@ -41,14 +40,18 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -41,14 +40,18 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private private
def load_projects(base_scope) def default_sorting
projects = base_scope.sorted_by_activity.includes(:route, namespace: :route) 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 end
def load_events 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 = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0) @events = @events.limit(20).offset(params[:offset] || 0)
end end
......
class Explore::ProjectsController < Explore::ApplicationController class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects include ParamsBackwardCompatibility
before_action :set_non_archived_param
def index def index
@projects = load_projects params[:sort] ||= 'latest_activity_desc'
@tags = @projects.tags_on(:tags) @sort = params[:sort]
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @projects = load_projects.page(params[:page])
@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])
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -21,10 +19,9 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -21,10 +19,9 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def trending def trending
@projects = load_projects(Project.trending) params[:trending] = true
@projects = filter_projects(@projects) @sort = params[:sort]
@projects = @projects.sort(@sort = params[:sort]) @projects = load_projects.page(params[:page])
@projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -37,10 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -37,10 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
def starred def starred
@projects = load_projects @projects = load_projects.reorder('star_count DESC').page(params[:page])
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -52,10 +46,10 @@ class Explore::ProjectsController < Explore::ApplicationController ...@@ -52,10 +46,10 @@ class Explore::ProjectsController < Explore::ApplicationController
end end
end end
protected private
def load_projects(base_scope = nil) def load_projects
base_scope ||= ProjectsFinder.new.execute(current_user) ProjectsFinder.new(current_user: current_user, params: params).
base_scope.includes(:route, namespace: :route) execute.includes(:route, namespace: :route)
end end
end end
...@@ -27,7 +27,7 @@ class Groups::ApplicationController < ApplicationController ...@@ -27,7 +27,7 @@ class Groups::ApplicationController < ApplicationController
end end
def group_projects def group_projects
@projects ||= GroupProjectsFinder.new(group).execute(current_user) @projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end end
def authorize_admin_group! def authorize_admin_group!
......
class GroupsController < Groups::ApplicationController class GroupsController < Groups::ApplicationController
include FilterProjects
include IssuesAction include IssuesAction
include MergeRequestsAction include MergeRequestsAction
include ParamsBackwardCompatibility
respond_to :html respond_to :html
...@@ -105,15 +105,16 @@ class GroupsController < Groups::ApplicationController ...@@ -105,15 +105,16 @@ class GroupsController < Groups::ApplicationController
protected protected
def setup_projects def setup_projects
set_non_archived_param
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
options = {} options = {}
options[:only_owned] = true if params[:shared] == '0' options[:only_owned] = true if params[:shared] == '0'
options[:only_shared] = true if params[:shared] == '1' 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.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? @projects = @projects.page(params[:page]) if params[:name].blank?
end end
......
...@@ -9,7 +9,7 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -9,7 +9,7 @@ class Projects::ForksController < Projects::ApplicationController
def index def index
base_query = project.forks.includes(:creator) 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 @total_forks_count = base_query.size
@private_forks_count = @total_forks_count - @forks.size @private_forks_count = @total_forks_count - @forks.size
@public_forks_count = @total_forks_count - @private_forks_count @public_forks_count = @total_forks_count - @private_forks_count
......
...@@ -140,6 +140,6 @@ class UsersController < ApplicationController ...@@ -140,6 +140,6 @@ class UsersController < ApplicationController
end end
def projects_for_current_user def projects_for_current_user
ProjectsFinder.new.execute(current_user) ProjectsFinder.new(current_user: current_user).execute
end end
end end
class GroupProjectsFinder < UnionFinder # GroupProjectsFinder
def initialize(group, options = {}) #
# 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 @group = group
@options = options @options = options
end end
def execute(current_user = nil)
segments = group_projects(current_user)
find_union(segments, Project)
end
private private
def group_projects(current_user) def init_collection
only_owned = @options.fetch(:only_owned, false) only_owned = options.fetch(:only_owned, false)
only_shared = @options.fetch(:only_shared, false) only_shared = options.fetch(:only_shared, false)
projects = [] projects = []
if current_user if current_user
if @group.users.include?(current_user) if group.users.include?(current_user)
projects << @group.projects unless only_shared projects << group.projects unless only_shared
projects << @group.shared_projects unless only_owned projects << group.shared_projects unless only_owned
else else
unless only_shared unless only_shared
projects << @group.projects.visible_to_user(current_user) projects << group.projects.visible_to_user(current_user)
projects << @group.projects.public_to_user(current_user) projects << group.projects.public_to_user(current_user)
end end
unless only_owned unless only_owned
projects << @group.shared_projects.visible_to_user(current_user) projects << group.shared_projects.visible_to_user(current_user)
projects << @group.shared_projects.public_to_user(current_user) projects << group.shared_projects.public_to_user(current_user)
end end
end end
else else
projects << @group.projects.public_only unless only_shared projects << group.projects.public_only unless only_shared
projects << @group.shared_projects.public_only unless only_owned projects << group.shared_projects.public_only unless only_owned
end end
projects projects
end end
def union(items)
find_union(items, Project)
end
end end
...@@ -116,9 +116,9 @@ class IssuableFinder ...@@ -116,9 +116,9 @@ class IssuableFinder
if current_user && params[:authorized_only].presence && !current_user_related? if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects current_user.authorized_projects
elsif group elsif group
GroupProjectsFinder.new(group).execute(current_user) GroupProjectsFinder.new(group: group, current_user: current_user).execute
else 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 end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
...@@ -405,8 +405,4 @@ class IssuableFinder ...@@ -405,8 +405,4 @@ class IssuableFinder
def current_user_related? def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me' params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end end
def projects_finder
@projects_finder ||= ProjectsFinder.new
end
end end
...@@ -83,7 +83,7 @@ class LabelsFinder < UnionFinder ...@@ -83,7 +83,7 @@ class LabelsFinder < UnionFinder
def projects def projects
return @projects if defined?(@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.in_namespace(params[:group_id]) if group?
@projects = @projects.where(id: params[:project_ids]) if projects? @projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil) @projects = @projects.reorder(nil)
......
# 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 class ProjectsFinder < UnionFinder
def execute(current_user = nil, project_ids_relation = nil) attr_accessor :params
segments = all_projects(current_user) attr_reader :current_user, :project_ids_relation
segments.map! { |s| s.where(id: project_ids_relation) } if 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 end
private private
def all_projects(current_user) def init_collection
projects = [] projects = []
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 << current_user.authorized_projects if current_user
projects << Project.unscoped.public_to_user(current_user) projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present?
end
projects projects
end 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 end
...@@ -95,7 +95,7 @@ class TodosFinder ...@@ -95,7 +95,7 @@ class TodosFinder
def projects(items) def projects(items)
item_project_ids = items.reorder(nil).select(:project_id) 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 end
def type? def type?
......
...@@ -25,8 +25,8 @@ module SortingHelper ...@@ -25,8 +25,8 @@ module SortingHelper
def projects_sort_options_hash def projects_sort_options_hash
options = { options = {
sort_value_name => sort_title_name, sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated, sort_value_latest_activity => sort_title_latest_activity,
sort_value_oldest_updated => sort_title_oldest_updated, sort_value_oldest_activity => sort_title_oldest_activity,
sort_value_recently_created => sort_title_recently_created, sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created sort_value_oldest_created => sort_title_oldest_created
} }
...@@ -78,6 +78,14 @@ module SortingHelper ...@@ -78,6 +78,14 @@ module SortingHelper
'Last updated' 'Last updated'
end end
def sort_title_oldest_activity
'Oldest updated'
end
def sort_title_latest_activity
'Last updated'
end
def sort_title_oldest_created def sort_title_oldest_created
'Oldest created' 'Oldest created'
end end
...@@ -198,6 +206,14 @@ module SortingHelper ...@@ -198,6 +206,14 @@ module SortingHelper
'updated_desc' 'updated_desc'
end end
def sort_value_oldest_activity
'latest_activity_asc'
end
def sort_value_latest_activity
'latest_activity_desc'
end
def sort_value_oldest_created def sort_value_oldest_created
'created_asc' 'created_asc'
end end
......
...@@ -58,6 +58,10 @@ class Blob < SimpleDelegator ...@@ -58,6 +58,10 @@ class Blob < SimpleDelegator
binary? && extname.downcase.delete('.') == 'sketch' binary? && extname.downcase.delete('.') == 'sketch'
end end
def stl?
extname.downcase.delete('.') == 'stl'
end
def size_within_svg_limits? def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE size <= MAXIMUM_SVG_SIZE
end end
...@@ -81,6 +85,8 @@ class Blob < SimpleDelegator ...@@ -81,6 +85,8 @@ class Blob < SimpleDelegator
'notebook' 'notebook'
elsif sketch? elsif sketch?
'sketch' 'sketch'
elsif stl?
'stl'
elsif text? elsif text?
'text' 'text'
else else
......
...@@ -350,10 +350,15 @@ class Project < ActiveRecord::Base ...@@ -350,10 +350,15 @@ class Project < ActiveRecord::Base
end end
def sort(method) 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 # storage_size is a joined column so we need to
# pass a string to avoid AR adding the table name # pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC') 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 else
order_by(method) order_by(method)
end end
......
...@@ -12,7 +12,7 @@ class GroupPolicy < BasePolicy ...@@ -12,7 +12,7 @@ class GroupPolicy < BasePolicy
can_read ||= globally_viewable can_read ||= globally_viewable
can_read ||= member can_read ||= member
can_read ||= @user.admin? 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 can! :read_group if can_read
# Only group masters and group owners can create new projects # Only group masters and group owners can create new projects
...@@ -41,6 +41,6 @@ class GroupPolicy < BasePolicy ...@@ -41,6 +41,6 @@ class GroupPolicy < BasePolicy
return true if @subject.internal? && !@user.external? return true if @subject.internal? && !@user.external?
return true if @subject.users.include?(@user) return true if @subject.users.include?(@user)
GroupProjectsFinder.new(@subject).execute(@user).any? GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end end
end end
...@@ -39,7 +39,7 @@ module MergeRequests ...@@ -39,7 +39,7 @@ module MergeRequests
private private
# Returns all origin and fork merge requests from `@project` satisfying passed arguments. # 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 MergeRequest
.with_state(mr_states) .with_state(mr_states)
.where(source_branch: source_branch, source_project_id: @project.id) .where(source_branch: source_branch, source_project_id: @project.id)
......
...@@ -8,7 +8,7 @@ module Search ...@@ -8,7 +8,7 @@ module Search
def execute def execute
group = Group.find_by(id: params[:group_id]) if params[:group_id].present? 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 if group
projects = projects.inside_path(group.full_path) projects = projects.inside_path(group.full_path)
......
...@@ -13,5 +13,7 @@ ...@@ -13,5 +13,7 @@
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button', %button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': 'Add emoji', 'aria-label': 'Add emoji',
data: { title: 'Add emoji', placement: "bottom" } } 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") = icon('spinner spin', class: "award-control-icon award-control-icon-loading")
- 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 .blank-state.blank-state-welcome
%h2.blank-state-welcome-title %h2.blank-state-welcome-title
Welcome to GitLab Welcome to GitLab
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
%title= page_title(site_name) %title= page_title(site_name)
%meta{ name: "description", content: page_description } %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 "application", media: "all"
= stylesheet_link_tag "print", media: "print" = stylesheet_link_tag "print", media: "print"
......
- 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
- environment = local_assigns.fetch(:environment) - 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 = link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
= icon('area-chart') = icon('area-chart')
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
= page_specific_javascript_bundle_tag('monitoring') = page_specific_javascript_bundle_tag('monitoring')
= render "projects/pipelines/head" = render "projects/pipelines/head"
%div{ class: container_class } .prometheus-container{ class: container_class, 'data-has-metrics': "#{@environment.has_metrics?}" }
.top-area .top-area
.row .row
.col-sm-6 .col-sm-6
...@@ -16,6 +16,61 @@ ...@@ -16,6 +16,61 @@
.col-sm-6 .col-sm-6
.nav-controls .nav-controls
= render 'projects/deployments/actions', deployment: @environment.last_deployment = render 'projects/deployments/actions', deployment: @environment.last_deployment
.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 .row
.col-sm-12 .col-sm-12
%h4 %h4
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"}, check_enable: #{@merge_request.unchecked? ? "true" : "false"},
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", 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)}", ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
......
...@@ -59,7 +59,9 @@ ...@@ -59,7 +59,9 @@
- if note.emoji_awardable? - 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 = 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('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 - if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
......
<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
<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
<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
<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>
<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>
<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>
- @sort ||= sort_value_recently_updated - @sort ||= sort_value_latest_activity
.dropdown .dropdown
- toggle_text = projects_sort_options_hash[@sort] - toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
......
...@@ -2,6 +2,8 @@ class RepositoryImportWorker ...@@ -2,6 +2,8 @@ class RepositoryImportWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_EXPIRATION
attr_accessor :project, :current_user attr_accessor :project, :current_user
def perform(project_id) def perform(project_id)
...@@ -12,7 +14,7 @@ class RepositoryImportWorker ...@@ -12,7 +14,7 @@ class RepositoryImportWorker
import_url: @project.import_url, import_url: @project.import_url,
path: @project.path_with_namespace) 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 result = Projects::ImportService.new(project, current_user).execute
......
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
---
title: Show CI status as Favicon on Pipelines, Job and MR pages
merge_request: 10144
author:
---
title: ProjectsFinder should handle more options
merge_request: 9682
author: Jacopo Beschi @jacopo-beschi
---
title: Enable creation of deploy keys with write access via the API
merge_request:
author:
---
title: Disable invalid service templates
merge_request:
author:
---
title: Include reopened MRs when searching for opened ones
merge_request: 10407
author:
---
title: Fixes HTML structure that was preventing the tooltip to disappear when hovering
out of the button.
merge_request:
author:
---
title: Include endpoint in metrics for ETag caching middleware
merge_request: 10495
author:
---
title: Introduced error/empty states for the environments performance metrics
merge_request: 10271
author:
---
title: Add remove_concurrent_index to database helper
merge_request: 10441
author: blackst0ne
---
title: Added award emoji animation and improved active state
merge_request:
author:
---
title: Add rake task to import GitHub projects from the command line
merge_request:
author:
---
title: Periodically mark projects that are stuck in importing as failed
merge_request:
author:
...@@ -349,6 +349,9 @@ Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsW ...@@ -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'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *' 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['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 # GitLab Shell
......
...@@ -43,6 +43,7 @@ var config = { ...@@ -43,6 +43,7 @@ var config = {
protected_branches: './protected_branches/protected_branches_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js',
protected_tags: './protected_tags', protected_tags: './protected_tags',
snippet: './snippet/snippet_bundle.js', snippet: './snippet/snippet_bundle.js',
stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js', terminal: './terminal/terminal_bundle.js',
u2f: ['vendor/u2f'], u2f: ['vendor/u2f'],
users: './users/users_bundle.js', users: './users/users_bundle.js',
......
# rubocop:disable RemoveIndex
class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexOnRunnersLocked < ActiveRecord::Migration class AddIndexOnRunnersLocked < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexForPipelineUserId < ActiveRecord::Migration class AddIndexForPipelineUserId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class MergeRequestDiffRemoveUniq < ActiveRecord::Migration class MergeRequestDiffRemoveUniq < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
......
# rubocop:disable RemoveIndex
class MergeRequestDiffAddIndex < ActiveRecord::Migration class MergeRequestDiffAddIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
......
# rubocop:disable RemoveIndex
class RemoveBuildsEnableIndexOnProjects < ActiveRecord::Migration class RemoveBuildsEnableIndexOnProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToListsLabelId < ActiveRecord::Migration class AddUniqueIndexToListsLabelId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddDeletedAtToNamespaces < ActiveRecord::Migration class AddDeletedAtToNamespaces < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexForBuildToken < ActiveRecord::Migration class AddIndexForBuildToken < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveRedundantIndexes < ActiveRecord::Migration class RemoveRedundantIndexes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToNoteDiscussionId < ActiveRecord::Migration class AddIndexToNoteDiscussionId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIncomingEmailTokenToUsers < ActiveRecord::Migration class AddIncomingEmailTokenToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddGroupIdToLabels < ActiveRecord::Migration class AddGroupIdToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToLabelsTitle < ActiveRecord::Migration class AddIndexToLabelsTitle < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToLabels < ActiveRecord::Migration class AddUniqueIndexToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToSubscriptions < ActiveRecord::Migration class AddUniqueIndexToSubscriptions < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddProjectImportDataProjectIndex < ActiveRecord::Migration class AddProjectImportDataProjectIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToParentId < ActiveRecord::Migration class AddIndexToParentId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveUnnecessaryIndexes < ActiveRecord::Migration class RemoveUnnecessaryIndexes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToRoutes < ActiveRecord::Migration class AddIndexToRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddPathIndexToNamespace < ActiveRecord::Migration class AddPathIndexToNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddNameIndexToNamespace < ActiveRecord::Migration class AddNameIndexToNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddLowerPathIndexToRoutes < ActiveRecord::Migration class AddLowerPathIndexToRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToCiBuildsForStatusRunnerIdAndType < ActiveRecord::Migration class AddIndexToCiBuildsForStatusRunnerIdAndType < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToCiRunnersForIsShared < ActiveRecord::Migration class AddIndexToCiRunnersForIsShared < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToProjectAuthorizations < ActiveRecord::Migration class AddIndexToProjectAuthorizations < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
...@@ -6,8 +7,10 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration ...@@ -6,8 +7,10 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration
disable_ddl_transaction! disable_ddl_transaction!
def up def up
unless index_exists?(:project_authorizations, :project_id)
add_concurrent_index(:project_authorizations, :project_id) add_concurrent_index(:project_authorizations, :project_id)
end end
end
def down def down
remove_index(:project_authorizations, :project_id) if remove_index(:project_authorizations, :project_id) if
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddRelativePositionToIssues < ActiveRecord::Migration class AddRelativePositionToIssues < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToUserAgentDetail < ActiveRecord::Migration class AddIndexToUserAgentDetail < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
DOWNTIME = false DOWNTIME = false
......
# rubocop:disable RemoveIndex
class DropIndexForBuildsProjectStatus < ActiveRecord::Migration class DropIndexForBuildsProjectStatus < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
DOWNTIME = false DOWNTIME = false
......
# rubocop:disable RemoveIndex
class RemoveOldProjectIdColumns < ActiveRecord::Migration class RemoveOldProjectIdColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToUserGhost < ActiveRecord::Migration class AddIndexToUserGhost < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
# 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
# See http://doc.gitlab.com/ce/development/migration_style_guide.html # See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab. # for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveIndexForUsersCurrentSignInAt < ActiveRecord::Migration class RemoveIndexForUsersCurrentSignInAt < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers include Gitlab::Database::MigrationHelpers
......
class AddImportJidToProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :projects, :import_jid, :string
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -920,6 +920,7 @@ ActiveRecord::Schema.define(version: 20170402231018) do ...@@ -920,6 +920,7 @@ ActiveRecord::Schema.define(version: 20170402231018) do
t.text "description_html" t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved" t.boolean "only_allow_merge_if_all_discussions_are_resolved"
t.boolean "printing_merge_request_link_enabled", default: true, null: false t.boolean "printing_merge_request_link_enabled", default: true, null: false
t.string "import_jid"
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
# 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
...@@ -58,10 +58,22 @@ migration was tested. ...@@ -58,10 +58,22 @@ migration was tested.
## Removing indices ## 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 ```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 ## Adding indices
......
...@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration ...@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
# migration requires downtime. # migration requires downtime.
# DOWNTIME_REASON = '' # DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default" # When using the methods "add_concurrent_index", "remove_concurrent_index" or
# you must disable the use of transactions as these methods can not run in an # "add_column_with_default" you must disable the use of transactions
# existing transaction. When using "add_concurrent_index" make sure that this # as these methods can not run in an existing transaction.
# method is the _only_ method called in the migration, any other changes # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# should go in a separate migration. This ensures that upon failure _only_ the # that either of them is the _only_ method called in the migration,
# index creation fails and can be retried or reverted easily. # 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 # To disable transactions uncomment the following line and remove these
# comments: # comments:
......
...@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration ...@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
# migration requires downtime. # migration requires downtime.
# DOWNTIME_REASON = '' # DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default" # When using the methods "add_concurrent_index", "remove_concurrent_index" or
# you must disable the use of transactions as these methods can not run in an # "add_column_with_default" you must disable the use of transactions
# existing transaction. When using "add_concurrent_index" make sure that this # as these methods can not run in an existing transaction.
# method is the _only_ method called in the migration, any other changes # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# should go in a separate migration. This ensures that upon failure _only_ the # that either of them is the _only_ method called in the migration,
# index creation fails and can be retried or reverted easily. # 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 # To disable transactions uncomment the following line and remove these
# comments: # comments:
......
...@@ -6,12 +6,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration ...@@ -6,12 +6,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
# When using the methods "add_concurrent_index" or "add_column_with_default" # When using the methods "add_concurrent_index", "remove_concurrent_index" or
# you must disable the use of transactions as these methods can not run in an # "add_column_with_default" you must disable the use of transactions
# existing transaction. When using "add_concurrent_index" make sure that this # as these methods can not run in an existing transaction.
# method is the _only_ method called in the migration, any other changes # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# should go in a separate migration. This ensures that upon failure _only_ the # that either of them is the _only_ method called in the migration,
# index creation fails and can be retried or reverted easily. # 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 # To disable transactions uncomment the following line and remove these
# comments: # comments:
......
...@@ -47,6 +47,7 @@ module API ...@@ -47,6 +47,7 @@ module API
params do params do
requires :key, type: String, desc: 'The new deploy key' requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the 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 end
post ":id/deploy_keys" do post ":id/deploy_keys" do
params[:key].strip! params[:key].strip!
......
...@@ -142,7 +142,7 @@ module API ...@@ -142,7 +142,7 @@ module API
end end
get ":id/projects" do get ":id/projects" do
group = find_group!(params[:id]) 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) projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user present paginate(projects), with: entity, current_user: current_user
......
...@@ -84,7 +84,7 @@ module API ...@@ -84,7 +84,7 @@ module API
end end
get do get do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails 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 end
desc 'Create new project' do desc 'Create new project' do
......
...@@ -341,7 +341,7 @@ module API ...@@ -341,7 +341,7 @@ module API
not_found!('User') unless user not_found!('User') unless user
events = user.events. events = user.events.
merge(ProjectsFinder.new.execute(current_user)). merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project). references(:project).
with_associations. with_associations.
recent recent
......
...@@ -151,7 +151,7 @@ module API ...@@ -151,7 +151,7 @@ module API
end end
get ":id/projects" do get ":id/projects" do
group = find_group!(params[:id]) 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) projects = filter_projects(projects)
entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user present paginate(projects), with: entity, current_user: current_user
......
...@@ -107,7 +107,7 @@ module API ...@@ -107,7 +107,7 @@ module API
end end
get '/visible' do get '/visible' do
entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails 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 end
desc 'Get a projects list for authenticated user' do desc 'Get a projects list for authenticated user' do
......
...@@ -138,7 +138,7 @@ module API ...@@ -138,7 +138,7 @@ module API
not_found!('User') unless user not_found!('User') unless user
events = user.events. events = user.events.
merge(ProjectsFinder.new.execute(current_user)). merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project). references(:project).
with_associations. with_associations.
recent recent
......
...@@ -26,6 +26,30 @@ module Gitlab ...@@ -26,6 +26,30 @@ module Gitlab
add_index(table_name, column_name, options) add_index(table_name, column_name, options)
end 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. # Adds a foreign key with only minimal locking on the tables involved.
# #
# This method only requires minimal locking when using PostgreSQL. When # This method only requires minimal locking when using PostgreSQL. When
......
...@@ -2,26 +2,34 @@ module Gitlab ...@@ -2,26 +2,34 @@ module Gitlab
module EtagCaching module EtagCaching
class Middleware class Middleware
RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES.map { |word| "/#{word}/" }.join('|') RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES.map { |word| "/#{word}/" }.join('|')
ROUTE_REGEXP = Regexp.union( ROUTES = [
%r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z), {
%r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z) regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z),
) name: 'issue_notes'
},
{
regexp: %r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z),
name: 'issue_title'
}
].freeze
def initialize(app) def initialize(app)
@app = app @app = app
end end
def call(env) def call(env)
return @app.call(env) unless enabled_for_current_route?(env) route = match_current_route(env)
Gitlab::Metrics.add_event(:etag_caching_middleware_used) return @app.call(env) unless route
track_event(:etag_caching_middleware_used, route)
etag, cached_value_present = get_etag(env) etag, cached_value_present = get_etag(env)
if_none_match = env['HTTP_IF_NONE_MATCH'] if_none_match = env['HTTP_IF_NONE_MATCH']
if if_none_match == etag if if_none_match == etag
handle_cache_hit(etag) handle_cache_hit(etag, route)
else else
track_cache_miss(if_none_match, cached_value_present) track_cache_miss(if_none_match, cached_value_present, route)
status, headers, body = @app.call(env) status, headers, body = @app.call(env)
headers['ETag'] = etag headers['ETag'] = etag
...@@ -31,8 +39,8 @@ module Gitlab ...@@ -31,8 +39,8 @@ module Gitlab
private private
def enabled_for_current_route?(env) def match_current_route(env)
ROUTE_REGEXP.match(env['PATH_INFO']) ROUTES.find { |route| route[:regexp].match(env['PATH_INFO']) }
end end
def get_etag(env) def get_etag(env)
...@@ -52,22 +60,26 @@ module Gitlab ...@@ -52,22 +60,26 @@ module Gitlab
%Q{W/"#{value}"} %Q{W/"#{value}"}
end end
def handle_cache_hit(etag) def handle_cache_hit(etag, route)
Gitlab::Metrics.add_event(:etag_caching_cache_hit) track_event(:etag_caching_cache_hit, route)
status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429 status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429
[status_code, { 'ETag' => etag }, ['']] [status_code, { 'ETag' => etag }, ['']]
end end
def track_cache_miss(if_none_match, cached_value_present) def track_cache_miss(if_none_match, cached_value_present, route)
if if_none_match.blank? if if_none_match.blank?
Gitlab::Metrics.add_event(:etag_caching_header_missing) track_event(:etag_caching_header_missing, route)
elsif !cached_value_present elsif !cached_value_present
Gitlab::Metrics.add_event(:etag_caching_key_not_found) track_event(:etag_caching_key_not_found, route)
else else
Gitlab::Metrics.add_event(:etag_caching_resource_changed) track_event(:etag_caching_resource_changed, route)
end
end end
def track_event(name, route)
Gitlab::Metrics.add_event(name, endpoint: route[:name])
end end
end end
end end
......
...@@ -72,6 +72,8 @@ module Gitlab ...@@ -72,6 +72,8 @@ module Gitlab
# job_ids - The Sidekiq job IDs to check. # job_ids - The Sidekiq job IDs to check.
# #
# Returns an array of true or false indicating job completion. # Returns an array of true or false indicating job completion.
# true = job is still running
# false = job completed
def self.job_status(job_ids) def self.job_status(job_ids)
keys = job_ids.map { |jid| key_for(jid) } keys = job_ids.map { |jid| key_for(jid) }
...@@ -82,6 +84,17 @@ module Gitlab ...@@ -82,6 +84,17 @@ module Gitlab
end end
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) def self.key_for(jid)
STATUS_KEY % jid STATUS_KEY % jid
end end
......
...@@ -2,7 +2,9 @@ module Gitlab ...@@ -2,7 +2,9 @@ module Gitlab
module SidekiqStatus module SidekiqStatus
class ClientMiddleware class ClientMiddleware
def call(_, job, _, _) 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 yield
end end
end end
......
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
...@@ -9,7 +9,7 @@ module RuboCop ...@@ -9,7 +9,7 @@ module RuboCop
include MigrationHelpers include MigrationHelpers
MSG = '`add_concurrent_index` is not reversible so you must manually define ' \ 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) def on_send(node)
return unless in_migration?(node) return unless in_migration?(node)
......
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
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
...@@ -5,3 +5,5 @@ require_relative 'cop/migration/add_column_with_default' ...@@ -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_foreign_key'
require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_concurrent_index'
require_relative 'cop/migration/add_index' require_relative 'cop/migration/add_index'
require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index'
...@@ -23,5 +23,9 @@ FactoryGirl.define do ...@@ -23,5 +23,9 @@ FactoryGirl.define do
factory :another_deploy_key, class: 'DeployKey' do factory :another_deploy_key, class: 'DeployKey' do
end end
end end
factory :write_access_key, class: 'DeployKey' do
can_push true
end
end end
end end
...@@ -86,6 +86,7 @@ feature 'Group', feature: true do ...@@ -86,6 +86,7 @@ feature 'Group', feature: true do
describe 'create a nested group' do describe 'create a nested group' do
let(:group) { create(:group, path: 'foo') } let(:group) { create(:group, path: 'foo') }
context 'as admin' do
before do before do
visit subgroups_group_path(group) visit subgroups_group_path(group)
click_link 'New Subgroup' click_link 'New Subgroup'
...@@ -100,6 +101,28 @@ feature 'Group', feature: true do ...@@ -100,6 +101,28 @@ feature 'Group', feature: true do
end end
end end
context 'as group owner' do
let(:user) { create(:user) }
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
it 'checks permissions to avoid exposing groups by parent_id' do it 'checks permissions to avoid exposing groups by parent_id' do
group = create(:group, :private, path: 'secret-group') group = create(:group, :private, path: 'secret-group')
......
...@@ -71,6 +71,22 @@ feature "New project", feature: true do ...@@ -71,6 +71,22 @@ feature "New project", feature: true do
end end
end 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 end
context 'Import project options' do context 'Import project options' do
......
...@@ -3,8 +3,9 @@ require 'spec_helper' ...@@ -3,8 +3,9 @@ require 'spec_helper'
describe GroupProjectsFinder do describe GroupProjectsFinder do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:current_user) { create(:user) } 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!(:public_project) { create(:empty_project, :public, group: group, path: '1') }
let!(:private_project) { create(:empty_project, :private, group: group, path: '2') } let!(:private_project) { create(:empty_project, :private, group: group, path: '2') }
...@@ -18,22 +19,27 @@ describe GroupProjectsFinder do ...@@ -18,22 +19,27 @@ describe GroupProjectsFinder do
shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group) shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
end end
subject { finder.execute }
describe 'with a group member current user' do 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 context "only shared" do
subject { described_class.new(group, only_shared: true).execute(current_user) } let(:options) { { only_shared: true } }
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 end
context "only owned" do context "only owned" do
subject { described_class.new(group, only_owned: true).execute(current_user) } let(:options) { { only_owned: true } }
it { is_expected.to eq([private_project, public_project]) }
it { is_expected.to match_array([private_project, public_project]) }
end end
context "all" do context "all" do
subject { described_class.new(group).execute(current_user) } it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
end end
end end
...@@ -44,46 +50,56 @@ describe GroupProjectsFinder do ...@@ -44,46 +50,56 @@ describe GroupProjectsFinder do
end end
context "only shared" do context "only shared" do
let(:options) { { only_shared: true } }
context "without external user" do context "without external user" do
subject { described_class.new(group, only_shared: true).execute(current_user) } it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
end end
context "with external user" do context "with external user" do
before { current_user.update_attributes(external: true) } before do
subject { described_class.new(group, only_shared: true).execute(current_user) } current_user.update_attributes(external: true)
it { is_expected.to eq([shared_project_2, shared_project_1]) } end
it { is_expected.to match_array([shared_project_2, shared_project_1]) }
end end
end end
context "only owned" do context "only owned" do
let(:options) { { only_owned: true } }
context "without external user" do context "without external user" do
before { private_project.team << [current_user, Gitlab::Access::MASTER] } before do
subject { described_class.new(group, only_owned: true).execute(current_user) } private_project.team << [current_user, Gitlab::Access::MASTER]
it { is_expected.to eq([private_project, public_project]) } end
it { is_expected.to match_array([private_project, public_project]) }
end end
context "with external user" do context "with external user" do
before { current_user.update_attributes(external: true) } before do
subject { described_class.new(group, only_owned: true).execute(current_user) } current_user.update_attributes(external: true)
end
it { is_expected.to eq([public_project]) } it { is_expected.to eq([public_project]) }
end end
end
context "all" do context "all" do
subject { described_class.new(group).execute(current_user) } it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, public_project]) }
end
end end
end end
describe "no user" do describe "no user" do
context "only shared" do context "only shared" do
subject { described_class.new(group, only_shared: true).execute(current_user) } let(:options) { { only_shared: true } }
it { is_expected.to eq([shared_project_3, shared_project_1]) }
it { is_expected.to match_array([shared_project_3, shared_project_1]) }
end end
context "only owned" do context "only owned" do
subject { described_class.new(group, only_owned: true).execute(current_user) } let(:options) { { only_owned: true } }
it { is_expected.to eq([public_project]) } it { is_expected.to eq([public_project]) }
end end
end end
......
...@@ -21,38 +21,144 @@ describe ProjectsFinder do ...@@ -21,38 +21,144 @@ describe ProjectsFinder do
create(:empty_project, :private, name: 'D', path: 'D') create(:empty_project, :private, name: 'D', path: 'D')
end 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) }
describe 'without a user' do
subject { finder.execute } subject { finder.execute }
describe 'without a user' do
let(:current_user) { nil }
it { is_expected.to eq([public_project]) } it { is_expected.to eq([public_project]) }
end end
describe 'with a user' do describe 'with a user' do
subject { finder.execute(user) }
describe 'without private projects' do 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 end
describe 'with private projects' do describe 'with private projects' do
before do before do
private_project.add_user(user, Gitlab::Access::MASTER) private_project.add_master(user)
end end
it do it { is_expected.to match_array([public_project, internal_project, private_project]) }
is_expected.to eq([public_project, internal_project, private_project])
end
end end
end end
describe 'with project_ids_relation' do describe 'with project_ids_relation' do
let(:project_ids_relation) { Project.where(id: internal_project.id) } 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]) } it { is_expected.to eq([internal_project]) }
end 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
end end
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);
});
});
...@@ -3,6 +3,18 @@ import testPDF from './test.pdf'; ...@@ -3,6 +3,18 @@ import testPDF from './test.pdf';
describe('PDF renderer', () => { describe('PDF renderer', () => {
let viewer; let viewer;
let app;
const checkLoaded = (done) => {
if (app.loading) {
setTimeout(() => {
checkLoaded(done);
}, 100);
} else {
done();
}
};
preloadFixtures('static/pdf_viewer.html.raw'); preloadFixtures('static/pdf_viewer.html.raw');
beforeEach(() => { beforeEach(() => {
...@@ -21,11 +33,9 @@ describe('PDF renderer', () => { ...@@ -21,11 +33,9 @@ describe('PDF renderer', () => {
describe('successful response', () => { describe('successful response', () => {
beforeEach((done) => { beforeEach((done) => {
renderPDF(); app = renderPDF();
setTimeout(() => { checkLoaded(done);
done();
}, 500);
}); });
it('does not show loading icon', () => { it('does not show loading icon', () => {
...@@ -50,11 +60,9 @@ describe('PDF renderer', () => { ...@@ -50,11 +60,9 @@ describe('PDF renderer', () => {
describe('error getting file', () => { describe('error getting file', () => {
beforeEach((done) => { beforeEach((done) => {
viewer.dataset.endpoint = 'invalid/endpoint'; viewer.dataset.endpoint = 'invalid/endpoint';
renderPDF(); app = renderPDF();
setTimeout(() => { checkLoaded(done);
done();
}, 500);
}); });
it('does not show loading icon', () => { it('does not show loading icon', () => {
......
...@@ -75,6 +75,7 @@ describe('Build', () => { ...@@ -75,6 +75,7 @@ describe('Build', () => {
expect(url).toBe(`${BUILD_URL}.json`); expect(url).toBe(`${BUILD_URL}.json`);
expect(dataType).toBe('json'); expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function)); expect(success).toEqual(jasmine.any(Function));
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { trace_html: '<span>Example</span>', status: 'running' }); success.call(context, { trace_html: '<span>Example</span>', status: 'running' });
...@@ -83,6 +84,7 @@ describe('Build', () => { ...@@ -83,6 +84,7 @@ describe('Build', () => {
it('removes the spinner', () => { it('removes the spinner', () => {
const [{ success, context }] = $.ajax.calls.argsFor(0); const [{ success, context }] = $.ajax.calls.argsFor(0);
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { trace_html: '<span>Example</span>', status: 'success' }); success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
expect($('.js-build-refresh').length).toBe(0); expect($('.js-build-refresh').length).toBe(0);
......
%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 .top-area
.row .row
.col-sm-6 .col-sm-6
%h3.page-title %h3.page-title
Metrics for environment Metrics for environment
.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 .row
.col-sm-12 .col-sm-12
%svg.prometheus-graph{ 'graph-type' => 'cpu_values' } %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
......
...@@ -310,5 +310,56 @@ require('~/lib/utils/common_utils'); ...@@ -310,5 +310,56 @@ require('~/lib/utils/common_utils');
}); });
}, 10000); }, 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);
});
});
}); });
})(); })();
...@@ -142,18 +142,21 @@ require('~/lib/utils/datetime_utility'); ...@@ -142,18 +142,21 @@ require('~/lib/utils/datetime_utility');
it('should call showCIStatus even if a notification should not be displayed', function() { it('should call showCIStatus even if a notification should not be displayed', function() {
var spy; var spy;
spy = spyOn(this["class"], 'showCIStatus').and.stub(); spy = spyOn(this["class"], 'showCIStatus').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
}); });
it('should call showCIStatus when a notification should be displayed', function() { it('should call showCIStatus when a notification should be displayed', function() {
var spy; var spy;
spy = spyOn(this["class"], 'showCIStatus').and.stub(); spy = spyOn(this["class"], 'showCIStatus').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(true); this["class"].getCIStatus(true);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
}); });
it('should call showCICoverage when the coverage rate is set', function() { it('should call showCICoverage when the coverage rate is set', function() {
var spy; var spy;
spy = spyOn(this["class"], 'showCICoverage').and.stub(); spy = spyOn(this["class"], 'showCICoverage').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage); return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage);
}); });
...@@ -161,12 +164,14 @@ require('~/lib/utils/datetime_utility'); ...@@ -161,12 +164,14 @@ require('~/lib/utils/datetime_utility');
var spy; var spy;
this.ciStatusData.coverage = null; this.ciStatusData.coverage = null;
spy = spyOn(this["class"], 'showCICoverage').and.stub(); spy = spyOn(this["class"], 'showCICoverage').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
return expect(spy).not.toHaveBeenCalled(); return expect(spy).not.toHaveBeenCalled();
}); });
it('should not display a notification on the first check after the widget has been created', function() { it('should not display a notification on the first check after the widget has been created', function() {
var spy; var spy;
spy = spyOn(window, 'notify'); spy = spyOn(window, 'notify');
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"] = new window.gl.MergeRequestWidget(this.opts); this["class"] = new window.gl.MergeRequestWidget(this.opts);
this["class"].getCIStatus(true); this["class"].getCIStatus(true);
return expect(spy).not.toHaveBeenCalled(); return expect(spy).not.toHaveBeenCalled();
...@@ -174,6 +179,7 @@ require('~/lib/utils/datetime_utility'); ...@@ -174,6 +179,7 @@ require('~/lib/utils/datetime_utility');
it('should update the pipeline URL when the pipeline changes', function() { it('should update the pipeline URL when the pipeline changes', function() {
var spy; var spy;
spy = spyOn(this["class"], 'updatePipelineUrls').and.stub(); spy = spyOn(this["class"], 'updatePipelineUrls').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
this.ciStatusData.pipeline += 1; this.ciStatusData.pipeline += 1;
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
...@@ -182,6 +188,7 @@ require('~/lib/utils/datetime_utility'); ...@@ -182,6 +188,7 @@ require('~/lib/utils/datetime_utility');
it('should update the commit URL when the sha changes', function() { it('should update the commit URL when the sha changes', function() {
var spy; var spy;
spy = spyOn(this["class"], 'updateCommitUrls').and.stub(); spy = spyOn(this["class"], 'updateCommitUrls').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
this.ciStatusData.sha = "9b50b99a"; this.ciStatusData.sha = "9b50b99a";
this["class"].getCIStatus(false); this["class"].getCIStatus(false);
......
import 'jquery'; import 'jquery';
import '~/lib/utils/common_utils';
import PrometheusGraph from '~/monitoring/prometheus_graph'; import PrometheusGraph from '~/monitoring/prometheus_graph';
import { prometheusMockData } from './prometheus_mock_data'; import { prometheusMockData } from './prometheus_mock_data';
...@@ -12,6 +11,7 @@ describe('PrometheusGraph', () => { ...@@ -12,6 +11,7 @@ describe('PrometheusGraph', () => {
beforeEach(() => { beforeEach(() => {
loadFixtures(fixtureName); loadFixtures(fixtureName);
$('.prometheus-container').data('has-metrics', 'true');
this.prometheusGraph = new PrometheusGraph(); this.prometheusGraph = new PrometheusGraph();
const self = this; const self = this;
const fakeInit = (metricsResponse) => { const fakeInit = (metricsResponse) => {
...@@ -75,3 +75,24 @@ describe('PrometheusGraph', () => { ...@@ -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();
});
});
...@@ -58,6 +58,48 @@ describe Gitlab::Database::MigrationHelpers, lib: true do ...@@ -58,6 +58,48 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end end
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 describe '#add_concurrent_foreign_key' do
context 'inside a transaction' do context 'inside a transaction' do
it 'raises an error' do it 'raises an error' do
......
...@@ -47,9 +47,9 @@ describe Gitlab::EtagCaching::Middleware do ...@@ -47,9 +47,9 @@ describe Gitlab::EtagCaching::Middleware do
it 'tracks "etag_caching_key_not_found" event' do it 'tracks "etag_caching_key_not_found" event' do
expect(Gitlab::Metrics).to receive(:add_event) expect(Gitlab::Metrics).to receive(:add_event)
.with(:etag_caching_middleware_used) .with(:etag_caching_middleware_used, endpoint: 'issue_notes')
expect(Gitlab::Metrics).to receive(:add_event) expect(Gitlab::Metrics).to receive(:add_event)
.with(:etag_caching_key_not_found) .with(:etag_caching_key_not_found, endpoint: 'issue_notes')
middleware.call(build_env(path, if_none_match)) middleware.call(build_env(path, if_none_match))
end end
...@@ -93,9 +93,9 @@ describe Gitlab::EtagCaching::Middleware do ...@@ -93,9 +93,9 @@ describe Gitlab::EtagCaching::Middleware do
it 'tracks "etag_caching_cache_hit" event' do it 'tracks "etag_caching_cache_hit" event' do
expect(Gitlab::Metrics).to receive(:add_event) expect(Gitlab::Metrics).to receive(:add_event)
.with(:etag_caching_middleware_used) .with(:etag_caching_middleware_used, endpoint: 'issue_notes')
expect(Gitlab::Metrics).to receive(:add_event) expect(Gitlab::Metrics).to receive(:add_event)
.with(:etag_caching_cache_hit) .with(:etag_caching_cache_hit, endpoint: 'issue_notes')
middleware.call(build_env(path, if_none_match)) middleware.call(build_env(path, if_none_match))
end end
...@@ -132,9 +132,9 @@ describe Gitlab::EtagCaching::Middleware do ...@@ -132,9 +132,9 @@ describe Gitlab::EtagCaching::Middleware do
mock_app_response mock_app_response
expect(Gitlab::Metrics).to receive(:add_event) expect(Gitlab::Metrics).to receive(:add_event)
.with(:etag_caching_middleware_used) .with(:etag_caching_middleware_used, endpoint: 'issue_notes')
expect(Gitlab::Metrics).to receive(:add_event) expect(Gitlab::Metrics).to receive(:add_event)
.with(:etag_caching_resource_changed) .with(:etag_caching_resource_changed, endpoint: 'issue_notes')
middleware.call(build_env(path, if_none_match)) middleware.call(build_env(path, if_none_match))
end end
...@@ -150,9 +150,9 @@ describe Gitlab::EtagCaching::Middleware do ...@@ -150,9 +150,9 @@ describe Gitlab::EtagCaching::Middleware do
it 'tracks "etag_caching_header_missing" event' do it 'tracks "etag_caching_header_missing" event' do
expect(Gitlab::Metrics).to receive(:add_event) expect(Gitlab::Metrics).to receive(:add_event)
.with(:etag_caching_middleware_used) .with(:etag_caching_middleware_used, endpoint: 'issue_notes')
expect(Gitlab::Metrics).to receive(:add_event) expect(Gitlab::Metrics).to receive(:add_event)
.with(:etag_caching_header_missing) .with(:etag_caching_header_missing, endpoint: 'issue_notes')
middleware.call(build_env(path, if_none_match)) middleware.call(build_env(path, if_none_match))
end end
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::SidekiqStatus::ClientMiddleware do describe Gitlab::SidekiqStatus::ClientMiddleware do
describe '#call' do describe '#call' do
it 'tracks the job in Redis' 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. described_class.new.
call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil } call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil }
......
...@@ -73,4 +73,17 @@ describe Gitlab::SidekiqStatus do ...@@ -73,4 +73,17 @@ describe Gitlab::SidekiqStatus do
expect(key).to include('123') expect(key).to include('123')
end end
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 end
...@@ -111,6 +111,20 @@ describe Blob do ...@@ -111,6 +111,20 @@ describe Blob do
end end
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 describe '#to_partial_path' do
let(:project) { double(lfs_enabled?: true) } let(:project) { double(lfs_enabled?: true) }
...@@ -122,7 +136,8 @@ describe Blob do ...@@ -122,7 +136,8 @@ describe Blob do
lfs_pointer?: false, lfs_pointer?: false,
svg?: false, svg?: false,
text?: false, text?: false,
binary?: false binary?: false,
stl?: false
) )
described_class.decorate(double).tap do |blob| described_class.decorate(double).tap do |blob|
...@@ -175,6 +190,11 @@ describe Blob do ...@@ -175,6 +190,11 @@ describe Blob do
blob = stubbed_blob(text?: true, sketch?: true, binary?: true) blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
expect(blob.to_partial_path(project)).to eq 'sketch' expect(blob.to_partial_path(project)).to eq 'sketch'
end end
it 'handles STLs' do
blob = stubbed_blob(text?: true, stl?: true)
expect(blob.to_partial_path(project)).to eq 'stl'
end
end end
describe '#size_within_svg_limits?' do describe '#size_within_svg_limits?' do
......
...@@ -108,6 +108,15 @@ describe API::DeployKeys, api: true do ...@@ -108,6 +108,15 @@ describe API::DeployKeys, api: true do
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
end 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 end
describe 'DELETE /projects/:id/deploy_keys/:key_id' do describe 'DELETE /projects/:id/deploy_keys/:key_id' do
......
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
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
...@@ -49,6 +49,7 @@ describe MergeRequests::RefreshService, services: true do ...@@ -49,6 +49,7 @@ describe MergeRequests::RefreshService, services: true do
context 'push to origin repo source branch' do context 'push to origin repo source branch' do
let(:refresh_service) { service.new(@project, @user) } let(:refresh_service) { service.new(@project, @user) }
before do before do
allow(refresh_service).to receive(:execute_hooks) allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
...@@ -70,6 +71,32 @@ describe MergeRequests::RefreshService, services: true do ...@@ -70,6 +71,32 @@ describe MergeRequests::RefreshService, services: true do
end end
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 context 'push to origin repo target branch' do
before do before do
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
......
...@@ -23,10 +23,12 @@ describe RepositoryImportWorker do ...@@ -23,10 +23,12 @@ describe RepositoryImportWorker do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found } 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). expect_any_instance_of(Projects::ImportService).to receive(:execute).
and_return({ status: :error, message: error }) and_return({ status: :error, message: error })
allow(subject).to receive(:jid).and_return('123')
subject.perform(project.id) subject.perform(project.id)
expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/") 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 end
end end
......
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
...@@ -4305,6 +4305,18 @@ text-table@~0.2.0: ...@@ -4305,6 +4305,18 @@ text-table@~0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" 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: throttleit@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment