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