summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/dispatcher.js91
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/pages/admin/index.js2
-rw-r--r--app/assets/javascripts/pages/dashboard/activity/index.js2
-rw-r--r--app/assets/javascripts/pages/omniauth_callbacks/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/clusters/destroy/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/clusters/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/clusters/update/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/releases/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/services/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/snippets/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/snippets/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/snippets/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/tags/new/index.js4
-rw-r--r--app/assets/javascripts/pages/sessions/index.js4
-rw-r--r--app/assets/javascripts/pages/snippets/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/snippets/new/index.js2
-rw-r--r--app/assets/javascripts/pages/snippets/show/index.js11
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js10
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js (renamed from app/assets/javascripts/pipelines/pipeline_details_mediatior.js)3
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss9
-rw-r--r--app/finders/members_finder.rb57
-rw-r--r--app/finders/snippets_finder.rb6
-rw-r--r--app/models/project.rb46
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml12
27 files changed, 141 insertions, 160 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 5ca82ddef30..292cbce1ecf 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -51,70 +51,9 @@ var Dispatcher;
case 'projects:merge_requests:creations:new':
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
- case 'projects:blob:edit':
- case 'projects:blob:new':
- shortcut_handler = true;
- break;
- case 'projects:tags:new':
- import('./pages/projects/tags/new')
- .then(callDefault)
- .catch(fail);
- break;
- case 'projects:snippets:show':
- import('./pages/projects/snippets/show')
- .then(callDefault)
- .catch(fail);
- break;
- case 'projects:snippets:new':
- case 'projects:snippets:create':
- import('./pages/projects/snippets/new')
- .then(callDefault)
- .catch(fail);
- break;
- case 'projects:services:edit':
- import('./pages/projects/services/edit')
- .then(callDefault)
- .catch(fail);
- break;
- case 'projects:snippets:edit':
- case 'projects:snippets:update':
- import('./pages/projects/snippets/edit')
- .then(callDefault)
- .catch(fail);
- break;
- case 'snippets:new':
- import('./pages/snippets/new')
- .then(callDefault)
- .catch(fail);
- break;
- case 'snippets:edit':
- import('./pages/snippets/edit')
- .then(callDefault)
- .catch(fail);
- break;
- case 'snippets:create':
- import('./pages/snippets/new')
- .then(callDefault)
- .catch(fail);
- break;
- case 'snippets:update':
- import('./pages/snippets/edit')
- .then(callDefault)
- .catch(fail);
- break;
- case 'projects:releases:edit':
- import('./pages/projects/releases/edit')
- .then(callDefault)
- .catch(fail);
- break;
case 'projects:merge_requests:show':
shortcut_handler = true;
break;
- case 'dashboard:activity':
- import('./pages/dashboard/activity')
- .then(callDefault)
- .catch(fail);
- break;
case 'projects:commit:show':
import('./pages/projects/commit/show')
.then(callDefault)
@@ -311,11 +250,6 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
- case 'snippets:show':
- import('./pages/snippets/show')
- .then(callDefault)
- .catch(fail);
- break;
case 'import:fogbugz:new_user_map':
import('./pages/import/fogbugz/new_user_map')
.then(callDefault)
@@ -331,18 +265,6 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
- case 'projects:clusters:show':
- case 'projects:clusters:update':
- case 'projects:clusters:destroy':
- import('./pages/projects/clusters/show')
- .then(callDefault)
- .catch(fail);
- break;
- case 'projects:clusters:index':
- import('./pages/projects/clusters/index')
- .then(callDefault)
- .catch(fail);
- break;
case 'dashboard:groups:index':
import('./pages/dashboard/groups/index')
.then(callDefault)
@@ -350,20 +272,7 @@ var Dispatcher;
break;
}
switch (path[0]) {
- case 'sessions':
- import('./pages/sessions')
- .then(callDefault)
- .catch(fail);
- break;
- case 'omniauth_callbacks':
- import('./pages/omniauth_callbacks')
- .then(callDefault)
- .catch(fail);
- break;
case 'admin':
- import('./pages/admin')
- .then(callDefault)
- .catch(fail);
switch (path[1]) {
case 'broadcast_messages':
import('./pages/admin/broadcast_messages')
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 65e751dd460..dc9e5bb03f4 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -61,7 +61,7 @@ gl.lazyLoader = new LazyLoader({
observerNode: '#content-body',
});
-$(() => {
+document.addEventListener('DOMContentLoaded', () => {
const $body = $('body');
const $document = $(document);
const $window = $(window);
diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js
index 8b843037d85..e50b61f09e2 100644
--- a/app/assets/javascripts/pages/admin/index.js
+++ b/app/assets/javascripts/pages/admin/index.js
@@ -1,3 +1,3 @@
import initAdmin from './admin';
-export default () => initAdmin();
+document.addEventListener('DOMContentLoaded', initAdmin);
diff --git a/app/assets/javascripts/pages/dashboard/activity/index.js b/app/assets/javascripts/pages/dashboard/activity/index.js
index 95faf1f1e98..1b887cad496 100644
--- a/app/assets/javascripts/pages/dashboard/activity/index.js
+++ b/app/assets/javascripts/pages/dashboard/activity/index.js
@@ -1,3 +1,3 @@
import Activities from '~/activities';
-export default () => new Activities();
+document.addEventListener('DOMContentLoaded', () => new Activities());
diff --git a/app/assets/javascripts/pages/omniauth_callbacks/index.js b/app/assets/javascripts/pages/omniauth_callbacks/index.js
index 54f4e56359a..c2c069d1ca8 100644
--- a/app/assets/javascripts/pages/omniauth_callbacks/index.js
+++ b/app/assets/javascripts/pages/omniauth_callbacks/index.js
@@ -1,5 +1,3 @@
import initU2F from '../../shared/sessions/u2f';
-export default () => {
- initU2F();
-};
+document.addEventListener('DOMContentLoaded', initU2F);
diff --git a/app/assets/javascripts/pages/projects/clusters/destroy/index.js b/app/assets/javascripts/pages/projects/clusters/destroy/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/destroy/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/index/index.js b/app/assets/javascripts/pages/projects/clusters/index/index.js
index d531ab81dc7..e4b8baede58 100644
--- a/app/assets/javascripts/pages/projects/clusters/index/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index/index.js
@@ -1,5 +1,5 @@
import ClustersIndex from '~/clusters/clusters_index';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new ClustersIndex(); // eslint-disable-line no-new
-};
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js
index 0458c02a66f..8001d2dd1da 100644
--- a/app/assets/javascripts/pages/projects/clusters/show/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/show/index.js
@@ -1,5 +1,5 @@
import ClustersBundle from '~/clusters/clusters_bundle';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new ClustersBundle(); // eslint-disable-line no-new
-};
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/update/index.js b/app/assets/javascripts/pages/projects/clusters/update/index.js
new file mode 100644
index 00000000000..8001d2dd1da
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/update/index.js
@@ -0,0 +1,5 @@
+import ClustersBundle from '~/clusters/clusters_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new ClustersBundle(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/projects/releases/edit/index.js b/app/assets/javascripts/pages/projects/releases/edit/index.js
index 3d997cdfff0..0bf53a8de09 100644
--- a/app/assets/javascripts/pages/projects/releases/edit/index.js
+++ b/app/assets/javascripts/pages/projects/releases/edit/index.js
@@ -1,3 +1,3 @@
import initForm from '~/pages/projects/init_form';
-export default initForm($('.release-form'));
+document.addEventListener('DOMContentLoaded', () => initForm($('.release-form')));
diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js
index 5c73171e62e..ba4b271f09e 100644
--- a/app/assets/javascripts/pages/projects/services/edit/index.js
+++ b/app/assets/javascripts/pages/projects/services/edit/index.js
@@ -1,7 +1,7 @@
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
@@ -10,4 +10,4 @@ export default () => {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
}
-};
+});
diff --git a/app/assets/javascripts/pages/projects/snippets/edit/index.js b/app/assets/javascripts/pages/projects/snippets/edit/index.js
index 9edb16dc73b..caf9ee9b398 100644
--- a/app/assets/javascripts/pages/projects/snippets/edit/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/edit/index.js
@@ -1,3 +1,3 @@
import initForm from '~/pages/projects/init_form';
-export default initForm($('.snippet-form'));
+document.addEventListener('DOMContentLoaded', () => initForm($('.snippet-form')));
diff --git a/app/assets/javascripts/pages/projects/snippets/new/index.js b/app/assets/javascripts/pages/projects/snippets/new/index.js
index 9edb16dc73b..caf9ee9b398 100644
--- a/app/assets/javascripts/pages/projects/snippets/new/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/new/index.js
@@ -1,3 +1,3 @@
import initForm from '~/pages/projects/init_form';
-export default initForm($('.snippet-form'));
+document.addEventListener('DOMContentLoaded', () => initForm($('.snippet-form')));
diff --git a/app/assets/javascripts/pages/projects/snippets/show/index.js b/app/assets/javascripts/pages/projects/snippets/show/index.js
index a3cf75c385b..a134599cb04 100644
--- a/app/assets/javascripts/pages/projects/snippets/show/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/show/index.js
@@ -3,9 +3,9 @@ import ZenMode from '~/zen_mode';
import LineHighlighter from '../../../../line_highlighter';
import BlobViewer from '../../../../blob/viewer';
-export default function () {
+document.addEventListener('DOMContentLoaded', () => {
new LineHighlighter(); // eslint-disable-line no-new
new BlobViewer(); // eslint-disable-line no-new
initNotes();
new ZenMode(); // eslint-disable-line no-new
-}
+});
diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js
index dacc2875c8c..191c98b36bb 100644
--- a/app/assets/javascripts/pages/projects/tags/new/index.js
+++ b/app/assets/javascripts/pages/projects/tags/new/index.js
@@ -2,8 +2,8 @@ import RefSelectDropdown from '../../../../ref_select_dropdown';
import ZenMode from '../../../../zen_mode';
import GLForm from '../../../../gl_form';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.tag-form'), true); // eslint-disable-line no-new
new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new
-};
+});
diff --git a/app/assets/javascripts/pages/sessions/index.js b/app/assets/javascripts/pages/sessions/index.js
index 54f4e56359a..c2c069d1ca8 100644
--- a/app/assets/javascripts/pages/sessions/index.js
+++ b/app/assets/javascripts/pages/sessions/index.js
@@ -1,5 +1,3 @@
import initU2F from '../../shared/sessions/u2f';
-export default () => {
- initU2F();
-};
+document.addEventListener('DOMContentLoaded', initU2F);
diff --git a/app/assets/javascripts/pages/snippets/edit/index.js b/app/assets/javascripts/pages/snippets/edit/index.js
index 9c664b5f1ff..2ee38b64ca1 100644
--- a/app/assets/javascripts/pages/snippets/edit/index.js
+++ b/app/assets/javascripts/pages/snippets/edit/index.js
@@ -1,3 +1,3 @@
import form from '../form';
-export default form;
+document.addEventListener('DOMContentLoaded', form);
diff --git a/app/assets/javascripts/pages/snippets/new/index.js b/app/assets/javascripts/pages/snippets/new/index.js
index 9c664b5f1ff..2ee38b64ca1 100644
--- a/app/assets/javascripts/pages/snippets/new/index.js
+++ b/app/assets/javascripts/pages/snippets/new/index.js
@@ -1,3 +1,3 @@
import form from '../form';
-export default form;
+document.addEventListener('DOMContentLoaded', form);
diff --git a/app/assets/javascripts/pages/snippets/show/index.js b/app/assets/javascripts/pages/snippets/show/index.js
index 04c9562bfbb..f548b9fad65 100644
--- a/app/assets/javascripts/pages/snippets/show/index.js
+++ b/app/assets/javascripts/pages/snippets/show/index.js
@@ -1,12 +1,11 @@
-/* eslint-disable no-new */
import LineHighlighter from '../../../line_highlighter';
import BlobViewer from '../../../blob/viewer';
import ZenMode from '../../../zen_mode';
import initNotes from '../../../init_notes';
-export default () => {
- new LineHighlighter();
- new BlobViewer();
+document.addEventListener('DOMContentLoaded', () => {
+ new LineHighlighter(); // eslint-disable-line no-new
+ new BlobViewer(); // eslint-disable-line no-new
initNotes();
- new ZenMode();
-};
+ new ZenMode(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index a1f58580318..ab84711d4a2 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -52,7 +52,7 @@
</script>
<template>
<div class="build-content middle-block js-pipeline-graph">
- <div class="pipeline-visualization pipeline-graph">
+ <div class="pipeline-visualization pipeline-graph pipeline-tab-content">
<div class="text-center">
<loading-icon
v-if="isLoading"
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index d88d280cb3f..705a60b3ba2 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,10 +1,14 @@
import Vue from 'vue';
-import Flash from '../flash';
-import PipelinesMediator from './pipeline_details_mediatior';
+import Flash from '~/flash';
+import Translate from '~/vue_shared/translate';
+import { __ } from '~/locale';
+import PipelinesMediator from './pipeline_details_mediator';
import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
+Vue.use(Translate);
+
document.addEventListener('DOMContentLoaded', () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
@@ -54,7 +58,7 @@ document.addEventListener('DOMContentLoaded', () => {
postAction(action) {
this.mediator.service.postAction(action.path)
.then(() => this.mediator.refreshPipeline())
- .catch(() => new Flash('An error occurred while making the request.'));
+ .catch(() => Flash(__('An error occurred while making the request.')));
},
},
render(createElement) {
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index 823ccd849f4..10f238fe73b 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -1,6 +1,7 @@
import Visibility from 'visibilityjs';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
+import { __ } from '../locale';
import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
@@ -47,7 +48,7 @@ export default class pipelinesMediator {
errorCallback() {
this.state.isLoading = false;
- return new Flash('An error occurred while fetching the pipeline.');
+ Flash(__('An error occurred while fetching the pipeline.'));
}
refreshPipeline() {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index f10908c3630..42772f13155 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -320,14 +320,17 @@
}
}
-// Pipeline graph
-.pipeline-graph {
+.pipeline-tab-content {
width: 100%;
background-color: $gray-light;
padding: $gl-padding;
+ overflow: auto;
+}
+
+// Pipeline graph
+.pipeline-graph {
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
- overflow: auto;
.stage-column-list,
.builds-container > ul {
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index af24045886e..4734d97b8c7 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -10,26 +10,59 @@ class MembersFinder
def execute
project_members = project.project_members
project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
- wheres = ["members.id IN (#{project_members.select(:id).to_sql})"]
if group
- # We need `.where.not(user_id: nil)` here otherwise when a group has an
- # invitee, it would make the following query return 0 rows since a NULL
- # user_id would be present in the subquery
- # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
- non_null_user_ids = project_members.where.not(user_id: nil).select(:user_id)
-
group_members = GroupMembersFinder.new(group).execute
- group_members = group_members.where.not(user_id: non_null_user_ids)
- group_members = group_members.non_invite unless can?(current_user, :admin_group, group)
+ group_members = group_members.non_invite
- wheres << "members.id IN (#{group_members.select(:id).to_sql})"
- end
+ union = Gitlab::SQL::Union.new([project_members, group_members], remove_duplicates: false)
- Member.where(wheres.join(' OR '))
+ sql = distinct_on(union)
+
+ Member.includes(:user).from("(#{sql}) AS #{Member.table_name}")
+ else
+ project_members
+ end
end
def can?(*args)
Ability.allowed?(*args)
end
+
+ private
+
+ def distinct_on(union)
+ # We're interested in a list of members without duplicates by user_id.
+ # We prefer project members over group members, project members should go first.
+ if Gitlab::Database.postgresql?
+ <<~SQL
+ SELECT DISTINCT ON (user_id, invite_email) member_union.*
+ FROM (#{union.to_sql}) AS member_union
+ ORDER BY user_id,
+ invite_email,
+ CASE
+ WHEN type = 'ProjectMember' THEN 1
+ WHEN type = 'GroupMember' THEN 2
+ ELSE 3
+ END
+ SQL
+ else
+ # Older versions of MySQL do not support window functions (and DISTINCT ON is postgres-specific).
+ <<~SQL
+ SELECT t1.*
+ FROM (#{union.to_sql}) AS t1
+ JOIN (
+ SELECT
+ COALESCE(user_id, -1) AS user_id,
+ COALESCE(invite_email, 'NULL') AS invite_email,
+ MIN(CASE WHEN type = 'ProjectMember' THEN 1 WHEN type = 'GroupMember' THEN 2 ELSE 3 END) AS type_number
+ FROM
+ (#{union.to_sql}) AS t3
+ GROUP BY COALESCE(user_id, -1), COALESCE(invite_email, 'NULL')
+ ) AS t2 ON COALESCE(t1.user_id, -1) = t2.user_id
+ AND COALESCE(t1.invite_email, 'NULL') = t2.invite_email
+ AND CASE WHEN t1.type = 'ProjectMember' THEN 1 WHEN t1.type = 'GroupMember' THEN 2 ELSE 3 END = t2.type_number
+ SQL
+ end
+ end
end
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index 33359fa1efb..ec61fe1892e 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -56,8 +56,10 @@ class SnippetsFinder < UnionFinder
end
def feature_available_projects
- projects = Project.public_or_visible_to_user(current_user)
- .with_feature_available_for_user(:snippets, current_user).select(:id)
+ projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part|
+ part.with_feature_available_for_user(:snippets, current_user)
+ end.select(:id)
+
arel_query = Arel::Nodes::SqlLiteral.new(projects.to_sql)
table[:project_id].in(arel_query)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 2ba6a863500..79058d51af8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -316,18 +316,42 @@ class Project < ActiveRecord::Base
# Returns a collection of projects that is either public or visible to the
# logged in user.
- def self.public_or_visible_to_user(user = nil)
- if user
- authorized = user
- .project_authorizations
- .select(1)
- .where('project_authorizations.project_id = projects.id')
-
- levels = Gitlab::VisibilityLevel.levels_for_user(user)
-
- where('EXISTS (?) OR projects.visibility_level IN (?)', authorized, levels)
+ #
+ # A caller may pass in a block to modify individual parts of
+ # the query, e.g. to apply .with_feature_available_for_user on top of it.
+ # This is useful for performance as we can stick those additional filters
+ # at the bottom of e.g. the UNION.
+ #
+ # Optionally, turning `use_where_in` off leads to returning a
+ # relation using #from instead of #where. This can perform much better
+ # but leads to trouble when used in conjunction with AR's #merge method.
+ def self.public_or_visible_to_user(user = nil, use_where_in: true, &block)
+ # If we don't get a block passed, use identity to avoid if/else repetitions
+ block = ->(part) { part } unless block_given?
+
+ return block.call(public_to_user) unless user
+
+ # If the user is allowed to see all projects,
+ # we can shortcut and just return.
+ return block.call(all) if user.full_private_access?
+
+ authorized = user
+ .project_authorizations
+ .select(1)
+ .where('project_authorizations.project_id = projects.id')
+ authorized_projects = block.call(where('EXISTS (?)', authorized))
+
+ levels = Gitlab::VisibilityLevel.levels_for_user(user)
+ visible_projects = block.call(where(visibility_level: levels))
+
+ # We use a UNION here instead of OR clauses since this results in better
+ # performance.
+ union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
+
+ if use_where_in
+ where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
else
- public_to_user
+ from("(#{union.to_sql}) AS #{table_name}")
end
end
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 5de17977d5a..852143ecb2a 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -3,16 +3,16 @@
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator
%li.js-pipeline-tab-link
- = link_to project_pipeline_path(@project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
- Pipeline
+ = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
+ = _("Pipeline")
%li.js-builds-tab-link
- = link_to builds_project_pipeline_path(@project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
- Jobs
+ = link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
+ = _("Jobs")
%span.badge.js-builds-counter= pipeline.total_size
- if failed_builds.present?
%li.js-failures-tab-link
- = link_to failures_project_pipeline_path(@project, @pipeline), data: {target: 'div#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
- Failed Jobs
+ = link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
+ = _("Failed Jobs")
%span.badge.js-failures-counter= failed_builds.count
.tab-content