Commit 55811ac9 authored by James Edwards-Jones's avatar James Edwards-Jones

Merge branch 'last-green-master' into 18471-restrict-tag-pushes-protected-tags

parents 902054db c3af43c3
......@@ -33,7 +33,7 @@ core team members will mention this person.
### Merge request coaching
Several people from the [GitLab team][team] are helping community members to get
their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done).
their contributions accepted by meeting our [Definition of done][done].
What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
......@@ -64,6 +64,49 @@ Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
### Between the 1st and the 7th
These types of merge requests need special consideration:
* **Large features**: a large feature is one that is highlighted in the kick-off
and the release blogpost; typically this will have its own channel in Slack
and a dedicated team with front-end, back-end, and UX.
* **Small features**: any other feature request.
**Large features** must be with a maintainer **by the 1st**. It's OK if they
aren't completely done, but this allows the maintainer enough time to make the
decision about whether this can make it in before the freeze. If the maintainer
doesn't think it will make it, they should inform the developers working on it
and the Product Manager responsible for the feature.
**Small features** must be with a reviewer (not necessarily maintainer) **by the
3rd**.
Most merge requests from the community do not have a specific release
target. However, if one does and falls into either of the above categories, it's
the reviewer's responsibility to manage the above communication and assignment
on behalf of the community member.
### On the 7th
Merge requests should still be complete, following the
[definition of done][done]. The single exception is documentation, and this can
only be left until after the freeze if:
* There is a follow-up issue to add documentation.
* It is assigned to the person writing documentation for this feature, and they
are aware of it.
* It is in the correct milestone, with the ~Deliverable label.
All Community Edition merge requests from GitLab team members merged on the
freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the
subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts].
### Between the 7th and the 22nd
Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
and security issues will be cherry-picked into the stable branch.
Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
......@@ -158,3 +201,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http
[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done
[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html
......@@ -263,7 +263,8 @@ AwardsHandler.prototype.addAward = function addAward(
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
return typeof callback === 'function' ? callback() : undefined;
});
return $('.emoji-menu').removeClass('is-visible');
$('.emoji-menu').removeClass('is-visible');
$('.js-add-award.is-active').removeClass('is-active');
};
AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar(
......
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, {
export default () => {
const el = document.getElementById('js-pdf-viewer');
new Vue({
return new Vue({
el,
data() {
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() {
dataType: 'json',
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height());
}
......
......@@ -227,9 +227,11 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:pipelines:builds':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
new gl.Pipelines({
initTabs: true,
pipelineStatusUrl,
tabsOptions: {
action: controllerAction,
defaultAction: 'pipelines',
......
......@@ -75,6 +75,7 @@ export default {
class="fa fa-spinner fa-spin"
aria-hidden="true"/>
</span>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
......@@ -91,7 +92,6 @@ export default {
</button>
</li>
</ul>
</button>
</div>
`,
};
......@@ -2,6 +2,8 @@
(function() {
(function(w) {
var base;
const faviconEl = document.getElementById('favicon');
const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
w.gl || (w.gl = {});
(base = w.gl).utils || (base.utils = {});
w.gl.utils.isInGroupsPage = function() {
......@@ -361,5 +363,34 @@
fn(next, stop);
});
};
w.gl.utils.setFavicon = (iconName) => {
if (faviconEl && iconName) {
faviconEl.setAttribute('href', `/assets/${iconName}.ico`);
}
};
w.gl.utils.resetFavicon = () => {
if (faviconEl) {
faviconEl.setAttribute('href', originalFavicon);
}
};
w.gl.utils.setCiStatusFavicon = (pageUrl) => {
$.ajax({
url: pageUrl,
dataType: 'json',
success: function(data) {
if (data && data.icon) {
gl.utils.setFavicon(`ci_favicons/${data.icon}`);
} else {
gl.utils.resetFavicon();
}
},
error: function() {
gl.utils.resetFavicon();
}
});
};
})(window);
}).call(window);
......@@ -38,11 +38,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior
//
// check_enable - Boolean, whether to check automerge status
// merge_check_url - String, URL to use to check automerge status
// check_enable - Boolean, whether to check automerge status
// merge_check_url - String, URL to use to check automerge status
// ci_status_url - String, URL to use to check CI status
// pipeline_status_url - String, URL to use to get CI status for Favicon
//
this.opts = opts;
this.opts.pipeline_status_url = `${this.opts.pipeline_status_url}.json`;
this.$widgetBody = $('.mr-widget-body');
$('#modal_merge_info').modal({
show: false
......@@ -159,6 +161,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.status = data.status;
_this.hasCi = data.has_ci;
_this.updateMergeButton(_this.status, _this.hasCi);
gl.utils.setCiStatusFavicon(_this.opts.pipeline_status_url);
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (data.status !== _this.opts.ci_status ||
data.sha !== _this.opts.ci_sha ||
......
......@@ -6,7 +6,10 @@ import statusCodes from '~/lib/utils/http_status';
import { formatRelevantDigits } from '~/lib/utils/number_utils';
import '../flash';
const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json';
const timeFormat = d3.time.format('%H:%M');
const dayFormat = d3.time.format('%b %e, %a');
......@@ -14,19 +17,30 @@ const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100;
class PrometheusGraph {
constructor() {
this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
extraAddedWidthParent;
this.originalWidth = parentContainerWidth;
this.originalHeight = 330;
this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0;
this.configureGraph();
this.init();
const $prometheusContainer = $(prometheusContainer);
const hasMetrics = $prometheusContainer.data('has-metrics');
this.docLink = $prometheusContainer.data('doc-link');
this.integrationLink = $prometheusContainer.data('prometheus-integration');
$(document).ajaxError(() => {});
if (hasMetrics) {
this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
extraAddedWidthParent;
this.originalWidth = parentContainerWidth;
this.originalHeight = 330;
this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0;
this.configureGraph();
this.init();
} else {
this.state = '.js-getting-started';
this.updateState();
}
}
createGraph() {
......@@ -40,8 +54,19 @@ class PrometheusGraph {
init() {
this.getData().then((metricsResponse) => {
if (Object.keys(metricsResponse).length === 0) {
new Flash('Empty metrics', 'alert');
let enoughData = true;
Object.keys(metricsResponse.metrics).forEach((key) => {
let currentKey;
if (key === 'cpu_values' || key === 'memory_values') {
currentKey = metricsResponse.metrics[key];
if (Object.keys(currentKey).length === 0) {
enoughData = false;
}
}
});
if (!enoughData) {
this.state = '.js-loading';
this.updateState();
} else {
this.transformData(metricsResponse);
this.createGraph();
......@@ -345,14 +370,17 @@ class PrometheusGraph {
}
return resp.metrics;
})
.catch(() => new Flash('An error occurred while fetching metrics.', 'alert'));
.catch(() => {
this.state = '.js-unable-to-connect';
this.updateState();
});
}
transformData(metricsResponse) {
Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0];
if (typeof metricValues !== 'undefined') {
if (metricValues !== undefined) {
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
time: new Date(metric[0] * 1000),
value: metric[1],
......@@ -361,6 +389,13 @@ class PrometheusGraph {
}
});
}
updateState() {
const $statesContainer = $(prometheusStatesContainer);
$(prometheusParentGraphContainer).hide();
$(`${this.state}`, $statesContainer).removeClass('hidden');
$(prometheusStatesContainer).show();
}
}
export default PrometheusGraph;
......@@ -9,6 +9,10 @@ require('./lib/utils/bootstrap_linked_tabs');
new global.LinkedTabs(options.tabsOptions);
}
if (options.pipelineStatusUrl) {
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
}
this.addMarginToBuildColumns();
}
......
......@@ -91,7 +91,7 @@
.award-menu-holder {
display: inline-block;
position: relative;
position: absolute;
.tooltip {
white-space: nowrap;
......@@ -117,11 +117,41 @@
&.active,
&:hover,
&:active {
&:active,
&.is-active {
background-color: $row-hover;
border-color: $row-hover-border;
box-shadow: none;
outline: 0;
.award-control-icon svg {
background: $award-emoji-positive-add-bg;
path {
fill: $award-emoji-positive-add-lines;
}
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
transform: scale(1.15);
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
transform: scale(1);
}
.award-control-icon-super-positive {
opacity: 1;
transform: scale(1);
}
}
&.btn {
......@@ -162,9 +192,33 @@
color: $border-gray-normal;
margin-top: 1px;
padding: 0 2px;
svg {
margin-bottom: 1px;
height: 18px;
width: 18px;
border-radius: 50%;
path {
fill: $border-gray-normal;
}
}
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
left: 7px;
bottom: 9px;
opacity: 0;
@include transition(opacity, transform);
}
.award-control-text {
vertical-align: middle;
}
}
.note-awards .award-control-icon-positive {
left: 6px;
}
......@@ -275,3 +275,9 @@ span.idiff {
}
}
}
.is-stl-loading {
.stl-controls {
display: none;
}
}
......@@ -16,6 +16,8 @@ body.modal-open {
overflow: hidden;
}
.modal .modal-dialog {
width: 860px;
@media (min-width: $screen-md-min) {
.modal-dialog {
width: 860px;
}
}
......@@ -293,6 +293,8 @@ $badge-color: $gl-text-color-secondary;
* Award emoji
*/
$award-emoji-menu-shadow: rgba(0,0,0,.175);
$award-emoji-positive-add-bg: #fed159;
$award-emoji-positive-add-lines: #bb9c13;
/*
* Search Box
......
......@@ -233,6 +233,15 @@
stroke-width: 1;
}
.prometheus-state {
margin-top: 10px;
display: none;
.state-button-section {
margin-top: 10px;
}
}
.environments-actions {
.external-url,
.monitoring-url,
......
......@@ -329,8 +329,6 @@
}
#modal_merge_info .modal-dialog {
width: 600px;
.dark {
margin-right: 40px;
}
......
......@@ -398,13 +398,50 @@ ul.notes {
font-size: 17px;
}
&:hover {
svg {
height: 16px;
width: 16px;
fill: $gray-darkest;
vertical-align: text-top;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
margin-left: -20px;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight {
color: $gl-text-red;
}
.link-highlight {
color: $gl-link-color;
svg {
fill: $gl-link-color;
}
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
}
}
}
......@@ -508,7 +545,6 @@ ul.notes {
}
.line-resolve-all-container {
.btn-group {
margin-left: -4px;
}
......@@ -537,7 +573,6 @@ ul.notes {
fill: $gray-darkest;
}
}
}
.line-resolve-all {
......
......@@ -230,6 +230,14 @@
font-size: 0;
}
.fade-right {
right: 0;
}
.fade-left {
left: 0;
}
@media (max-width: $screen-xs-max) {
.cover-block {
padding-top: 20px;
......
......@@ -6,6 +6,8 @@
}
.trigger-actions {
white-space: nowrap;
.btn {
margin-left: 10px;
}
......
......@@ -145,8 +145,6 @@
margin: 0;
}
#modal-remove-blob > .modal-dialog { width: 850px; }
.blob-upload-dropzone-previews {
text-align: center;
border: 2px;
......
......@@ -3,6 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer]
def index
params[:sort] ||= 'latest_activity_desc'
@projects = Project.with_statistics
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
......
# == 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
include FilterProjects
include ParamsBackwardCompatibility
before_action :set_non_archived_param
before_action :default_sorting
def index
@projects = load_projects(current_user.authorized_projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@projects = load_projects(params.merge(non_public: true)).page(params[:page])
respond_to do |format|
format.html { @last_push = current_user.recent_push }
......@@ -21,10 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def starred
@projects = load_projects(current_user.viewable_starred_projects)
@projects = @projects.includes(:forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@projects = load_projects(params.merge(starred: true)).
includes(:forked_from_project, :tags).page(params[:page])
@last_push = current_user.recent_push
@groups = []
......@@ -41,14 +40,18 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
def load_projects(base_scope)
projects = base_scope.sorted_by_activity.includes(:route, namespace: :route)
def default_sorting
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
end
filter_projects(projects)
def load_projects(finder_params)
ProjectsFinder.new(params: finder_params, current_user: current_user).
execute.includes(:route, namespace: :route)
end
def load_events
@events = Event.in_projects(load_projects(current_user.authorized_projects))
@events = Event.in_projects(load_projects(params.merge(non_public: true)))
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
......
class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects
include ParamsBackwardCompatibility
before_action :set_non_archived_param
def index
@projects = load_projects
@tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page])
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
@projects = load_projects.page(params[:page])
respond_to do |format|
format.html
......@@ -21,10 +19,9 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
@projects = load_projects(Project.trending)
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
params[:trending] = true
@sort = params[:sort]
@projects = load_projects.page(params[:page])
respond_to do |format|
format.html
......@@ -37,10 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def starred
@projects = load_projects
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page])
@projects = load_projects.reorder('star_count DESC').page(params[:page])
respond_to do |format|
format.html
......@@ -52,10 +46,10 @@ class Explore::ProjectsController < Explore::ApplicationController
end
end
protected
private
def load_projects(base_scope = nil)
base_scope ||= ProjectsFinder.new.execute(current_user)
base_scope.includes(:route, namespace: :route)
def load_projects
ProjectsFinder.new(current_user: current_user, params: params).
execute.includes(:route, namespace: :route)
end
end
......@@ -27,7 +27,7 @@ class Groups::ApplicationController < ApplicationController
end
def group_projects
@projects ||= GroupProjectsFinder.new(group).execute(current_user)
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end
def authorize_admin_group!
......
class GroupsController < Groups::ApplicationController
include FilterProjects
include IssuesAction
include MergeRequestsAction
include ParamsBackwardCompatibility
respond_to :html
......@@ -105,15 +105,16 @@ class GroupsController < Groups::ApplicationController
protected
def setup_projects
set_non_archived_param
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
options = {}
options[:only_owned] = true if params[:shared] == '0'
options[:only_shared] = true if params[:shared] == '1'
@projects = GroupProjectsFinder.new(group, options).execute(current_user)
@projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user).execute
@projects = @projects.includes(:namespace)
@projects = @projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:name].blank?
end
......
......@@ -9,7 +9,7 @@ class Projects::ForksController < Projects::ApplicationController
def index
base_query = project.forks.includes(:creator)
@forks = base_query.merge(ProjectsFinder.new.execute(current_user))
@forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute)
@total_forks_count = base_query.size
@private_forks_count = @total_forks_count - @forks.size
@public_forks_count = @total_forks_count - @private_forks_count
......
......@@ -140,6 +140,6 @@ class UsersController < ApplicationController
end
def projects_for_current_user
ProjectsFinder.new.execute(current_user)
ProjectsFinder.new(current_user: current_user).execute
end
end
class GroupProjectsFinder < UnionFinder
def initialize(group, options = {})
# GroupProjectsFinder
#
# Used to filter Projects by set of params
#
# Arguments:
# current_user - which user use
# project_ids_relation: int[] - project ids to use
# group
# options:
# only_owned: boolean
# only_shared: boolean
# params:
# sort: string
# visibility_level: int
# tags: string[]
# personal: boolean
# search: string
# non_archived: boolean
#
class GroupProjectsFinder < ProjectsFinder
attr_reader :group, :options
def initialize(group:, params: {}, options: {}, current_user: nil, project_ids_relation: nil)
super(params: params, current_user: current_user, project_ids_relation: project_ids_relation)
@group = group
@options = options
end
def execute(current_user = nil)
segments = group_projects(current_user)
find_union(segments, Project)
end
private
def group_projects(current_user)
only_owned = @options.fetch(:only_owned, false)
only_shared = @options.fetch(:only_shared, false)
def init_collection
only_owned = options.fetch(:only_owned, false)
only_shared = options.fetch(:only_shared, false)
projects = []
if current_user
if @group.users.include?(current_user)
projects << @group.projects unless only_shared
projects << @group.shared_projects unless only_owned
if group.users.include?(current_user)
projects << group.projects unless only_shared
projects << group.shared_projects unless only_owned
else
unless only_shared
projects << @group.projects.visible_to_user(current_user)
projects << @group.projects.public_to_user(current_user)
projects << group.projects.visible_to_user(current_user)
projects << group.projects.public_to_user(current_user)
end
unless only_owned
projects << @group.shared_projects.visible_to_user(current_user)
projects << @group.shared_projects.public_to_user(current_user)
projects << group.shared_projects.visible_to_user(current_user)
projects << group.shared_projects.public_to_user(current_user)
end
end
else
projects << @group.projects.public_only unless only_shared
projects << @group.shared_projects.public_only unless only_owned
projects << group.projects.public_only unless only_shared
projects << group.shared_projects.public_only unless only_owned
end
projects
end
def union(items)
find_union(items, Project)
end
end
......@@ -116,9 +116,9 @@ class IssuableFinder
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects
elsif group
GroupProjectsFinder.new(group).execute(current_user)
GroupProjectsFinder.new(group: group, current_user: current_user).execute
else
projects_finder.execute(current_user, item_project_ids(items))
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
......@@ -405,8 +405,4 @@ class IssuableFinder
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
def projects_finder
@projects_finder ||= ProjectsFinder.new
end
end
......@@ -83,7 +83,7 @@ class LabelsFinder < UnionFinder
def projects
return @projects if defined?(@projects)
@projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user)
@projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute
@projects = @projects.in_namespace(params[:group_id]) if group?
@projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil)
......
# ProjectsFinder
#
# Used to filter Projects by set of params
#
# Arguments:
# current_user - which user use
# project_ids_relation: int[] - project ids to use
# params:
# trending: boolean
# non_public: boolean
# starred: boolean
# sort: string
# visibility_level: int
# tags: string[]
# personal: boolean
# search: string
# non_archived: boolean
#
class ProjectsFinder < UnionFinder
def execute(current_user = nil, project_ids_relation = nil)
segments = all_projects(current_user)
segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
attr_accessor :params
attr_reader :current_user, :project_ids_relation
find_union(segments, Project).with_route
def initialize(params: {}, current_user: nil, project_ids_relation: nil)
@params = params
@current_user = current_user
@project_ids_relation = project_ids_relation
end
def execute
items = init_collection
items = by_ids(items)
items = union(items)
items = by_personal(items)
items = by_visibilty_level(items)
items = by_tags(items)
items = by_search(items)
items = by_archived(items)
sort(items)
end
private
def all_projects(current_user)
def init_collection
projects = []
projects << current_user.authorized_projects if current_user
projects << Project.unscoped.public_to_user(current_user)
if params[:trending].present?
projects << Project.trending
elsif params[:starred].present? && current_user
projects << current_user.viewable_starred_projects
else
projects << current_user.authorized_projects if current_user
projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present?
end
projects
end
def by_ids(items)
project_ids_relation ? items.map { |item| item.where(id: project_ids_relation) } : items
end
def union(items)
find_union(items, Project).with_route
end
def by_personal(items)
(params[:personal].present? && current_user) ? items.personal(current_user) : items
end
def by_visibilty_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end
def by_tags(items)
params[:tag].present? ? items.tagged_with(params[:tag]) : items
end
def by_search(items)
params[:search] ||= params[:name]
params[:search].present? ? items.search(params[:search]) : items
end
def sort(items)
params[:sort].present? ? items.sort(params[:sort]) : items
end
def by_archived(projects)
# Back-compatibility with the places where `params[:archived]` can be set explicitly to `false`
params[:non_archived] = !Gitlab::Utils.to_boolean(params[:archived]) if params.key?(:archived)
params[:non_archived] ? projects.non_archived : projects
end
end
......@@ -95,7 +95,7 @@ class TodosFinder
def projects(items)
item_project_ids = items.reorder(nil).select(:project_id)
ProjectsFinder.new.execute(current_user, item_project_ids)
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids).execute
end
def type?
......
......@@ -25,8 +25,8 @@ module SortingHelper
def projects_sort_options_hash
options = {
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_latest_activity => sort_title_latest_activity,
sort_value_oldest_activity => sort_title_oldest_activity,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created
}
......@@ -78,6 +78,14 @@ module SortingHelper
'Last updated'
end
def sort_title_oldest_activity
'Oldest updated'
end
def sort_title_latest_activity
'Last updated'
end
def sort_title_oldest_created
'Oldest created'
end
......@@ -198,6 +206,14 @@ module SortingHelper
'updated_desc'
end
def sort_value_oldest_activity
'latest_activity_asc'
end
def sort_value_latest_activity
'latest_activity_desc'
end
def sort_value_oldest_created
'created_asc'
end
......
......@@ -58,6 +58,10 @@ class Blob < SimpleDelegator
binary? && extname.downcase.delete('.') == 'sketch'
end
def stl?
extname.downcase.delete('.') == 'stl'
end
def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE
end
......@@ -81,6 +85,8 @@ class Blob < SimpleDelegator
'notebook'
elsif sketch?
'sketch'
elsif stl?
'stl'
elsif text?
'text'
else
......
......@@ -350,10 +350,15 @@ class Project < ActiveRecord::Base
end
def sort(method)
if method == 'storage_size_desc'
case method.to_s
when 'storage_size_desc'
# storage_size is a joined column so we need to
# pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC')
when 'latest_activity_desc'
reorder(last_activity_at: :desc)
when 'latest_activity_asc'
reorder(last_activity_at: :asc)
else
order_by(method)
end
......
......@@ -12,7 +12,7 @@ class GroupPolicy < BasePolicy
can_read ||= globally_viewable
can_read ||= member
can_read ||= @user.admin?
can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any?
can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
can! :read_group if can_read
# Only group masters and group owners can create new projects
......@@ -41,6 +41,6 @@ class GroupPolicy < BasePolicy
return true if @subject.internal? && !@user.external?
return true if @subject.users.include?(@user)
GroupProjectsFinder.new(@subject).execute(@user).any?
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end
end
......@@ -39,7 +39,7 @@ module MergeRequests
private
# Returns all origin and fork merge requests from `@project` satisfying passed arguments.
def merge_requests_for(source_branch, mr_states: [:opened])
def merge_requests_for(source_branch, mr_states: [:opened, :reopened])
MergeRequest
.with_state(mr_states)
.where(source_branch: source_branch, source_project_id: @project.id)
......
......@@ -8,7 +8,7 @@ module Search
def execute
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
projects = ProjectsFinder.new(current_user: current_user).execute
if group
projects = projects.inside_path(group.full_path)
......
......@@ -13,5 +13,7 @@
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': 'Add emoji',
data: { title: 'Add emoji', placement: "bottom" } }
= icon('smile-o', class: "award-control-icon award-control-icon-normal")
%span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley')
%span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile')
= icon('spinner spin', class: "award-control-icon award-control-icon-loading")
- publicish_project_count = ProjectsFinder.new.execute(current_user).count
- publicish_project_count = ProjectsFinder.new(current_user: current_user).execute.count
.blank-state.blank-state-welcome
%h2.blank-state-welcome-title
Welcome to GitLab
......
......@@ -23,7 +23,7 @@
%title= page_title(site_name)
%meta{ name: "description", content: page_description }
= favicon_link_tag favicon
= favicon_link_tag favicon, id: 'favicon'
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
......
- 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)
- return unless environment.has_metrics? && can?(current_user, :read_environment, environment)
- return unless can?(current_user, :read_environment, environment)
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
= icon('area-chart')
......
......@@ -5,7 +5,7 @@
= page_specific_javascript_bundle_tag('monitoring')
= render "projects/pipelines/head"
%div{ class: container_class }
.prometheus-container{ class: container_class, 'data-has-metrics': "#{@environment.has_metrics?}" }
.top-area
.row
.col-sm-6
......@@ -16,13 +16,68 @@
.col-sm-6
.nav-controls
= render 'projects/deployments/actions', deployment: @environment.last_deployment
.row
.col-sm-12
%h4
CPU utilization
%svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
.row
.col-sm-12
%h4
Memory usage
%svg.prometheus-graph{ 'graph-type' => 'memory_values' }
.prometheus-state
.js-getting-started.hidden
.row
.col-md-4.col-md-offset-4.state-svg
= render "shared/empty_states/monitoring/getting_started.svg"
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Get started with performance monitoring
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.
= link_to help_page_path('administration/monitoring/prometheus/index.md') do
Learn more about performance monitoring
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
= link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'), class: 'btn btn-success' do
Configure Prometheus
.js-loading.hidden
.row
.col-md-4.col-md-offset-4.state-svg
= render "shared/empty_states/monitoring/loading.svg"
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Waiting for performance data
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
= link_to help_page_path('administration/monitoring/prometheus/index.md'), class: 'btn btn-success' do
View documentation
.js-unable-to-connect.hidden
.row
.col-md-4.col-md-offset-4.state-svg
= render "shared/empty_states/monitoring/unable_to_connect.svg"
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Unable to connect to Prometheus server
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Ensure connectivity is available from the GitLab server to the
= link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus') do
Prometheus server
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
= link_to help_page_path('administration/monitoring/prometheus/index.md'), class:'btn btn-success' do
View documentation
.prometheus-graphs
.row
.col-sm-12
%h4
CPU utilization
%svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
.row
.col-sm-12
%h4
Memory usage
%svg.prometheus-graph{ 'graph-type' => 'memory_values' }
......@@ -12,6 +12,7 @@
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"},
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
pipeline_status_url: "#{pipeline_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
......
......@@ -59,7 +59,9 @@
- if note.emoji_awardable?
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
= icon('spinner spin')
= icon('smile-o', class: 'link-highlight')
%span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley')
%span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
......
<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
- toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
......
......@@ -2,6 +2,8 @@ class RepositoryImportWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_EXPIRATION
attr_accessor :project, :current_user
def perform(project_id)
......@@ -12,7 +14,7 @@ class RepositoryImportWorker
import_url: @project.import_url,
path: @project.path_with_namespace)
project.update_column(:import_error, nil)
project.update_columns(import_jid: self.jid, import_error: nil)
result = Projects::ImportService.new(project, current_user).execute
......
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
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *'
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker'
Settings.cron_jobs['stuck_import_jobs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['stuck_import_jobs_worker']['cron'] ||= '15 * * * *'
Settings.cron_jobs['stuck_import_jobs_worker']['job_class'] = 'StuckImportJobsWorker'
#
# GitLab Shell
......
......@@ -43,6 +43,7 @@ var config = {
protected_branches: './protected_branches/protected_branches_bundle.js',
protected_tags: './protected_tags',
snippet: './snippet/snippet_bundle.js',
stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js',
u2f: ['vendor/u2f'],
users: './users/users_bundle.js',
......
# rubocop:disable RemoveIndex
class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexOnRunnersLocked < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexForPipelineUserId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class MergeRequestDiffRemoveUniq < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
......
# rubocop:disable RemoveIndex
class MergeRequestDiffAddIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
......
# rubocop:disable RemoveIndex
class RemoveBuildsEnableIndexOnProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToListsLabelId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddDeletedAtToNamespaces < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexForBuildToken < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveRedundantIndexes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToNoteDiscussionId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIncomingEmailTokenToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddGroupIdToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToLabelsTitle < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddUniqueIndexToSubscriptions < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddProjectImportDataProjectIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToParentId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveUnnecessaryIndexes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddPathIndexToNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddNameIndexToNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddLowerPathIndexToRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToCiBuildsForStatusRunnerIdAndType < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToCiRunnersForIsShared < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToProjectAuthorizations < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......@@ -6,7 +7,9 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration
disable_ddl_transaction!
def up
add_concurrent_index(:project_authorizations, :project_id)
unless index_exists?(:project_authorizations, :project_id)
add_concurrent_index(:project_authorizations, :project_id)
end
end
def down
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddRelativePositionToIssues < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToUserAgentDetail < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# rubocop:disable RemoveIndex
class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
......
# rubocop:disable RemoveIndex
class DropIndexForBuildsProjectStatus < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
......
# rubocop:disable RemoveIndex
class RemoveOldProjectIdColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class AddIndexToUserGhost < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
......
# 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
# for more information on how to write migrations for GitLab.
# rubocop:disable RemoveIndex
class RemoveIndexForUsersCurrentSignInAt < ActiveRecord::Migration
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 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170402231018) do
ActiveRecord::Schema.define(version: 20170405080720) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -920,6 +920,7 @@ ActiveRecord::Schema.define(version: 20170402231018) do
t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved"
t.boolean "printing_merge_request_link_enabled", default: true, null: false
t.string "import_jid"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
# 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.
## Removing indices
If you need to remove index, please add a condition like in following example:
When removing an index make sure to use the method `remove_concurrent_index` instead
of the regular `remove_index` method. The `remove_concurrent_index` method
automatically drops concurrent indexes when using PostgreSQL, removing the
need for downtime. To use this method you must disable transactions by calling
the method `disable_ddl_transaction!` in the body of your migration class like
so:
```ruby
remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
remove_concurrent_index :table_name, :column_name if index_exists?(:table_name, :column_name)
end
end
```
## Adding indices
......
......@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
# When using the methods "add_concurrent_index", "remove_concurrent_index" or
# "add_column_with_default" you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
# any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
......
......@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
# When using the methods "add_concurrent_index", "remove_concurrent_index" or
# "add_column_with_default" you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
# any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
......
......@@ -6,12 +6,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
DOWNTIME = false
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
# When using the methods "add_concurrent_index", "remove_concurrent_index" or
# "add_column_with_default" you must disable the use of transactions
# as these methods can not run in an existing transaction.
# When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
# that either of them is the _only_ method called in the migration,
# any other changes should go in a separate migration.
# This ensures that upon failure _only_ the index creation or removing fails
# and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
......
......@@ -47,6 +47,7 @@ module API
params do
requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key'
optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
end
post ":id/deploy_keys" do
params[:key].strip!
......
......@@ -142,7 +142,7 @@ module API
end
get ":id/projects" do
group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute
projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user
......
......@@ -84,7 +84,7 @@ module API
end
get do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
present_projects ProjectsFinder.new.execute(current_user), with: entity, statistics: params[:statistics]
present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity, statistics: params[:statistics]
end
desc 'Create new project' do
......
......@@ -341,7 +341,7 @@ module API
not_found!('User') unless user
events = user.events.
merge(ProjectsFinder.new.execute(current_user)).
merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project).
with_associations.
recent
......
......@@ -151,7 +151,7 @@ module API
end
get ":id/projects" do
group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute
projects = filter_projects(projects)
entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user
......
......@@ -107,7 +107,7 @@ module API
end
get '/visible' do
entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
present_projects ProjectsFinder.new.execute(current_user), with: entity
present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity
end
desc 'Get a projects list for authenticated user' do
......
......@@ -138,7 +138,7 @@ module API
not_found!('User') unless user
events = user.events.
merge(ProjectsFinder.new.execute(current_user)).
merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project).
with_associations.
recent
......
......@@ -26,6 +26,30 @@ module Gitlab
add_index(table_name, column_name, options)
end
# Removes an existed index, concurrently when supported
#
# On PostgreSQL this method removes an index concurrently.
#
# Example:
#
# remove_concurrent_index :users, :some_column
#
# See Rails' `remove_index` for more info on the available arguments.
def remove_concurrent_index(table_name, column_name, options = {})
if transaction_open?
raise 'remove_concurrent_index can not be run inside a transaction, ' \
'you can disable transactions by calling disable_ddl_transaction! ' \
'in the body of your migration class'
end
if Database.postgresql?
options = options.merge({ algorithm: :concurrently })
disable_statement_timeout
end
remove_index(table_name, options.merge({ column: column_name }))
end
# Adds a foreign key with only minimal locking on the tables involved.
#
# This method only requires minimal locking when using PostgreSQL. When
......
......@@ -2,26 +2,34 @@ module Gitlab
module EtagCaching
class Middleware
RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES.map { |word| "/#{word}/" }.join('|')
ROUTE_REGEXP = Regexp.union(
%r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z),
%r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z)
)
ROUTES = [
{
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)
@app = app
end
def call(env)
return @app.call(env) unless enabled_for_current_route?(env)
Gitlab::Metrics.add_event(:etag_caching_middleware_used)
route = match_current_route(env)
return @app.call(env) unless route
track_event(:etag_caching_middleware_used, route)
etag, cached_value_present = get_etag(env)
if_none_match = env['HTTP_IF_NONE_MATCH']
if if_none_match == etag
handle_cache_hit(etag)
handle_cache_hit(etag, route)
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)
headers['ETag'] = etag
......@@ -31,8 +39,8 @@ module Gitlab
private
def enabled_for_current_route?(env)
ROUTE_REGEXP.match(env['PATH_INFO'])
def match_current_route(env)
ROUTES.find { |route| route[:regexp].match(env['PATH_INFO']) }
end
def get_etag(env)
......@@ -52,23 +60,27 @@ module Gitlab
%Q{W/"#{value}"}
end
def handle_cache_hit(etag)
Gitlab::Metrics.add_event(:etag_caching_cache_hit)
def handle_cache_hit(etag, route)
track_event(:etag_caching_cache_hit, route)
status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429
[status_code, { 'ETag' => etag }, ['']]
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?
Gitlab::Metrics.add_event(:etag_caching_header_missing)
track_event(:etag_caching_header_missing, route)
elsif !cached_value_present
Gitlab::Metrics.add_event(:etag_caching_key_not_found)
track_event(:etag_caching_key_not_found, route)
else
Gitlab::Metrics.add_event(:etag_caching_resource_changed)
track_event(:etag_caching_resource_changed, route)
end
end
def track_event(name, route)
Gitlab::Metrics.add_event(name, endpoint: route[:name])
end
end
end
end
......@@ -72,6 +72,8 @@ module Gitlab
# job_ids - The Sidekiq job IDs to check.
#
# Returns an array of true or false indicating job completion.
# true = job is still running
# false = job completed
def self.job_status(job_ids)
keys = job_ids.map { |jid| key_for(jid) }
......@@ -82,6 +84,17 @@ module Gitlab
end
end
# Returns the JIDs that are completed
#
# job_ids - The Sidekiq job IDs to check.
#
# Returns an array of completed JIDs
def self.completed_jids(job_ids)
Sidekiq.redis do |redis|
job_ids.reject { |jid| redis.exists(key_for(jid)) }
end
end
def self.key_for(jid)
STATUS_KEY % jid
end
......
......@@ -2,7 +2,9 @@ module Gitlab
module SidekiqStatus
class ClientMiddleware
def call(_, job, _, _)
Gitlab::SidekiqStatus.set(job['jid'])
status_expiration = job['status_expiration'] || Gitlab::SidekiqStatus::DEFAULT_EXPIRATION
Gitlab::SidekiqStatus.set(job['jid'], status_expiration)
yield
end
end
......
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
include MigrationHelpers
MSG = '`add_concurrent_index` is not reversible so you must manually define ' \
'the `up` and `down` methods in your migration class, using `remove_index` in `down`'.freeze
'the `up` and `down` methods in your migration class, using `remove_concurrent_index` in `down`'.freeze
def on_send(node)
return unless in_migration?(node)
......
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'
require_relative 'cop/migration/add_concurrent_foreign_key'
require_relative 'cop/migration/add_concurrent_index'
require_relative 'cop/migration/add_index'
require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index'
......@@ -23,5 +23,9 @@ FactoryGirl.define do
factory :another_deploy_key, class: 'DeployKey' do
end
end
factory :write_access_key, class: 'DeployKey' do
can_push true
end
end
end
......@@ -86,17 +86,40 @@ feature 'Group', feature: true do
describe 'create a nested group' do
let(:group) { create(:group, path: 'foo') }
before do
visit subgroups_group_path(group)
click_link 'New Subgroup'
context 'as admin' do
before do
visit subgroups_group_path(group)
click_link 'New Subgroup'
end
it 'creates a nested group' do
fill_in 'Group path', with: 'bar'
click_button 'Create group'
expect(current_path).to eq(group_path('foo/bar'))
expect(page).to have_content("Group 'bar' was successfully created.")
end
end
it 'creates a nested group' do
fill_in 'Group path', with: 'bar'
click_button 'Create group'
context 'as group owner' do
let(:user) { create(:user) }
expect(current_path).to eq(group_path('foo/bar'))
expect(page).to have_content("Group 'bar' was successfully created.")
before do
group.add_owner(user)
logout
login_as(user)
visit subgroups_group_path(group)
click_link 'New Subgroup'
end
it 'creates a nested group' do
fill_in 'Group path', with: 'bar'
click_button 'Create group'
expect(current_path).to eq(group_path('foo/bar'))
expect(page).to have_content("Group 'bar' was successfully created.")
end
end
end
......
......@@ -71,6 +71,22 @@ feature "New project", feature: true do
end
end
end
context "with subgroup namespace" do
let(:group) { create(:group, :private, owner: user) }
let(:subgroup) { create(:group, parent: group) }
before do
group.add_master(user)
visit new_project_path(namespace_id: subgroup.id)
end
it "selects the group namespace" do
namespace = find("#project_namespace_id option[selected]")
expect(namespace.text).to eq subgroup.full_path
end
end
end
context 'Import project options' do
......
......@@ -3,8 +3,9 @@ require 'spec_helper'
describe GroupProjectsFinder do
let(:group) { create(:group) }
let(:current_user) { create(:user) }
let(:options) { {} }
let(:finder) { described_class.new(source_user) }
let(:finder) { described_class.new(group: group, current_user: current_user, options: options) }
let!(:public_project) { create(:empty_project, :public, group: group, path: '1') }
let!(:private_project) { create(:empty_project, :private, group: group, path: '2') }
......@@ -18,22 +19,27 @@ describe GroupProjectsFinder do
shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
end
subject { finder.execute }
describe 'with a group member current user' do
before { group.add_user(current_user, Gitlab::Access::MASTER) }
before do
group.add_master(current_user)
end
context "only shared" do
subject { described_class.new(group, only_shared: true).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
let(:options) { { only_shared: true } }
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
end
context "only owned" do
subject { described_class.new(group, only_owned: true).execute(current_user) }
it { is_expected.to eq([private_project, public_project]) }
let(:options) { { only_owned: true } }
it { is_expected.to match_array([private_project, public_project]) }
end
context "all" do
subject { described_class.new(group).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
end
end
......@@ -44,47 +50,57 @@ describe GroupProjectsFinder do
end
context "only shared" do
let(:options) { { only_shared: true } }
context "without external user" do
subject { described_class.new(group, only_shared: true).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
end
context "with external user" do
before { current_user.update_attributes(external: true) }
subject { described_class.new(group, only_shared: true).execute(current_user) }
it { is_expected.to eq([shared_project_2, shared_project_1]) }
before do
current_user.update_attributes(external: true)
end
it { is_expected.to match_array([shared_project_2, shared_project_1]) }
end
end
context "only owned" do
let(:options) { { only_owned: true } }
context "without external user" do
before { private_project.team << [current_user, Gitlab::Access::MASTER] }
subject { described_class.new(group, only_owned: true).execute(current_user) }
it { is_expected.to eq([private_project, public_project]) }
before do
private_project.team << [current_user, Gitlab::Access::MASTER]
end
it { is_expected.to match_array([private_project, public_project]) }
end
context "with external user" do
before { current_user.update_attributes(external: true) }
subject { described_class.new(group, only_owned: true).execute(current_user) }
it { is_expected.to eq([public_project]) }
end
before do
current_user.update_attributes(external: true)
end
context "all" do
subject { described_class.new(group).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, public_project]) }
it { is_expected.to eq([public_project]) }
end
end
context "all" do
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) }
end
end
describe "no user" do
context "only shared" do
subject { described_class.new(group, only_shared: true).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_1]) }
let(:options) { { only_shared: true } }
it { is_expected.to match_array([shared_project_3, shared_project_1]) }
end
context "only owned" do
subject { described_class.new(group, only_owned: true).execute(current_user) }
it { is_expected.to eq([public_project]) }
let(:options) { { only_owned: true } }
it { is_expected.to eq([public_project]) }
end
end
end
......@@ -21,38 +21,144 @@ describe ProjectsFinder do
create(:empty_project, :private, name: 'D', path: 'D')
end
let(:finder) { described_class.new }
let(:params) { {} }
let(:current_user) { user }
let(:project_ids_relation) { nil }
let(:finder) { described_class.new(params: params, current_user: current_user, project_ids_relation: project_ids_relation) }
subject { finder.execute }
describe 'without a user' do
subject { finder.execute }
let(:current_user) { nil }
it { is_expected.to eq([public_project]) }
end
describe 'with a user' do
subject { finder.execute(user) }
describe 'without private projects' do
it { is_expected.to eq([public_project, internal_project]) }
it { is_expected.to match_array([public_project, internal_project]) }
end
describe 'with private projects' do
before do
private_project.add_user(user, Gitlab::Access::MASTER)
private_project.add_master(user)
end
it do
is_expected.to eq([public_project, internal_project, private_project])
end
it { is_expected.to match_array([public_project, internal_project, private_project]) }
end
end
describe 'with project_ids_relation' do
let(:project_ids_relation) { Project.where(id: internal_project.id) }
subject { finder.execute(user, project_ids_relation) }
it { is_expected.to eq([internal_project]) }
end
describe 'filter by visibility_level' do
before do
private_project.add_master(user)
end
context 'private' do
let(:params) { { visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
it { is_expected.to eq([private_project]) }
end
context 'internal' do
let(:params) { { visibility_level: Gitlab::VisibilityLevel::INTERNAL } }
it { is_expected.to eq([internal_project]) }
end
context 'public' do
let(:params) { { visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
it { is_expected.to eq([public_project]) }
end
end
describe 'filter by tags' do
before do
public_project.tag_list.add('foo')
public_project.save!
end
let(:params) { { tag: 'foo' } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by personal' do
let!(:personal_project) { create(:empty_project, namespace: user.namespace) }
let(:params) { { personal: true } }
it { is_expected.to eq([personal_project]) }
end
describe 'filter by search' do
let(:params) { { search: 'C' } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by name for backward compatibility' do
let(:params) { { name: 'C' } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by archived' do
let!(:archived_project) { create(:empty_project, :public, :archived, name: 'E', path: 'E') }
context 'non_archived=true' do
let(:params) { { non_archived: true } }
it { is_expected.to match_array([public_project, internal_project]) }
end
context 'non_archived=false' do
let(:params) { { non_archived: false } }
it { is_expected.to match_array([public_project, internal_project, archived_project]) }
end
describe 'filter by archived for backward compatibility' do
let(:params) { { archived: false } }
it { is_expected.to match_array([public_project, internal_project]) }
end
end
describe 'filter by trending' do
let!(:trending_project) { create(:trending_project, project: public_project) }
let(:params) { { trending: true } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by non_public' do
let(:params) { { non_public: true } }
before do
private_project.add_developer(current_user)
end
it { is_expected.to eq([private_project]) }
end
describe 'filter by viewable_starred_projects' do
let(:params) { { starred: true } }
before do
current_user.toggle_star(public_project)
end
it { is_expected.to eq([public_project]) }
end
describe 'sorting' do
let(:params) { { sort: 'name_asc' } }
it { is_expected.to eq([internal_project, public_project]) }
end
end
end
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';
describe('PDF renderer', () => {
let viewer;
let app;
const checkLoaded = (done) => {
if (app.loading) {
setTimeout(() => {
checkLoaded(done);
}, 100);
} else {
done();
}
};
preloadFixtures('static/pdf_viewer.html.raw');
beforeEach(() => {
......@@ -21,11 +33,9 @@ describe('PDF renderer', () => {
describe('successful response', () => {
beforeEach((done) => {
renderPDF();
app = renderPDF();
setTimeout(() => {
done();
}, 500);
checkLoaded(done);
});
it('does not show loading icon', () => {
......@@ -50,11 +60,9 @@ describe('PDF renderer', () => {
describe('error getting file', () => {
beforeEach((done) => {
viewer.dataset.endpoint = 'invalid/endpoint';
renderPDF();
app = renderPDF();
setTimeout(() => {
done();
}, 500);
checkLoaded(done);
});
it('does not show loading icon', () => {
......
......@@ -75,6 +75,7 @@ describe('Build', () => {
expect(url).toBe(`${BUILD_URL}.json`);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { trace_html: '<span>Example</span>', status: 'running' });
......@@ -83,6 +84,7 @@ describe('Build', () => {
it('removes the spinner', () => {
const [{ success, context }] = $.ajax.calls.argsFor(0);
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
expect($('.js-build-refresh').length).toBe(0);
......
%div
.prometheus-container{ 'data-has-metrics': "false", 'data-doc-link': '/help/administration/monitoring/prometheus/index.md', 'data-prometheus-integration': '/root/hello-prometheus/services/prometheus/edit' }
.top-area
.row
.col-sm-6
%h3.page-title
Metrics for environment
.row
.col-sm-12
%svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
.row
.col-sm-12
%svg.prometheus-graph{ 'graph-type' => 'memory_values' }
\ No newline at end of file
.prometheus-state
.js-getting-started.hidden
.row
.col-md-4.col-md-offset-4.state-svg
%svg
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Get started with performance monitoring
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments. Learn more about performance monitoring
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
%a.btn.btn-success
Configure Prometheus
.js-loading.hidden
.row
.col-md-4.col-md-offset-4.state-svg
%svg
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Waiting for performance data
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
%a.btn.btn-success
View documentation
.js-unable-to-connect.hidden
.row
.col-md-4.col-md-offset-4.state-svg
%svg
.row
.col-md-6.col-md-offset-3
%h4.text-center.state-title
Unable to connect to Prometheus server
.row
.col-md-6.col-md-offset-3
.description-text.text-center.state-description
Ensure connectivity is available from the GitLab server to the Prometheus server
.row.state-button-section
.col-md-4.col-md-offset-4.text-center.state-button
%a.btn.btn-success
View documentation
.prometheus-graphs
.row
.col-sm-12
%svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
.row
.col-sm-12
%svg.prometheus-graph{ 'graph-type' => 'memory_values' }
......@@ -310,5 +310,56 @@ require('~/lib/utils/common_utils');
});
}, 10000);
});
describe('gl.utils.setFavicon', () => {
it('should set page favicon to provided favicon', () => {
const faviconName = 'custom_favicon';
const fakeLink = {
setAttribute() {},
};
spyOn(window.document, 'getElementById').and.callFake(() => fakeLink);
spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => {
expect(attr).toEqual('href');
expect(val.indexOf('/assets/custom_favicon.ico') > -1).toBe(true);
});
gl.utils.setFavicon(faviconName);
});
});
describe('gl.utils.resetFavicon', () => {
it('should reset page favicon to tanuki', () => {
const fakeLink = {
setAttribute() {},
};
spyOn(window.document, 'getElementById').and.callFake(() => fakeLink);
spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => {
expect(attr).toEqual('href');
expect(val).toMatch(/favicon/);
});
gl.utils.resetFavicon();
});
});
describe('gl.utils.setCiStatusFavicon', () => {
it('should set page favicon to CI status favicon based on provided status', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`;
const FAVICON_PATH = 'ci_favicons/';
const FAVICON = 'icon_status_success';
const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub();
const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub();
spyOn($, 'ajax').and.callFake(function (options) {
options.success({ icon: FAVICON });
expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH + FAVICON);
options.success();
expect(spyResetFavicon).toHaveBeenCalled();
options.error();
expect(spyResetFavicon).toHaveBeenCalled();
});
gl.utils.setCiStatusFavicon(BUILD_URL);
});
});
});
})();
......@@ -142,18 +142,21 @@ require('~/lib/utils/datetime_utility');
it('should call showCIStatus even if a notification should not be displayed', function() {
var spy;
spy = spyOn(this["class"], 'showCIStatus').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
});
it('should call showCIStatus when a notification should be displayed', function() {
var spy;
spy = spyOn(this["class"], 'showCIStatus').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(true);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
});
it('should call showCICoverage when the coverage rate is set', function() {
var spy;
spy = spyOn(this["class"], 'showCICoverage').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage);
});
......@@ -161,12 +164,14 @@ require('~/lib/utils/datetime_utility');
var spy;
this.ciStatusData.coverage = null;
spy = spyOn(this["class"], 'showCICoverage').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
return expect(spy).not.toHaveBeenCalled();
});
it('should not display a notification on the first check after the widget has been created', function() {
var spy;
spy = spyOn(window, 'notify');
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"] = new window.gl.MergeRequestWidget(this.opts);
this["class"].getCIStatus(true);
return expect(spy).not.toHaveBeenCalled();
......@@ -174,6 +179,7 @@ require('~/lib/utils/datetime_utility');
it('should update the pipeline URL when the pipeline changes', function() {
var spy;
spy = spyOn(this["class"], 'updatePipelineUrls').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
this.ciStatusData.pipeline += 1;
this["class"].getCIStatus(false);
......@@ -182,6 +188,7 @@ require('~/lib/utils/datetime_utility');
it('should update the commit URL when the sha changes', function() {
var spy;
spy = spyOn(this["class"], 'updateCommitUrls').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
this.ciStatusData.sha = "9b50b99a";
this["class"].getCIStatus(false);
......
import 'jquery';
import '~/lib/utils/common_utils';
import PrometheusGraph from '~/monitoring/prometheus_graph';
import { prometheusMockData } from './prometheus_mock_data';
......@@ -12,6 +11,7 @@ describe('PrometheusGraph', () => {
beforeEach(() => {
loadFixtures(fixtureName);
$('.prometheus-container').data('has-metrics', 'true');
this.prometheusGraph = new PrometheusGraph();
const self = this;
const fakeInit = (metricsResponse) => {
......@@ -75,3 +75,24 @@ describe('PrometheusGraph', () => {
});
});
});
describe('PrometheusGraphs UX states', () => {
const fixtureName = 'static/environments/metrics.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
loadFixtures(fixtureName);
this.prometheusGraph = new PrometheusGraph();
});
it('shows a specified state', () => {
this.prometheusGraph.state = '.js-getting-started';
this.prometheusGraph.updateState();
const $state = $('.js-getting-started');
expect($state).toBeDefined();
expect($('.state-title', $state)).toBeDefined();
expect($('.state-svg', $state)).toBeDefined();
expect($('.state-description', $state)).toBeDefined();
expect($('.state-button', $state)).toBeDefined();
});
});
......@@ -58,6 +58,48 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
describe '#remove_concurrent_index' do
context 'outside a transaction' do
before do
allow(model).to receive(:transaction_open?).and_return(false)
end
context 'using PostgreSQL' do
before do
allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
allow(model).to receive(:disable_statement_timeout)
end
it 'removes the index concurrently' do
expect(model).to receive(:remove_index).
with(:users, { algorithm: :concurrently, column: :foo })
model.remove_concurrent_index(:users, :foo)
end
end
context 'using MySQL' do
it 'removes an index' do
expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
expect(model).to receive(:remove_index).
with(:users, { column: :foo })
model.remove_concurrent_index(:users, :foo)
end
end
end
context 'inside a transaction' do
it 'raises RuntimeError' do
expect(model).to receive(:transaction_open?).and_return(true)
expect { model.remove_concurrent_index(:users, :foo) }.
to raise_error(RuntimeError)
end
end
end
describe '#add_concurrent_foreign_key' do
context 'inside a transaction' do
it 'raises an error' do
......
......@@ -47,9 +47,9 @@ describe Gitlab::EtagCaching::Middleware do
it 'tracks "etag_caching_key_not_found" event' do
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)
.with(:etag_caching_key_not_found)
.with(:etag_caching_key_not_found, endpoint: 'issue_notes')
middleware.call(build_env(path, if_none_match))
end
......@@ -93,9 +93,9 @@ describe Gitlab::EtagCaching::Middleware do
it 'tracks "etag_caching_cache_hit" event' do
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)
.with(:etag_caching_cache_hit)
.with(:etag_caching_cache_hit, endpoint: 'issue_notes')
middleware.call(build_env(path, if_none_match))
end
......@@ -132,9 +132,9 @@ describe Gitlab::EtagCaching::Middleware do
mock_app_response
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)
.with(:etag_caching_resource_changed)
.with(:etag_caching_resource_changed, endpoint: 'issue_notes')
middleware.call(build_env(path, if_none_match))
end
......@@ -150,9 +150,9 @@ describe Gitlab::EtagCaching::Middleware do
it 'tracks "etag_caching_header_missing" event' do
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)
.with(:etag_caching_header_missing)
.with(:etag_caching_header_missing, endpoint: 'issue_notes')
middleware.call(build_env(path, if_none_match))
end
......
......@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::SidekiqStatus::ClientMiddleware do
describe '#call' do
it 'tracks the job in Redis' do
expect(Gitlab::SidekiqStatus).to receive(:set).with('123')
expect(Gitlab::SidekiqStatus).to receive(:set).with('123', Gitlab::SidekiqStatus::DEFAULT_EXPIRATION)
described_class.new.
call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil }
......
......@@ -73,4 +73,17 @@ describe Gitlab::SidekiqStatus do
expect(key).to include('123')
end
end
describe 'completed', :redis do
it 'returns the completed job' do
expect(described_class.completed_jids(%w(123))).to eq(['123'])
end
it 'returns only the jobs completed' do
described_class.set('123')
described_class.set('456')
expect(described_class.completed_jids(%w(123 456 789))).to eq(['789'])
end
end
end
......@@ -111,6 +111,20 @@ describe Blob do
end
end
describe '#stl?' do
it 'is falsey with image extension' do
git_blob = Gitlab::Git::Blob.new(name: 'file.png')
expect(described_class.decorate(git_blob)).not_to be_stl
end
it 'is truthy with STL extension' do
git_blob = Gitlab::Git::Blob.new(name: 'file.stl')
expect(described_class.decorate(git_blob)).to be_stl
end
end
describe '#to_partial_path' do
let(:project) { double(lfs_enabled?: true) }
......@@ -122,7 +136,8 @@ describe Blob do
lfs_pointer?: false,
svg?: false,
text?: false,
binary?: false
binary?: false,
stl?: false
)
described_class.decorate(double).tap do |blob|
......@@ -175,6 +190,11 @@ describe Blob do
blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
expect(blob.to_partial_path(project)).to eq 'sketch'
end
it 'handles STLs' do
blob = stubbed_blob(text?: true, stl?: true)
expect(blob.to_partial_path(project)).to eq 'stl'
end
end
describe '#size_within_svg_limits?' do
......
......@@ -108,6 +108,15 @@ describe API::DeployKeys, api: true do
expect(response).to have_http_status(201)
end
it 'accepts can_push parameter' do
key_attrs = attributes_for :write_access_key
post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
expect(response).to have_http_status(201)
expect(json_response['can_push']).to eq(true)
end
end
describe 'DELETE /projects/:id/deploy_keys/:key_id' do
......
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
context 'push to origin repo source branch' do
let(:refresh_service) { service.new(@project, @user) }
before do
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
......@@ -70,6 +71,32 @@ describe MergeRequests::RefreshService, services: true do
end
end
context 'push to origin repo source branch when an MR was reopened' do
let(:refresh_service) { service.new(@project, @user) }
before do
@merge_request.update(state: :reopened)
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
end
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks).
with(@merge_request, 'update', @oldrev)
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
expect(@merge_request.diff_head_sha).to eq(@newrev)
expect(@fork_merge_request).to be_open
expect(@fork_merge_request.notes).to be_empty
expect(@build_failed_todo).to be_done
expect(@fork_build_failed_todo).to be_done
end
end
context 'push to origin repo target branch' do
before do
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
......
......@@ -23,10 +23,12 @@ describe RepositoryImportWorker do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
expect_any_instance_of(Projects::ImportService).to receive(:execute).
and_return({ status: :error, message: error })
allow(subject).to receive(:jid).and_return('123')
subject.perform(project.id)
expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/")
expect(project.reload.import_jid).not_to be_nil
end
end
end
......
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:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
three-orbit-controls@^82.1.0:
version "82.1.0"
resolved "https://registry.yarnpkg.com/three-orbit-controls/-/three-orbit-controls-82.1.0.tgz#11a7f33d0a20ecec98f098b37780f6537374fab4"
three-stl-loader@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/three-stl-loader/-/three-stl-loader-1.0.4.tgz#6b3319a31e3b910aab1883d19b00c81a663c3e03"
three@^0.84.0:
version "0.84.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
throttleit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
......
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