Commit 25bd5413 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' into build-chunks-on-object-storage

parents b025e89e 969b7c56
*.erb *.erb
lib/gitlab/sanitizers/svg/whitelist.rb lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb lib/gitlab/diff/position_tracer.rb
app/controllers/projects/approver_groups_controller.rb
app/controllers/projects/approvers_controller.rb
app/controllers/projects/protected_branches/merge_access_levels_controller.rb
app/controllers/projects/protected_branches/push_access_levels_controller.rb
app/controllers/projects/protected_tags/create_access_levels_controller.rb
app/policies/project_policy.rb app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb lib/gitlab/gitaly_client/operation_service.rb
app/models/project_services/packagist_service.rb
lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
lib/gitlab/background_migration/* lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb app/models/project_services/kubernetes_service.rb
lib/gitlab/workhorse.rb lib/gitlab/workhorse.rb
...@@ -19,6 +26,8 @@ ee/db/**/* ...@@ -19,6 +26,8 @@ ee/db/**/*
ee/app/serializers/ee/merge_request_widget_entity.rb ee/app/serializers/ee/merge_request_widget_entity.rb
ee/lib/api/epics.rb ee/lib/api/epics.rb
ee/lib/api/geo_nodes.rb ee/lib/api/geo_nodes.rb
ee/lib/ee/api/group_boards.rb
ee/lib/ee/api/boards.rb
ee/lib/ee/gitlab/ldap/sync/admin_users.rb ee/lib/ee/gitlab/ldap/sync/admin_users.rb
ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb
ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb
......
...@@ -269,10 +269,10 @@ package-and-qa: ...@@ -269,10 +269,10 @@ package-and-qa:
<<: *single-script-job-variables <<: *single-script-job-variables
SCRIPT_NAME: trigger-build-docs SCRIPT_NAME: trigger-build-docs
environment: environment:
name: review-docs/$CI_COMMIT_REF_NAME name: review-docs/$CI_COMMIT_REF_SLUG
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693 # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
url: http://$DOCS_GITLAB_REPO_SUFFIX-$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX url: http://$CI_ENVIRONMENT_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup on_stop: review-docs-cleanup
# Trigger a manual docs build in gitlab-docs only on non docs-only branches. # Trigger a manual docs build in gitlab-docs only on non docs-only branches.
...@@ -298,9 +298,8 @@ review-docs-deploy: ...@@ -298,9 +298,8 @@ review-docs-deploy:
- gem install gitlab --no-ri --no-rdoc - gem install gitlab --no-ri --no-rdoc
- ./$SCRIPT_NAME deploy - ./$SCRIPT_NAME deploy
only: only:
- /(^docs[\/-].*|.*-docs$)/ - /(^docs[\/-].*|.*-docs$)/@gitlab-org/gitlab-ce
- branches@gitlab-org/gitlab-ce - /(^docs[\/-].*|.*-docs$)/@gitlab-org/gitlab-ee
- branches@gitlab-org/gitlab-ee
<<: *except-qa <<: *except-qa
# Cleanup remote environment of gitlab-docs # Cleanup remote environment of gitlab-docs
...@@ -308,7 +307,7 @@ review-docs-cleanup: ...@@ -308,7 +307,7 @@ review-docs-cleanup:
<<: *review-docs <<: *review-docs
stage: post-cleanup stage: post-cleanup
environment: environment:
name: review-docs/$CI_COMMIT_REF_NAME name: review-docs/$CI_COMMIT_REF_SLUG
action: stop action: stop
when: manual when: manual
script: script:
...@@ -326,11 +325,9 @@ cloud-native-image: ...@@ -326,11 +325,9 @@ cloud-native-image:
variables: variables:
GIT_DEPTH: "1" GIT_DEPTH: "1"
cache: {} cache: {}
before_script:
- gem install gitlab --no-rdoc --no-ri
- chmod 755 ./scripts/trigger-build-cloud-native
script: script:
- ./scripts/trigger-build-cloud-native - gem install gitlab --no-ri --no-rdoc
- ./trigger-build cng
only: only:
- tags@gitlab-org/gitlab-ce - tags@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ee - tags@gitlab-org/gitlab-ee
......
...@@ -10,10 +10,6 @@ ...@@ -10,10 +10,6 @@
Capybara/CurrentPathExpectation: Capybara/CurrentPathExpectation:
Enabled: false Enabled: false
# Offense count: 956
Capybara/FeatureMethods:
Enabled: false
# Offense count: 23 # Offense count: 23
FactoryBot/DynamicAttributeDefinedStatically: FactoryBot/DynamicAttributeDefinedStatically:
Exclude: Exclude:
......
...@@ -2,6 +2,26 @@ ...@@ -2,6 +2,26 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.0.3 (2018-07-05)
### Fixed (14 changes, 1 of them is from the community)
- Revert merge request widget button max height. !20175 (George Tsiolis)
- Implement upload copy when moving an issue with upload on object storage. !20191
- Fix broken '!' support to autocomplete MRs in GFM fields. !20204
- Restore showing Elasticsearch and Geo status on dashboard. !20276
- Fix merge request page rendering error when its target/source branch is missing. !20280
- Fix sidebar collapse breapoints for job and wiki pages.
- fix size of code blocks in headings.
- Fix loading screen for search autocomplete dropdown.
- Fix ambiguous due_date column for Issue scopes.
- Always serve favicon from main GitLab domain so that CI badge can be drawn over it.
- Fix tooltip flickering bug.
- Fix refreshing cache keys for open issues count.
- Replace deprecated bs.affix in merge request tabs with sticky polyfill.
- Prevent pipeline job tooltip from scrolling off dropdown container.
## 11.0.2 (2018-06-26) ## 11.0.2 (2018-06-26)
### Fixed (8 changes, 1 of them is from the community) ### Fixed (8 changes, 1 of them is from the community)
......
...@@ -47,7 +47,7 @@ gem 'omniauth-google-oauth2', '~> 0.5.3' ...@@ -47,7 +47,7 @@ gem 'omniauth-google-oauth2', '~> 0.5.3'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2' gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.10' gem 'omniauth-saml', '~> 1.10'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.3.0'
gem 'omniauth-twitter', '~> 1.4' gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.3' gem 'omniauth-authentiq', '~> 0.3.3'
...@@ -132,7 +132,7 @@ gem 'unf', '~> 0.1.4' ...@@ -132,7 +132,7 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.7' gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 2.7.1' gem 'html-pipeline', '~> 2.8'
gem 'deckar01-task_list', '2.0.0' gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.4' gem 'gitlab-markup', '~> 1.6.4'
gem 'redcarpet', '~> 3.4' gem 'redcarpet', '~> 3.4'
......
...@@ -108,7 +108,7 @@ GEM ...@@ -108,7 +108,7 @@ GEM
capybara-screenshot (1.0.14) capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3) capybara (>= 1.0, < 3)
launchy launchy
carrierwave (1.2.1) carrierwave (1.2.3)
activemodel (>= 4.0.0) activemodel (>= 4.0.0)
activesupport (>= 4.0.0) activesupport (>= 4.0.0)
mime-types (>= 1.16) mime-types (>= 1.16)
...@@ -394,7 +394,7 @@ GEM ...@@ -394,7 +394,7 @@ GEM
hipchat (1.5.2) hipchat (1.5.2)
httparty httparty
mimemagic mimemagic
html-pipeline (2.7.1) html-pipeline (2.8.3)
activesupport (>= 2) activesupport (>= 2)
nokogiri (>= 1.4) nokogiri (>= 1.4)
html2text (0.2.0) html2text (0.2.0)
...@@ -568,7 +568,7 @@ GEM ...@@ -568,7 +568,7 @@ GEM
omniauth-saml (1.10.0) omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2) omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7) ruby-saml (~> 1.7)
omniauth-shibboleth (1.2.1) omniauth-shibboleth (1.3.0)
omniauth (>= 1.0.0) omniauth (>= 1.0.0)
omniauth-twitter (1.4.0) omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1) omniauth-oauth (~> 1.1)
...@@ -1061,7 +1061,7 @@ DEPENDENCIES ...@@ -1061,7 +1061,7 @@ DEPENDENCIES
hashie-forbidden_attributes hashie-forbidden_attributes
health_check (~> 2.6.0) health_check (~> 2.6.0)
hipchat (~> 1.5.0) hipchat (~> 1.5.0)
html-pipeline (~> 2.7.1) html-pipeline (~> 2.8)
html2text html2text
httparty (~> 0.13.3) httparty (~> 0.13.3)
icalendar icalendar
...@@ -1101,7 +1101,7 @@ DEPENDENCIES ...@@ -1101,7 +1101,7 @@ DEPENDENCIES
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.3.0)
omniauth-twitter (~> 1.4) omniauth-twitter (~> 1.4)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12) org-ruby (~> 0.9.12)
......
...@@ -397,7 +397,7 @@ GEM ...@@ -397,7 +397,7 @@ GEM
hipchat (1.5.2) hipchat (1.5.2)
httparty httparty
mimemagic mimemagic
html-pipeline (2.7.1) html-pipeline (2.8.3)
activesupport (>= 2) activesupport (>= 2)
nokogiri (>= 1.4) nokogiri (>= 1.4)
html2text (0.2.0) html2text (0.2.0)
...@@ -572,7 +572,7 @@ GEM ...@@ -572,7 +572,7 @@ GEM
omniauth-saml (1.10.0) omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2) omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7) ruby-saml (~> 1.7)
omniauth-shibboleth (1.2.1) omniauth-shibboleth (1.3.0)
omniauth (>= 1.0.0) omniauth (>= 1.0.0)
omniauth-twitter (1.4.0) omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1) omniauth-oauth (~> 1.1)
...@@ -1071,7 +1071,7 @@ DEPENDENCIES ...@@ -1071,7 +1071,7 @@ DEPENDENCIES
hashie-forbidden_attributes hashie-forbidden_attributes
health_check (~> 2.6.0) health_check (~> 2.6.0)
hipchat (~> 1.5.0) hipchat (~> 1.5.0)
html-pipeline (~> 2.7.1) html-pipeline (~> 2.8)
html2text html2text
httparty (~> 0.13.3) httparty (~> 0.13.3)
icalendar icalendar
...@@ -1111,7 +1111,7 @@ DEPENDENCIES ...@@ -1111,7 +1111,7 @@ DEPENDENCIES
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.3.0)
omniauth-twitter (~> 1.4) omniauth-twitter (~> 1.4)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12) org-ruby (~> 0.9.12)
......
...@@ -120,6 +120,10 @@ All documentation can be found on [docs.gitlab.com/ce/](https://docs.gitlab.com/ ...@@ -120,6 +120,10 @@ All documentation can be found on [docs.gitlab.com/ce/](https://docs.gitlab.com/
Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on our website for the many options to get help. Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on our website for the many options to get help.
## Why?
[Read here](https://about.gitlab.com/why/)
## Is it any good? ## Is it any good?
[Yes](https://news.ycombinator.com/item?id=3067434) [Yes](https://news.ycombinator.com/item?id=3067434)
......
...@@ -28,23 +28,29 @@ export default { ...@@ -28,23 +28,29 @@ export default {
}, },
}, },
methods: { methods: {
buildUpdateRequest(list) {
return {
add_label_ids: [list.label.id],
};
},
addIssues() { addIssues() {
const firstListIndex = 1; const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex]; const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues(); const selectedIssues = ModalStore.getSelectedIssues();
const issueIds = selectedIssues.map(issue => issue.id); const issueIds = selectedIssues.map(issue => issue.id);
const req = this.buildUpdateRequest(list);
// Post the data to the backend // Post the data to the backend
gl.boardService.bulkUpdate(issueIds, { gl.boardService
add_label_ids: [list.label.id], .bulkUpdate(issueIds, req)
}).catch(() => { .catch(() => {
Flash(__('Failed to update issues, please try again.')); Flash(__('Failed to update issues, please try again.'));
selectedIssues.forEach((issue) => { selectedIssues.forEach((issue) => {
list.removeIssue(issue); list.removeIssue(issue);
list.issuesSize -= 1; list.issuesSize -= 1;
});
}); });
});
// Add the issues on the frontend // Add the issues on the frontend
selectedIssues.forEach((issue) => { selectedIssues.forEach((issue) => {
......
...@@ -121,8 +121,7 @@ ...@@ -121,8 +121,7 @@
<div <div
v-if="issuesCount > 0 && issues.length === 0" v-if="issuesCount > 0 && issues.length === 0"
class="empty-state add-issues-empty-state-filter text-center"> class="empty-state add-issues-empty-state-filter text-center">
<div <div class="svg-content">
class="svg-content">
<img :src="emptyStateSvg" /> <img :src="emptyStateSvg" />
</div> </div>
<div class="text-content"> <div class="text-content">
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
export default { export default Vue.extend({
props: { props: {
issue: { issue: {
type: Object, type: Object,
...@@ -25,19 +25,16 @@ ...@@ -25,19 +25,16 @@
removeIssue() { removeIssue() {
const { issue } = this; const { issue } = this;
const lists = issue.getLists(); const lists = issue.getLists();
const listLabelIds = lists.map(list => list.label.id); const req = this.buildPatchRequest(issue, lists);
let labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
if (labelIds.length === 0) {
labelIds = [''];
}
const data = { const data = {
issue: { issue: this.seedPatchRequest(issue, req),
label_ids: labelIds,
},
}; };
if (data.issue.label_ids.length === 0) {
data.issue.label_ids = [''];
}
// Post the remove data // Post the remove data
Vue.http.patch(this.updateUrl, data).catch(() => { Vue.http.patch(this.updateUrl, data).catch(() => {
Flash(__('Failed to remove issue from board, please try again.')); Flash(__('Failed to remove issue from board, please try again.'));
...@@ -54,8 +51,30 @@ ...@@ -54,8 +51,30 @@
Store.detail.issue = {}; Store.detail.issue = {};
}, },
/**
* Build the default patch request.
*/
buildPatchRequest(issue, lists) {
const listLabelIds = lists.map(list => list.label.id);
const labelIds = issue.labels
.map(label => label.id)
.filter(id => !listLabelIds.includes(id));
return {
label_ids: labelIds,
};
},
/**
* Seed the given patch request.
*
* (This is overridden in EE)
*/
seedPatchRequest(issue, req) {
return req;
},
}, },
}; });
</script> </script>
<template> <template>
<div <div
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
/* global ListAssignee */ /* global ListAssignee */
import Vue from 'vue'; import Vue from 'vue';
import '~/vue_shared/models/label';
import IssueProject from './project'; import IssueProject from './project';
class ListIssue { class ListIssue {
......
...@@ -7,6 +7,24 @@ import queryData from '../utils/query_data'; ...@@ -7,6 +7,24 @@ import queryData from '../utils/query_data';
const PER_PAGE = 20; const PER_PAGE = 20;
const TYPES = {
backlog: {
isPreset: true,
isExpandable: true,
isBlank: false,
},
closed: {
isPreset: true,
isExpandable: true,
isBlank: false,
},
blank: {
isPreset: true,
isExpandable: false,
isBlank: true,
},
};
class List { class List {
constructor(obj, defaultAvatar) { constructor(obj, defaultAvatar) {
this.id = obj.id; this.id = obj.id;
...@@ -14,8 +32,10 @@ class List { ...@@ -14,8 +32,10 @@ class List {
this.position = obj.position; this.position = obj.position;
this.title = obj.title; this.title = obj.title;
this.type = obj.list_type; this.type = obj.list_type;
this.preset = ['backlog', 'closed', 'blank'].indexOf(this.type) > -1;
this.isExpandable = ['backlog', 'closed'].indexOf(this.type) > -1; const typeInfo = this.getTypeInfo(this.type);
this.preset = !!typeInfo.isPreset;
this.isExpandable = !!typeInfo.isExpandable;
this.isExpanded = true; this.isExpanded = true;
this.page = 1; this.page = 1;
this.loading = true; this.loading = true;
...@@ -31,7 +51,7 @@ class List { ...@@ -31,7 +51,7 @@ class List {
this.title = this.assignee.name; this.title = this.assignee.name;
} }
if (this.type !== 'blank' && this.id) { if (!typeInfo.isBlank && this.id) {
this.getIssues().catch(() => { this.getIssues().catch(() => {
// TODO: handle request error // TODO: handle request error
}); });
...@@ -107,7 +127,7 @@ class List { ...@@ -107,7 +127,7 @@ class List {
return gl.boardService return gl.boardService
.getIssuesForList(this.id, data) .getIssuesForList(this.id, data)
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
this.loading = false; this.loading = false;
this.issuesSize = data.size; this.issuesSize = data.size;
...@@ -126,18 +146,7 @@ class List { ...@@ -126,18 +146,7 @@ class List {
return gl.boardService return gl.boardService
.newIssue(this.id, issue) .newIssue(this.id, issue)
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => this.onNewIssueResponse(issue, data));
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
issue.path = data.real_path;
issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
}
});
} }
createIssues(data) { createIssues(data) {
...@@ -217,6 +226,25 @@ class List { ...@@ -217,6 +226,25 @@ class List {
return !matchesRemove; return !matchesRemove;
}); });
} }
getTypeInfo (type) {
return TYPES[type] || {};
}
onNewIssueResponse (issue, data) {
issue.id = data.id;
issue.iid = data.iid;
issue.project = data.project;
issue.path = data.real_path;
issue.referencePath = data.reference_path;
if (this.issuesSize > 1) {
const moveBeforeId = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
}
}
} }
window.List = List; window.List = List;
export default List;
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import _ from 'underscore'; import _ from 'underscore';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Tooltip from '~/vue_shared/directives/tooltip'; import Tooltip from '~/vue_shared/directives/tooltip';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
ClipboardButton, ClipboardButton,
EditButton, EditButton,
Icon, Icon,
FileIcon,
}, },
directives: { directives: {
Tooltip, Tooltip,
...@@ -139,18 +141,20 @@ export default { ...@@ -139,18 +141,20 @@ export default {
:name="collapseIcon" :name="collapseIcon"
:size="16" :size="16"
aria-hidden="true" aria-hidden="true"
class="diff-toggle-caret" class="diff-toggle-caret append-right-5"
@click.stop="handleToggle" @click.stop="handleToggle"
/> />
<a <a
ref="titleWrapper" ref="titleWrapper"
:href="titleLink" :href="titleLink"
class="append-right-4"
> >
<i <file-icon
:class="`fa-${icon}`" :file-name="filePath"
class="fa fa-fw" :size="18"
aria-hidden="true" aria-hidden="true"
></i> css-classes="js-file-icon append-right-5"
/>
<span v-if="diffFile.renamedFile"> <span v-if="diffFile.renamedFile">
<strong <strong
v-tooltip v-tooltip
......
...@@ -24,19 +24,21 @@ export default { ...@@ -24,19 +24,21 @@ export default {
...mapGetters(['commit']), ...mapGetters(['commit']),
parallelDiffLines() { parallelDiffLines() {
return this.diffLines.map(line => { return this.diffLines.map(line => {
const parallelLine = Object.assign({}, line);
if (line.left) { if (line.left) {
Object.assign(line, { left: trimFirstCharOfLineContent(line.left) }); parallelLine.left = trimFirstCharOfLineContent(line.left);
} else { } else {
Object.assign(line, { left: { type: EMPTY_CELL_TYPE } }); parallelLine.left = { type: EMPTY_CELL_TYPE };
} }
if (line.right) { if (line.right) {
Object.assign(line, { right: trimFirstCharOfLineContent(line.right) }); parallelLine.right = trimFirstCharOfLineContent(line.right);
} else { } else {
Object.assign(line, { right: { type: EMPTY_CELL_TYPE } }); parallelLine.right = { type: EMPTY_CELL_TYPE };
} }
return line; return parallelLine;
}); });
}, },
diffLinesLength() { diffLinesLength() {
......
...@@ -155,18 +155,21 @@ export function addContextLines(options) { ...@@ -155,18 +155,21 @@ export function addContextLines(options) {
} }
} }
export function trimFirstCharOfLineContent(line) { /**
if (!line.richText) { * Trims the first char of the `richText` property when it's either a space or a diff symbol.
return line; * @param {Object} line
} * @returns {Object}
*/
const firstChar = line.richText.charAt(0); export function trimFirstCharOfLineContent(line = {}) {
const parsedLine = Object.assign({}, line);
if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
Object.assign(line, { if (line.richText) {
richText: line.richText.substring(1), const firstChar = parsedLine.richText.charAt(0);
});
if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
parsedLine.richText = line.richText.substring(1);
}
} }
return line; return parsedLine;
} }
import $ from 'jquery'; import $ from 'jquery';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import flash from '~/flash'; import flash from '~/flash';
import { stripHtml } from '~/lib/utils/text_utility';
import * as rootTypes from '../../mutation_types'; import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils'; import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import router from '../../../ide_router'; import router from '../../../ide_router';
...@@ -198,11 +197,18 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -198,11 +197,18 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
if (err.response.status === 400) { if (err.response.status === 400) {
$('#ide-create-branch-modal').modal('show'); $('#ide-create-branch-modal').modal('show');
} else { } else {
let errMsg = __('Error committing changes. Please try again.'); dispatch(
if (err.response.data && err.response.data.message) { 'setErrorMessage',
errMsg += ` (${stripHtml(err.response.data.message)})`; {
} text: __('An error accured whilst committing your changes.'),
flash(errMsg, 'alert', document, null, false, true); action: () =>
dispatch('commitChanges').then(() =>
dispatch('setErrorMessage', null, { root: true }),
),
actionText: __('Please try again'),
},
{ root: true },
);
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
} }
......
import { __ } from '../../../../locale'; import { __ } from '../../../../locale';
import Api from '../../../../api'; import Api from '../../../../api';
import flash from '../../../../flash';
import router from '../../../ide_router'; import router from '../../../ide_router';
import { scopes } from './constants'; import { scopes } from './constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -8,8 +7,20 @@ import * as rootTypes from '../../mutation_types'; ...@@ -8,8 +7,20 @@ import * as rootTypes from '../../mutation_types';
export const requestMergeRequests = ({ commit }, type) => export const requestMergeRequests = ({ commit }, type) =>
commit(types.REQUEST_MERGE_REQUESTS, type); commit(types.REQUEST_MERGE_REQUESTS, type);
export const receiveMergeRequestsError = ({ commit }, type) => { export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => {
flash(__('Error loading merge requests.')); dispatch(
'setErrorMessage',
{
text: __('Error loading merge requests.'),
action: payload =>
dispatch('fetchMergeRequests', payload).then(() =>
dispatch('setErrorMessage', null, { root: true }),
),
actionText: __('Please try again'),
actionPayload: { type, search },
},
{ root: true },
);
commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type); commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type);
}; };
export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) => export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) =>
...@@ -22,7 +33,7 @@ export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, searc ...@@ -22,7 +33,7 @@ export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, searc
Api.mergeRequests({ scope, state, search }) Api.mergeRequests({ scope, state, search })
.then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data })) .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data }))
.catch(() => dispatch('receiveMergeRequestsError', type)); .catch(() => dispatch('receiveMergeRequestsError', { type, search }));
}; };
export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type); export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type);
......
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import axios from 'axios'; import axios from 'axios';
import httpStatus from '../../../../lib/utils/http_status';
import { __ } from '../../../../locale'; import { __ } from '../../../../locale';
import flash from '../../../../flash';
import Poll from '../../../../lib/utils/poll'; import Poll from '../../../../lib/utils/poll';
import service from '../../../services'; import service from '../../../services';
import { rightSidebarViews } from '../../../constants'; import { rightSidebarViews } from '../../../constants';
...@@ -18,10 +18,27 @@ export const stopPipelinePolling = () => { ...@@ -18,10 +18,27 @@ export const stopPipelinePolling = () => {
export const restartPipelinePolling = () => { export const restartPipelinePolling = () => {
if (eTagPoll) eTagPoll.restart(); if (eTagPoll) eTagPoll.restart();
}; };
export const forcePipelineRequest = () => {
if (eTagPoll) eTagPoll.makeRequest();
};
export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE); export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
export const receiveLatestPipelineError = ({ commit, dispatch }) => { export const receiveLatestPipelineError = ({ commit, dispatch }, err) => {
flash(__('There was an error loading latest pipeline')); if (err.status !== httpStatus.NOT_FOUND) {
dispatch(
'setErrorMessage',
{
text: __('An error occured whilst fetching the latest pipline.'),
action: () =>
dispatch('forcePipelineRequest').then(() =>
dispatch('setErrorMessage', null, { root: true }),
),
actionText: __('Please try again'),
actionPayload: null,
},
{ root: true },
);
}
commit(types.RECEIVE_LASTEST_PIPELINE_ERROR); commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
dispatch('stopPipelinePolling'); dispatch('stopPipelinePolling');
}; };
...@@ -46,7 +63,7 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => { ...@@ -46,7 +63,7 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
method: 'lastCommitPipelines', method: 'lastCommitPipelines',
data: { getters: rootGetters }, data: { getters: rootGetters },
successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data), successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data),
errorCallback: () => dispatch('receiveLatestPipelineError'), errorCallback: err => dispatch('receiveLatestPipelineError', err),
}); });
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
...@@ -63,9 +80,21 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => { ...@@ -63,9 +80,21 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
}; };
export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id); export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id);
export const receiveJobsError = ({ commit }, id) => { export const receiveJobsError = ({ commit, dispatch }, stage) => {
flash(__('There was an error loading jobs')); dispatch(
commit(types.RECEIVE_JOBS_ERROR, id); 'setErrorMessage',
{
text: __('An error occured whilst loading the pipelines jobs.'),
action: payload =>
dispatch('fetchJobs', payload).then(() =>
dispatch('setErrorMessage', null, { root: true }),
),
actionText: __('Please try again'),
actionPayload: stage,
},
{ root: true },
);
commit(types.RECEIVE_JOBS_ERROR, stage.id);
}; };
export const receiveJobsSuccess = ({ commit }, { id, data }) => export const receiveJobsSuccess = ({ commit }, { id, data }) =>
commit(types.RECEIVE_JOBS_SUCCESS, { id, data }); commit(types.RECEIVE_JOBS_SUCCESS, { id, data });
...@@ -76,7 +105,7 @@ export const fetchJobs = ({ dispatch }, stage) => { ...@@ -76,7 +105,7 @@ export const fetchJobs = ({ dispatch }, stage) => {
axios axios
.get(stage.dropdownPath) .get(stage.dropdownPath)
.then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data })) .then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
.catch(() => dispatch('receiveJobsError', stage.id)); .catch(() => dispatch('receiveJobsError', stage));
}; };
export const toggleStageCollapsed = ({ commit }, stageId) => export const toggleStageCollapsed = ({ commit }, stageId) =>
...@@ -90,8 +119,18 @@ export const setDetailJob = ({ commit, dispatch }, job) => { ...@@ -90,8 +119,18 @@ export const setDetailJob = ({ commit, dispatch }, job) => {
}; };
export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE); export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE);
export const receiveJobTraceError = ({ commit }) => { export const receiveJobTraceError = ({ commit, dispatch }) => {
flash(__('Error fetching job trace')); dispatch(
'setErrorMessage',
{
text: __('An error occured whilst fetching the job trace.'),
action: () =>
dispatch('fetchJobTrace').then(() => dispatch('setErrorMessage', null, { root: true })),
actionText: __('Please try again'),
actionPayload: null,
},
{ root: true },
);
commit(types.RECEIVE_JOB_TRACE_ERROR); commit(types.RECEIVE_JOB_TRACE_ERROR);
}; };
export const receiveJobTraceSuccess = ({ commit }, data) => export const receiveJobTraceSuccess = ({ commit }, data) =>
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import { __ } from '~/locale'; import { __ } from '~/locale';
import '~/gl_dropdown';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store'; import ModalStore from './boards/stores/modal_store';
...@@ -251,3 +252,5 @@ export default class MilestoneSelect { ...@@ -251,3 +252,5 @@ export default class MilestoneSelect {
}); });
} }
} }
window.MilestoneSelect = MilestoneSelect;
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash'; import Flash from '../../flash';
import MonitoringService from '../services/monitoring_service'; import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
...@@ -13,6 +15,7 @@ export default { ...@@ -13,6 +15,7 @@ export default {
Graph, Graph,
GraphGroup, GraphGroup,
EmptyState, EmptyState,
Icon,
}, },
props: { props: {
hasMetrics: { hasMetrics: {
...@@ -80,6 +83,14 @@ export default { ...@@ -80,6 +83,14 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
environmentsEndpoint: {
type: String,
required: true,
},
currentEnvironmentName: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -96,6 +107,7 @@ export default { ...@@ -96,6 +107,7 @@ export default {
this.service = new MonitoringService({ this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint, metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint, deploymentEndpoint: this.deploymentEndpoint,
environmentsEndpoint: this.environmentsEndpoint,
}); });
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged); eventHub.$on('hoverChanged', this.hoverChanged);
...@@ -122,7 +134,11 @@ export default { ...@@ -122,7 +134,11 @@ export default {
this.service this.service
.getDeploymentData() .getDeploymentData()
.then(data => this.store.storeDeploymentData(data)) .then(data => this.store.storeDeploymentData(data))
.catch(() => new Flash('Error getting deployment information.')), .catch(() => Flash(s__('Metrics|There was an error getting deployment information.'))),
this.service
.getEnvironmentsData()
.then((data) => this.store.storeEnvironmentsData(data))
.catch(() => Flash(s__('Metrics|There was an error getting environments information.'))),
]) ])
.then(() => { .then(() => {
if (this.store.groups.length < 1) { if (this.store.groups.length < 1) {
...@@ -155,8 +171,41 @@ export default { ...@@ -155,8 +171,41 @@ export default {
<template> <template>
<div <div
v-if="!showEmptyState" v-if="!showEmptyState"
class="prometheus-graphs" class="prometheus-graphs prepend-top-10"
> >
<div class="environments d-flex align-items-center">
{{ s__('Metrics|Environment') }}
<div class="dropdown prepend-left-10">
<button
class="dropdown-menu-toggle"
data-toggle="dropdown"
type="button"
>
<span>
{{ currentEnvironmentName }}
</span>
<icon
name="chevron-down"
/>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
<li
v-for="environment in store.environmentsData"
:key="environment.latest.id"
>
<a
:href="environment.latest.metrics_path"
:class="{ 'is-active': environment.latest.name == currentEnvironmentName }"
class="dropdown-item"
>
{{ environment.latest.name }}
</a>
</li>
</ul>
</div>
</div>
</div>
<graph-group <graph-group
v-for="(groupData, index) in store.groups" v-for="(groupData, index) in store.groups"
:key="index" :key="index"
......
import axios from '../../lib/utils/axios_utils'; import axios from '../../lib/utils/axios_utils';
import statusCodes from '../../lib/utils/http_status'; import statusCodes from '../../lib/utils/http_status';
import { backOff } from '../../lib/utils/common_utils'; import { backOff } from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
const MAX_REQUESTS = 3; const MAX_REQUESTS = 3;
...@@ -23,9 +24,10 @@ function backOffRequest(makeRequestCallback) { ...@@ -23,9 +24,10 @@ function backOffRequest(makeRequestCallback) {
} }
export default class MonitoringService { export default class MonitoringService {
constructor({ metricsEndpoint, deploymentEndpoint }) { constructor({ metricsEndpoint, deploymentEndpoint, environmentsEndpoint }) {
this.metricsEndpoint = metricsEndpoint; this.metricsEndpoint = metricsEndpoint;
this.deploymentEndpoint = deploymentEndpoint; this.deploymentEndpoint = deploymentEndpoint;
this.environmentsEndpoint = environmentsEndpoint;
} }
getGraphsData() { getGraphsData() {
...@@ -33,7 +35,7 @@ export default class MonitoringService { ...@@ -33,7 +35,7 @@ export default class MonitoringService {
.then(resp => resp.data) .then(resp => resp.data)
.then((response) => { .then((response) => {
if (!response || !response.data) { if (!response || !response.data) {
throw new Error('Unexpected metrics data response from prometheus endpoint'); throw new Error(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
} }
return response.data; return response.data;
}); });
...@@ -47,9 +49,20 @@ export default class MonitoringService { ...@@ -47,9 +49,20 @@ export default class MonitoringService {
.then(resp => resp.data) .then(resp => resp.data)
.then((response) => { .then((response) => {
if (!response || !response.deployments) { if (!response || !response.deployments) {
throw new Error('Unexpected deployment data response from prometheus endpoint'); throw new Error(s__('Metrics|Unexpected deployment data response from prometheus endpoint'));
} }
return response.deployments; return response.deployments;
}); });
} }
getEnvironmentsData() {
return axios.get(this.environmentsEndpoint)
.then(resp => resp.data)
.then((response) => {
if (!response || !response.environments) {
throw new Error(s__('Metrics|There was an error fetching the environments data, please try again'));
}
return response.environments;
});
}
} }
...@@ -24,6 +24,7 @@ export default class MonitoringStore { ...@@ -24,6 +24,7 @@ export default class MonitoringStore {
constructor() { constructor() {
this.groups = []; this.groups = [];
this.deploymentData = []; this.deploymentData = [];
this.environmentsData = [];
} }
storeMetrics(groups = []) { storeMetrics(groups = []) {
...@@ -37,6 +38,10 @@ export default class MonitoringStore { ...@@ -37,6 +38,10 @@ export default class MonitoringStore {
this.deploymentData = deploymentData; this.deploymentData = deploymentData;
} }
storeEnvironmentsData(environmentsData = []) {
this.environmentsData = environmentsData;
}
getMetricsCount() { getMetricsCount() {
return this.groups.reduce((count, group) => count + group.metrics.length, 0); return this.groups.reduce((count, group) => count + group.metrics.length, 0);
} }
......
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import imageDiffHelper from '~/image_diff/helpers/index'; import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
export default { export default {
components: { components: {
DiffFileHeader, DiffFileHeader,
SkeletonLoadingContainer, SkeletonLoadingContainer,
},
props: {
discussion: {
type: Object,
required: true,
}, },
}, props: {
data() { discussion: {
return { type: Object,
error: false, required: true,
}; },
},
computed: {
...mapState({
noteableData: state => state.notes.noteableData,
}),
hasTruncatedDiffLines() {
return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
}, },
isDiscussionsExpanded() { data() {
return true; // TODO: @fatihacet - Fix this. return {
error: false,
};
}, },
isCollapsed() { computed: {
return this.diffFile.collapsed || false; ...mapState({
}, noteableData: state => state.notes.noteableData,
isImageDiff() { }),
return !this.diffFile.text; hasTruncatedDiffLines() {
}, return this.discussion.truncatedDiffLines &&
diffFileClass() { this.discussion.truncatedDiffLines.length !== 0;
const { text } = this.diffFile; },
return text ? 'text-file' : 'js-image-file'; isDiscussionsExpanded() {
}, return true; // TODO: @fatihacet - Fix this.
diffFile() { },
return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true }); isCollapsed() {
}, return this.diffFile.collapsed || false;
imageDiffHtml() { },
return this.discussion.imageDiffHtml; isImageDiff() {
}, return !this.diffFile.text;
currentUser() { },
return this.noteableData.current_user; diffFileClass() {
}, const { text } = this.diffFile;
userColorScheme() { return text ? 'text-file' : 'js-image-file';
return window.gon.user_color_scheme; },
}, diffFile() {
normalizedDiffLines() { return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true });
const lines = this.discussion.truncatedDiffLines || []; },
imageDiffHtml() {
return this.discussion.imageDiffHtml;
},
currentUser() {
return this.noteableData.current_user;
},
userColorScheme() {
return window.gon.user_color_scheme;
},
normalizedDiffLines() {
if (this.discussion.truncatedDiffLines) {
return this.discussion.truncatedDiffLines.map(line =>
trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line)),
);
}
return lines.map(line => trimFirstCharOfLineContent(convertObjectPropsToCamelCase(line))); return [];
},
}, },
}, mounted() {
mounted() { if (this.isImageDiff) {
if (this.isImageDiff) { const canCreateNote = false;
const canCreateNote = false; const renderCommentBadge = true;
const renderCommentBadge = true; imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge); } else if (!this.hasTruncatedDiffLines) {
} else if (!this.hasTruncatedDiffLines) { this.fetchDiff();
this.fetchDiff(); }
}
},
methods: {
...mapActions(['fetchDiscussionDiffLines']),
rowTag(html) {
return html.outerHTML ? 'tr' : 'template';
}, },
fetchDiff() { methods: {
this.error = false; ...mapActions(['fetchDiscussionDiffLines']),
this.fetchDiscussionDiffLines(this.discussion) rowTag(html) {
.then(this.highlight) return html.outerHTML ? 'tr' : 'template';
.catch(() => { },
this.error = true; fetchDiff() {
}); this.error = false;
this.fetchDiscussionDiffLines(this.discussion)
.then(this.highlight)
.catch(() => {
this.error = true;
});
},
}, },
}, };
};
</script> </script>
<template> <template>
......
...@@ -85,9 +85,9 @@ export const allDiscussions = (state, getters) => { ...@@ -85,9 +85,9 @@ export const allDiscussions = (state, getters) => {
export const resolvedDiscussionsById = state => { export const resolvedDiscussionsById = state => {
const map = {}; const map = {};
state.discussions.forEach(n => { state.discussions.filter(d => d.resolvable).forEach(n => {
if (n.notes) { if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system); const resolved = n.notes.filter(note => note.resolvable).every(note => note.resolved);
if (resolved) { if (resolved) {
map[n.id] = n; map[n.id] = n;
......
...@@ -39,6 +39,7 @@ export default class Todos { ...@@ -39,6 +39,7 @@ export default class Todos {
} }
initFilters() { initFilters() {
this.initFilterDropdown($('.js-group-search'), 'group_id', ['text']);
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type'); this.initFilterDropdown($('.js-type-search'), 'type');
this.initFilterDropdown($('.js-action-search'), 'action_id'); this.initFilterDropdown($('.js-action-search'), 'action_id');
...@@ -53,7 +54,16 @@ export default class Todos { ...@@ -53,7 +54,16 @@ export default class Todos {
filterable: searchFields ? true : false, filterable: searchFields ? true : false,
search: { fields: searchFields }, search: { fields: searchFields },
data: $dropdown.data('data'), data: $dropdown.data('data'),
clicked: () => $dropdown.closest('form.filter-form').submit(), clicked: () => {
const $formEl = $dropdown.closest('form.filter-form');
const mutexDropdowns = {
group_id: 'project_id',
project_id: 'group_id',
};
$formEl.find(`input[name="${mutexDropdowns[fieldName]}"]`).remove();
$formEl.submit();
},
}); });
} }
......
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
gcpSignupOffer();
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
gcpSignupOffer();
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
import Project from './project'; import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation'; import ShortcutsNavigation from '../../shortcuts_navigation';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const { page } = document.body.dataset;
const newClusterViews = [
'projects:clusters:new',
'projects:clusters:create_gcp',
'projects:clusters:create_user',
];
if (newClusterViews.indexOf(page) > -1) {
gcpSignupOffer();
initGkeDropdowns();
}
new Project(); // eslint-disable-line no-new new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
}); });
import initTerminal from '~/terminal/';
document.addEventListener('DOMContentLoaded', initTerminal);
...@@ -20,6 +20,7 @@ export default class ShortcutsNavigation extends Shortcuts { ...@@ -20,6 +20,7 @@ export default class ShortcutsNavigation extends Shortcuts {
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets')); Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes')); Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments')); Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics'));
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue')); Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
this.enabledHelp.push('.hidden-shortcut.project'); this.enabledHelp.push('.hidden-shortcut.project');
......
<script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
const MARK_TEXT = __('Mark todo as done');
const TODO_TEXT = __('Add todo');
export default {
directives: {
tooltip,
},
components: {
Icon,
LoadingIcon,
},
props: {
issuableId: {
type: Number,
required: true,
},
issuableType: {
type: String,
required: true,
},
isTodo: {
type: Boolean,
required: false,
default: true,
},
isActionActive: {
type: Boolean,
required: false,
default: false,
},
collapsed: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
buttonClasses() {
return this.collapsed ?
'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' :
'btn btn-default btn-todo issuable-header-btn float-right';
},
buttonLabel() {
return this.isTodo ? MARK_TEXT : TODO_TEXT;
},
collapsedButtonIconClasses() {
return this.isTodo ? 'todo-undone' : '';
},
collapsedButtonIcon() {
return this.isTodo ? 'todo-done' : 'todo-add';
},
},
methods: {
handleButtonClick() {
this.$emit('toggleTodo');
},
},
};
</script>
<template>
<button
v-tooltip
:class="buttonClasses"
:title="buttonLabel"
:aria-label="buttonLabel"
:data-issuable-id="issuableId"
:data-issuable-type="issuableType"
type="button"
data-container="body"
data-placement="left"
data-boundary="viewport"
@click="handleButtonClick"
>
<icon
v-show="collapsed"
:css-classes="collapsedButtonIconClasses"
:name="collapsedButtonIcon"
/>
<span
v-show="!collapsed"
class="issuable-todo-inner"
>
{{ buttonLabel }}
</span>
<loading-icon
v-show="isActionActive"
:inline="true"
/>
</button>
</template>
...@@ -29,8 +29,8 @@ ...@@ -29,8 +29,8 @@
methods: { methods: {
isValid(form) { isValid(form) {
return !form || return !form ||
form.find('.js-vue-markdown-field').length || form.find('.js-vue-markdown-field').length &&
$(this.$el).closest('form') === form[0]; $(this.$el).closest('form')[0] === form[0];
}, },
previewMarkdownTab(event, form) { previewMarkdownTab(event, form) {
......
...@@ -12,6 +12,11 @@ export default { ...@@ -12,6 +12,11 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
cssClasses: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
tooltipLabel() { tooltipLabel() {
...@@ -30,10 +35,12 @@ export default { ...@@ -30,10 +35,12 @@ export default {
<button <button
v-tooltip v-tooltip
:title="tooltipLabel" :title="tooltipLabel"
:class="cssClasses"
type="button" type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action" class="btn btn-blank gutter-toggle btn-sidebar-action"
data-container="body" data-container="body"
data-placement="left" data-placement="left"
data-boundary="viewport"
@click="toggle" @click="toggle"
> >
<i <i
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
} }
svg { svg {
vertical-align: text-bottom; vertical-align: middle;
} }
} }
......
...@@ -222,6 +222,23 @@ ...@@ -222,6 +222,23 @@
} }
} }
.prometheus-graphs {
.environments {
.dropdown-menu-toggle {
svg {
position: absolute;
right: 5%;
top: 25%;
}
}
.dropdown-menu-toggle,
.dropdown-menu {
width: 240px;
}
}
}
.environments-actions { .environments-actions {
.external-url, .external-url,
.monitoring-url, .monitoring-url,
......
...@@ -449,6 +449,7 @@ ...@@ -449,6 +449,7 @@
.todo-undone { .todo-undone {
color: $gl-link-color; color: $gl-link-color;
fill: $gl-link-color;
} }
.author { .author {
......
...@@ -191,6 +191,22 @@ ...@@ -191,6 +191,22 @@
} }
} }
.initialize-with-readme-setting {
.form-check {
margin-bottom: 10px;
.option-title {
font-weight: $gl-font-weight-normal;
display: inline-block;
color: $gl-text-color;
}
.option-description {
color: $project-option-descr-color;
}
}
}
.prometheus-metrics-monitoring { .prometheus-metrics-monitoring {
.card { .card {
.card-toggle { .card-toggle {
......
...@@ -174,6 +174,18 @@ ...@@ -174,6 +174,18 @@
} }
} }
@include media-breakpoint-down(lg) {
.todos-filters {
.filter-categories {
width: 75%;
.filter-item {
margin-bottom: 10px;
}
}
}
}
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
.todo { .todo {
.avatar { .avatar {
...@@ -199,6 +211,10 @@ ...@@ -199,6 +211,10 @@
} }
.todos-filters { .todos-filters {
.filter-categories {
width: auto;
}
.dropdown-menu-toggle { .dropdown-menu-toggle {
width: 100%; width: 100%;
} }
......
module TodosActions
extend ActiveSupport::Concern
def create
todo = TodoService.new.mark_todo(issuable, current_user)
render json: {
count: TodosFinder.new(current_user, state: :pending).execute.count,
delete_path: dashboard_todo_path(todo)
}
end
end
...@@ -45,6 +45,16 @@ module UploadsActions ...@@ -45,6 +45,16 @@ module UploadsActions
send_upload(uploader, attachment: uploader.filename, disposition: disposition) send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end end
def authorize
set_workhorse_internal_api_content_type
authorized = uploader_class.workhorse_authorize(
has_length: false,
maximum_size: Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i)
render json: authorized
end
private private
# Explicitly set the format. # Explicitly set the format.
......
...@@ -70,7 +70,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -70,7 +70,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end end
def todo_params def todo_params
params.permit(:action_id, :author_id, :project_id, :type, :sort, :state) params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
end end
def redirect_out_of_range(todos) def redirect_out_of_range(todos)
......
class Groups::UploadsController < Groups::ApplicationController class Groups::UploadsController < Groups::ApplicationController
include UploadsActions include UploadsActions
include WorkhorseRequest
skip_before_action :group, if: -> { action_name == 'show' && image_or_video? } skip_before_action :group, if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create] before_action :authorize_upload_file!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize]
private private
......
class Projects::Clusters::GcpController < Projects::ApplicationController
before_action :authorize_read_cluster!
before_action :authorize_create_cluster!, only: [:new, :create]
before_action :authorize_google_api, except: :login
helper_method :token_in_session
def login
begin
state = generate_session_key_redirect(gcp_new_namespace_project_clusters_path.to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
state: state).authorize_url
rescue GoogleApi::Auth::ConfigMissingError
# no-op
end
end
def new
@cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end
end
def create
@cluster = ::Clusters::CreateService
.new(project, current_user, create_params)
.execute(token_in_session)
if @cluster.persisted?
redirect_to project_cluster_path(project, @cluster)
else
render :new
end
end
private
def create_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type
]).merge(
provider_type: :gcp,
platform_type: :kubernetes
)
end
def authorize_google_api
unless GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
redirect_to action: 'login'
end
end
def token_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def expires_at_in_session
@expires_at_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
def generate_session_key_redirect(uri)
GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
session[key] = uri
end
end
end
class Projects::Clusters::UserController < Projects::ApplicationController
before_action :authorize_read_cluster!
before_action :authorize_create_cluster!, only: [:new, :create]
def new
@cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes
end
end
def create
@cluster = ::Clusters::CreateService
.new(project, current_user, create_params)
.execute
if @cluster.persisted?
redirect_to project_cluster_path(project, @cluster)
else
render :new
end
end
private
def create_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
platform_kubernetes_attributes: [
:namespace,
:api_url,
:token,
:ca_cert
]).merge(
provider_type: :user,
platform_type: :kubernetes
)
end
end
class Projects::ClustersController < Projects::ApplicationController class Projects::ClustersController < Projects::ApplicationController
before_action :cluster, except: [:index, :new] before_action :cluster, except: [:index, :new, :create_gcp, :create_user]
before_action :authorize_read_cluster! before_action :authorize_read_cluster!
before_action :generate_gcp_authorize_url, only: [:new]
before_action :validate_gcp_token, only: [:new]
before_action :gcp_cluster, only: [:new]
before_action :user_cluster, only: [:new]
before_action :authorize_create_cluster!, only: [:new] before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update] before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy] before_action :authorize_admin_cluster!, only: [:destroy]
before_action :update_applications_status, only: [:status] before_action :update_applications_status, only: [:status]
helper_method :token_in_session
STATUS_POLLING_INTERVAL = 10_000 STATUS_POLLING_INTERVAL = 10_000
...@@ -64,6 +69,38 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -64,6 +69,38 @@ class Projects::ClustersController < Projects::ApplicationController
end end
end end
def create_gcp
@gcp_cluster = ::Clusters::CreateService
.new(project, current_user, create_gcp_cluster_params)
.execute(token_in_session)
if @gcp_cluster.persisted?
redirect_to project_cluster_path(project, @gcp_cluster)
else
generate_gcp_authorize_url
validate_gcp_token
user_cluster
render :new, locals: { active_tab: 'gcp' }
end
end
def create_user
@user_cluster = ::Clusters::CreateService
.new(project, current_user, create_user_cluster_params)
.execute(token_in_session)
if @user_cluster.persisted?
redirect_to project_cluster_path(project, @user_cluster)
else
generate_gcp_authorize_url
validate_gcp_token
gcp_cluster
render :new, locals: { active_tab: 'user' }
end
end
private private
def cluster def cluster
...@@ -95,6 +132,80 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -95,6 +132,80 @@ class Projects::ClustersController < Projects::ApplicationController
end end
end end
def create_gcp_cluster_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type
]).merge(
provider_type: :gcp,
platform_type: :kubernetes
)
end
def create_user_cluster_params
params.require(:cluster).permit(
:enabled,
:name,
:environment_scope,
platform_kubernetes_attributes: [
:namespace,
:api_url,
:token,
:ca_cert
]).merge(
provider_type: :user,
platform_type: :kubernetes
)
end
def generate_gcp_authorize_url
state = generate_session_key_redirect(new_project_cluster_path(@project).to_s)
@authorize_url = GoogleApi::CloudPlatform::Client.new(
nil, callback_google_api_auth_url,
state: state).authorize_url
rescue GoogleApi::Auth::ConfigMissingError
# no-op
end
def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end
end
def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes
end
end
def validate_gcp_token
@valid_gcp_token = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
.validate_token(expires_at_in_session)
end
def token_in_session
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def expires_at_in_session
@expires_at_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
def generate_session_key_redirect(uri)
GoogleApi::CloudPlatform::Client.new_session_key_for_redirect_uri do |key|
session[key] = uri
end
end
def authorize_update_cluster! def authorize_update_cluster!
access_denied! unless can?(current_user, :update_cluster, cluster) access_denied! unless can?(current_user, :update_cluster, cluster)
end end
......
...@@ -120,6 +120,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -120,6 +120,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
end end
def metrics_redirect
environment = project.default_environment
if environment
redirect_to environment_metrics_path(environment)
else
render :empty
end
end
def metrics def metrics
# Currently, this acts as a hint to load the metrics details into the cache # Currently, this acts as a hint to load the metrics details into the cache
# if they aren't there already # if they aren't there already
......
...@@ -2,11 +2,12 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -2,11 +2,12 @@ class Projects::JobsController < Projects::ApplicationController
include SendFileUpload include SendFileUpload
before_action :build, except: [:index, :cancel_all] before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, before_action :authorize_read_build!
only: [:index, :show, :status, :raw, :trace]
before_action :authorize_update_build!, before_action :authorize_update_build!,
except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase] except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase]
before_action :authorize_erase_build!, only: [:erase] before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_workhorse_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
layout 'project' layout 'project'
...@@ -44,12 +45,10 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -44,12 +45,10 @@ class Projects::JobsController < Projects::ApplicationController
end end
def show def show
@builds = @project.pipelines @pipeline = @build.pipeline
.find_by_sha(@build.sha) @builds = @pipeline.builds
.builds
.order('id DESC') .order('id DESC')
.present(current_user: current_user) .present(current_user: current_user)
@pipeline = @build.pipeline
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -136,6 +135,15 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -136,6 +135,15 @@ class Projects::JobsController < Projects::ApplicationController
end end
end end
def terminal
end
# GET .../terminal.ws : implemented in gitlab-workhorse
def terminal_websocket_authorize
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(@build.terminal_specification)
end
private private
def authorize_update_build! def authorize_update_build!
...@@ -146,6 +154,14 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -146,6 +154,14 @@ class Projects::JobsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :erase_build, build) return access_denied! unless can?(current_user, :erase_build, build)
end end
def authorize_use_build_terminal!
return access_denied! unless can?(current_user, :create_build_terminal, build)
end
def verify_api_request!
Gitlab::Workhorse.verify_api_request!(request.headers)
end
def raw_send_params def raw_send_params
{ type: 'text/plain; charset=utf-8', disposition: 'inline' } { type: 'text/plain; charset=utf-8', disposition: 'inline' }
end end
......
...@@ -13,7 +13,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -13,7 +13,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@pipelines = PipelinesFinder @pipelines = PipelinesFinder
.new(project, scope: @scope) .new(project, current_user, scope: @scope)
.execute .execute
.page(params[:page]) .page(params[:page])
.per(30) .per(30)
...@@ -178,7 +178,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -178,7 +178,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def limited_pipelines_count(project, scope = nil) def limited_pipelines_count(project, scope = nil)
finder = PipelinesFinder.new(project, scope: scope) finder = PipelinesFinder.new(project, current_user, scope: scope)
view_context.limited_counter_with_delimiter(finder.execute) view_context.limited_counter_with_delimiter(finder.execute)
end end
......
...@@ -74,7 +74,7 @@ module Projects ...@@ -74,7 +74,7 @@ module Projects
.ordered .ordered
.page(params[:page]).per(20) .page(params[:page]).per(20)
@shared_runners = ::Ci::Runner.shared.active @shared_runners = ::Ci::Runner.instance_type.active
@shared_runners_count = @shared_runners.count(:all) @shared_runners_count = @shared_runners.count(:all)
......
class Projects::TodosController < Projects::ApplicationController class Projects::TodosController < Projects::ApplicationController
before_action :authenticate_user!, only: [:create] include Gitlab::Utils::StrongMemoize
include TodosActions
def create
todo = TodoService.new.mark_todo(issuable, current_user)
render json: { before_action :authenticate_user!, only: [:create]
count: TodosFinder.new(current_user, state: :pending).execute.count,
delete_path: dashboard_todo_path(todo)
}
end
private private
def issuable def issuable
@issuable ||= begin strong_memoize(:issuable) do
case params[:issuable_type] case params[:issuable_type]
when "issue" when "issue"
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
......
class Projects::UploadsController < Projects::ApplicationController class Projects::UploadsController < Projects::ApplicationController
include UploadsActions include UploadsActions
include WorkhorseRequest
# These will kick you out if you don't have access. # These will kick you out if you don't have access.
skip_before_action :project, :repository, skip_before_action :project, :repository,
if: -> { action_name == 'show' && image_or_video? } if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create] before_action :authorize_upload_file!, only: [:create, :authorize]
before_action :verify_workhorse_api!, only: [:authorize]
private private
......
...@@ -347,6 +347,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -347,6 +347,7 @@ class ProjectsController < Projects::ApplicationController
:visibility_level, :visibility_level,
:template_name, :template_name,
:merge_method, :merge_method,
:initialize_with_readme,
project_feature_attributes: %i[ project_feature_attributes: %i[
builds_access_level builds_access_level
......
class PipelinesFinder class PipelinesFinder
attr_reader :project, :pipelines, :params attr_reader :project, :pipelines, :params, :current_user
ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze
def initialize(project, params = {}) def initialize(project, current_user, params = {})
@project = project @project = project
@current_user = current_user
@pipelines = project.pipelines @pipelines = project.pipelines
@params = params @params = params
end end
def execute def execute
unless Ability.allowed?(current_user, :read_pipeline, project)
return Ci::Pipeline.none
end
items = pipelines items = pipelines
items = by_scope(items) items = by_scope(items)
items = by_status(items) items = by_status(items)
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
class TodosFinder class TodosFinder
prepend FinderWithCrossProjectAccess prepend FinderWithCrossProjectAccess
include FinderMethods include FinderMethods
include Gitlab::Utils::StrongMemoize
requires_cross_project_access unless: -> { project? } requires_cross_project_access unless: -> { project? }
...@@ -34,9 +35,11 @@ class TodosFinder ...@@ -34,9 +35,11 @@ class TodosFinder
items = by_author(items) items = by_author(items)
items = by_state(items) items = by_state(items)
items = by_type(items) items = by_type(items)
items = by_group(items)
# Filtering by project HAS TO be the last because we use # Filtering by project HAS TO be the last because we use
# the project IDs yielded by the todos query thus far # the project IDs yielded by the todos query thus far
items = by_project(items) items = by_project(items)
items = visible_to_user(items)
sort(items) sort(items)
end end
...@@ -82,6 +85,10 @@ class TodosFinder ...@@ -82,6 +85,10 @@ class TodosFinder
params[:project_id].present? params[:project_id].present?
end end
def group?
params[:group_id].present?
end
def project def project
return @project if defined?(@project) return @project if defined?(@project)
...@@ -100,18 +107,14 @@ class TodosFinder ...@@ -100,18 +107,14 @@ class TodosFinder
@project @project
end end
def project_ids(items) def group
ids = items.except(:order).select(:project_id) strong_memoize(:group) do
if Gitlab::Database.mysql? Group.find(params[:group_id])
# To make UPDATE work on MySQL, wrap it in a SELECT with an alias
ids = Todo.except(:order).select('*').from("(#{ids.to_sql}) AS t")
end end
ids
end end
def type? def type?
type.present? && %w(Issue MergeRequest).include?(type) type.present? && %w(Issue MergeRequest Epic).include?(type)
end end
def type def type
...@@ -148,12 +151,37 @@ class TodosFinder ...@@ -148,12 +151,37 @@ class TodosFinder
def by_project(items) def by_project(items)
if project? if project?
items.where(project: project) items = items.where(project: project)
else end
projects = Project.public_or_visible_to_user(current_user)
items
end
items.joins(:project).merge(projects) def by_group(items)
if group?
groups = group.self_and_descendants
items = items.where(
'project_id IN (?) OR group_id IN (?)',
Project.where(group: groups).select(:id),
groups.select(:id)
)
end end
items
end
def visible_to_user(items)
projects = Project.public_or_visible_to_user(current_user)
groups = Group.public_or_visible_to_user(current_user)
items
.joins('LEFT JOIN namespaces ON namespaces.id = todos.group_id')
.joins('LEFT JOIN projects ON projects.id = todos.project_id')
.where(
'project_id IN (?) OR group_id IN (?)',
projects.select(:id),
groups.select(:id)
)
end end
def by_state(items) def by_state(items)
......
...@@ -2,7 +2,10 @@ class GitlabSchema < GraphQL::Schema ...@@ -2,7 +2,10 @@ class GitlabSchema < GraphQL::Schema
use BatchLoader::GraphQL use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present use Gitlab::Graphql::Present
use Gitlab::Graphql::Connections
query(Types::QueryType) query(Types::QueryType)
default_max_page_size 100
# mutation(Types::MutationType) # mutation(Types::MutationType)
end end
module ResolvesPipelines
extend ActiveSupport::Concern
included do
type [Types::Ci::PipelineType], null: false
argument :status,
Types::Ci::PipelineStatusEnum,
required: false,
description: "Filter pipelines by their status"
argument :ref,
GraphQL::STRING_TYPE,
required: false,
description: "Filter pipelines by the ref they are run for"
argument :sha,
GraphQL::STRING_TYPE,
required: false,
description: "Filter pipelines by the sha of the commit they are run for"
end
def resolve_pipelines(project, params = {})
PipelinesFinder.new(project, context[:current_user], params).execute
end
end
module Resolvers
class MergeRequestPipelinesResolver < BaseResolver
include ::ResolvesPipelines
alias_method :merge_request, :object
def resolve(**args)
resolve_pipelines(project, args)
.merge(merge_request.all_pipelines)
end
def project
merge_request.source_project
end
end
end
module Resolvers
class ProjectPipelinesResolver < BaseResolver
include ResolvesPipelines
alias_method :project, :object
def resolve(**args)
resolve_pipelines(project, args)
end
end
end
module Types
module Ci
class PipelineStatusEnum < BaseEnum
::Ci::Pipeline.all_state_names.each do |state_symbol|
value state_symbol.to_s.upcase, value: state_symbol.to_s
end
end
end
end
module Types
module Ci
class PipelineType < BaseObject
expose_permissions Types::PermissionTypes::Ci::Pipeline
graphql_name 'Pipeline'
field :id, GraphQL::ID_TYPE, null: false
field :iid, GraphQL::ID_TYPE, null: false
field :sha, GraphQL::STRING_TYPE, null: false
field :before_sha, GraphQL::STRING_TYPE, null: true
field :status, PipelineStatusEnum, null: false
field :duration,
GraphQL::INT_TYPE,
null: true,
description: "Duration of the pipeline in seconds"
field :coverage,
GraphQL::FLOAT_TYPE,
null: true,
description: "Coverage percentage"
field :created_at, Types::TimeType, null: false
field :updated_at, Types::TimeType, null: false
field :started_at, Types::TimeType, null: true
field :finished_at, Types::TimeType, null: true
field :committed_at, Types::TimeType, null: true
# TODO: Add triggering user as a type
end
end
end
...@@ -45,5 +45,11 @@ module Types ...@@ -45,5 +45,11 @@ module Types
field :upvotes, GraphQL::INT_TYPE, null: false field :upvotes, GraphQL::INT_TYPE, null: false
field :downvotes, GraphQL::INT_TYPE, null: false field :downvotes, GraphQL::INT_TYPE, null: false
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false
field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline do
authorize :read_pipeline
end
field :pipelines, Types::Ci::PipelineType.connection_type,
resolver: Resolvers::MergeRequestPipelinesResolver
end end
end end
module Types
module PermissionTypes
module Ci
class Pipeline < BasePermissionType
graphql_name 'PipelinePermissions'
abilities :update_pipeline, :admin_pipeline, :destroy_pipeline
end
end
end
end
...@@ -70,5 +70,10 @@ module Types ...@@ -70,5 +70,10 @@ module Types
resolver: Resolvers::MergeRequestResolver do resolver: Resolvers::MergeRequestResolver do
authorize :read_merge_request authorize :read_merge_request
end end
field :pipelines,
Types::Ci::PipelineType.connection_type,
null: false,
resolver: Resolvers::ProjectPipelinesResolver
end end
end end
...@@ -122,7 +122,7 @@ module CiStatusHelper ...@@ -122,7 +122,7 @@ module CiStatusHelper
def no_runners_for_project?(project) def no_runners_for_project?(project)
project.runners.blank? && project.runners.blank? &&
Ci::Runner.shared.blank? Ci::Runner.instance_type.blank?
end end
def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body') def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body')
......
...@@ -131,6 +131,19 @@ module IssuablesHelper ...@@ -131,6 +131,19 @@ module IssuablesHelper
end end
end end
def group_dropdown_label(group_id, default_label)
return default_label if group_id.nil?
return "Any group" if group_id == "0"
group = ::Group.find_by(id: group_id)
if group
group.full_name
else
default_label
end
end
def milestone_dropdown_label(milestone_title, default_label = "Milestone") def milestone_dropdown_label(milestone_title, default_label = "Milestone")
title = title =
case milestone_title case milestone_title
......
...@@ -43,7 +43,7 @@ module TodosHelper ...@@ -43,7 +43,7 @@ module TodosHelper
project_commit_path(todo.project, project_commit_path(todo.project,
todo.target, anchor: anchor) todo.target, anchor: anchor)
else else
path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] path = [todo.parent, todo.target]
path.unshift(:pipelines) if todo.build_failed? path.unshift(:pipelines) if todo.build_failed?
...@@ -167,4 +167,12 @@ module TodosHelper ...@@ -167,4 +167,12 @@ module TodosHelper
def show_todo_state?(todo) def show_todo_state?(todo)
(todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state) (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
end end
def todo_group_options
groups = current_user.authorized_groups.map do |group|
{ id: group.id, text: group.full_name }
end
groups.unshift({ id: '', text: 'Any Group' }).to_json
end
end end
...@@ -26,4 +26,8 @@ class Board < ActiveRecord::Base ...@@ -26,4 +26,8 @@ class Board < ActiveRecord::Base
def closed_list def closed_list
lists.merge(List.closed).take lists.merge(List.closed).take
end end
def scoped?
false
end
end end
...@@ -27,7 +27,13 @@ module Ci ...@@ -27,7 +27,13 @@ module Ci
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata' has_one :metadata, class_name: 'Ci::BuildMetadata'
has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
accepts_nested_attributes_for :runner_session
delegate :timeout, to: :metadata, prefix: true, allow_nil: true delegate :timeout, to: :metadata, prefix: true, allow_nil: true
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project delegate :gitlab_deploy_token, to: :project
## ##
...@@ -174,6 +180,10 @@ module Ci ...@@ -174,6 +180,10 @@ module Ci
after_transition pending: :running do |build| after_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state build.ensure_metadata.update_timeout_state
end end
after_transition running: any do |build|
Ci::BuildRunnerSession.where(build: build).delete_all
end
end end
def ensure_metadata def ensure_metadata
...@@ -584,6 +594,10 @@ module Ci ...@@ -584,6 +594,10 @@ module Ci
super(options).merge(when: read_attribute(:when)) super(options).merge(when: read_attribute(:when))
end end
def has_terminal?
running? && runner_session_url.present?
end
private private
def update_artifacts_size def update_artifacts_size
......
module Ci
# The purpose of this class is to store Build related runner session.
# Data will be removed after transitioning from running to any state.
class BuildRunnerSession < ActiveRecord::Base
extend Gitlab::Ci::Model
self.table_name = 'ci_builds_runner_session'
belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session
validates :build, presence: true
validates :url, url: { protocols: %w(https) }
def terminal_specification
return {} unless url.present?
{
subprotocols: ['terminal.gitlab.com'].freeze,
url: "#{url}/exec".sub("https://", "wss://"),
headers: { Authorization: authorization.presence }.compact,
ca_pem: certificate.presence
}
end
end
end
...@@ -2,6 +2,7 @@ module Ci ...@@ -2,6 +2,7 @@ module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
include IgnorableColumn
include RedisCacheable include RedisCacheable
include ChronicDurationAttribute include ChronicDurationAttribute
...@@ -11,6 +12,8 @@ module Ci ...@@ -11,6 +12,8 @@ module Ci
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
ignore_column :is_shared
has_many :builds has_many :builds
has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects has_many :projects, through: :runner_projects
...@@ -21,13 +24,16 @@ module Ci ...@@ -21,13 +24,16 @@ module Ci
before_validation :set_default_values before_validation :set_default_values
scope :specific, -> { where(is_shared: false) }
scope :shared, -> { where(is_shared: true) }
scope :active, -> { where(active: true) } scope :active, -> { where(active: true) }
scope :paused, -> { where(active: false) } scope :paused, -> { where(active: false) }
scope :online, -> { where('contacted_at > ?', contact_time_deadline) } scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
scope :ordered, -> { order(id: :desc) } scope :ordered, -> { order(id: :desc) }
# BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb`
scope :deprecated_shared, -> { instance_type }
# this should get replaced with `project_type.or(group_type)` once using Rails5
scope :deprecated_specific, -> { where(runner_type: [runner_types[:project_type], runner_types[:group_type]]) }
scope :belonging_to_project, -> (project_id) { scope :belonging_to_project, -> (project_id) {
joins(:runner_projects).where(ci_runner_projects: { project_id: project_id }) joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
} }
...@@ -39,9 +45,9 @@ module Ci ...@@ -39,9 +45,9 @@ module Ci
joins(:groups).where(namespaces: { id: hierarchy_groups }) joins(:groups).where(namespaces: { id: hierarchy_groups })
} }
scope :owned_or_shared, -> (project_id) do scope :owned_or_instance_wide, -> (project_id) do
union = Gitlab::SQL::Union.new( union = Gitlab::SQL::Union.new(
[belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), shared], [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), instance_type],
remove_duplicates: false remove_duplicates: false
) )
from("(#{union.to_sql}) ci_runners") from("(#{union.to_sql}) ci_runners")
...@@ -63,7 +69,6 @@ module Ci ...@@ -63,7 +69,6 @@ module Ci
validate :no_groups, unless: :group_type? validate :no_groups, unless: :group_type?
validate :any_project, if: :project_type? validate :any_project, if: :project_type?
validate :exactly_one_group, if: :group_type? validate :exactly_one_group, if: :group_type?
validate :validate_is_shared
acts_as_taggable acts_as_taggable
...@@ -113,8 +118,7 @@ module Ci ...@@ -113,8 +118,7 @@ module Ci
end end
def assign_to(project, current_user = nil) def assign_to(project, current_user = nil)
if shared? if instance_type?
self.is_shared = false if shared?
self.runner_type = :project_type self.runner_type = :project_type
elsif group_type? elsif group_type?
raise ArgumentError, 'Transitioning a group runner to a project runner is not supported' raise ArgumentError, 'Transitioning a group runner to a project runner is not supported'
...@@ -137,10 +141,6 @@ module Ci ...@@ -137,10 +141,6 @@ module Ci
description description
end end
def shared?
is_shared
end
def online? def online?
contacted_at && contacted_at > self.class.contact_time_deadline contacted_at && contacted_at > self.class.contact_time_deadline
end end
...@@ -159,10 +159,6 @@ module Ci ...@@ -159,10 +159,6 @@ module Ci
runner_projects.count == 1 runner_projects.count == 1
end end
def specific?
!shared?
end
def assigned_to_group? def assigned_to_group?
runner_namespaces.any? runner_namespaces.any?
end end
...@@ -260,7 +256,7 @@ module Ci ...@@ -260,7 +256,7 @@ module Ci
end end
def assignable_for?(project_id) def assignable_for?(project_id)
self.class.owned_or_shared(project_id).where(id: self.id).any? self.class.owned_or_instance_wide(project_id).where(id: self.id).any?
end end
def no_projects def no_projects
...@@ -287,12 +283,6 @@ module Ci ...@@ -287,12 +283,6 @@ module Ci
end end
end end
def validate_is_shared
unless is_shared? == instance_type?
errors.add(:is_shared, 'is not equal to instance_type?')
end
end
def accepting_tags?(build) def accepting_tags?(build)
(run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty? (run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty?
end end
......
...@@ -243,6 +243,12 @@ module Issuable ...@@ -243,6 +243,12 @@ module Issuable
opened? opened?
end end
def overdue?
return false unless respond_to?(:due_date)
due_date.try(:past?) || false
end
def user_notes_count def user_notes_count
if notes.loaded? if notes.loaded?
# Use the in-memory association to select and count to avoid hitting the db # Use the in-memory association to select and count to avoid hitting the db
......
...@@ -39,6 +39,8 @@ class Group < Namespace ...@@ -39,6 +39,8 @@ class Group < Namespace
has_many :boards has_many :boards
has_many :badges, class_name: 'GroupBadge' has_many :badges, class_name: 'GroupBadge'
has_many :todos
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects validate :visibility_level_allowed_by_projects
...@@ -82,6 +84,12 @@ class Group < Namespace ...@@ -82,6 +84,12 @@ class Group < Namespace
where(id: user.authorized_groups.select(:id).reorder(nil)) where(id: user.authorized_groups.select(:id).reorder(nil))
end end
def public_or_visible_to_user(user)
where('id IN (?) OR namespaces.visibility_level IN (?)',
user.authorized_groups.select(:id),
Gitlab::VisibilityLevel.levels_for_user(user))
end
def select_for_project_authorization def select_for_project_authorization
if current_scope.joins_values.include?(:shared_projects) if current_scope.joins_values.include?(:shared_projects)
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id') joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
......
...@@ -275,10 +275,6 @@ class Issue < ActiveRecord::Base ...@@ -275,10 +275,6 @@ class Issue < ActiveRecord::Base
user ? readable_by?(user) : publicly_visible? user ? readable_by?(user) : publicly_visible?
end end
def overdue?
due_date.try(:past?) || false
end
def check_for_spam? def check_for_spam?
project.public? && (title_changed? || description_changed?) project.public? && (title_changed? || description_changed?)
end end
......
...@@ -229,6 +229,10 @@ class Note < ActiveRecord::Base ...@@ -229,6 +229,10 @@ class Note < ActiveRecord::Base
!for_personal_snippet? !for_personal_snippet?
end end
def for_issuable?
for_issue? || for_merge_request?
end
def skip_project_check? def skip_project_check?
!for_project_noteable? !for_project_noteable?
end end
......
...@@ -1422,7 +1422,7 @@ class Project < ActiveRecord::Base ...@@ -1422,7 +1422,7 @@ class Project < ActiveRecord::Base
end end
def shared_runners def shared_runners
@shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none @shared_runners ||= shared_runners_available? ? Ci::Runner.instance_type : Ci::Runner.none
end end
def group_runners def group_runners
...@@ -1774,6 +1774,15 @@ class Project < ActiveRecord::Base ...@@ -1774,6 +1774,15 @@ class Project < ActiveRecord::Base
end end
end end
def default_environment
production_first = "(CASE WHEN name = 'production' THEN 0 ELSE 1 END), id ASC"
environments
.with_state(:available)
.reorder(production_first)
.first
end
def secret_variables_for(ref:, environment: nil) def secret_variables_for(ref:, environment: nil)
# EE would use the environment # EE would use the environment
if protected_for?(ref) if protected_for?(ref)
......
...@@ -240,7 +240,7 @@ class KubernetesService < DeploymentService ...@@ -240,7 +240,7 @@ class KubernetesService < DeploymentService
end end
def deprecation_validation def deprecation_validation
return if active_changed?(from: true, to: false) return if active_changed?(from: true, to: false) || (new_record? && !active?)
if deprecated? if deprecated?
errors[:base] << deprecation_message errors[:base] << deprecation_message
......
...@@ -281,9 +281,9 @@ class Service < ActiveRecord::Base ...@@ -281,9 +281,9 @@ class Service < ActiveRecord::Base
def self.build_from_template(project_id, template) def self.build_from_template(project_id, template)
service = template.dup service = template.dup
service.active = false unless service.valid?
service.template = false service.template = false
service.project_id = project_id service.project_id = project_id
service.active = false if service.active? && !service.valid?
service service
end end
......
...@@ -22,15 +22,18 @@ class Todo < ActiveRecord::Base ...@@ -22,15 +22,18 @@ class Todo < ActiveRecord::Base
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :note belongs_to :note
belongs_to :project belongs_to :project
belongs_to :group
belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :user belongs_to :user
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, to: :author, prefix: true, allow_nil: true
validates :action, :project, :target_type, :user, presence: true validates :action, :target_type, :user, presence: true
validates :author, presence: true validates :author, presence: true
validates :target_id, presence: true, unless: :for_commit? validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit? validates :commit_id, presence: true, if: :for_commit?
validates :project, presence: true, unless: :group_id
validates :group, presence: true, unless: :project_id
scope :pending, -> { with_state(:pending) } scope :pending, -> { with_state(:pending) }
scope :done, -> { with_state(:done) } scope :done, -> { with_state(:done) }
...@@ -44,7 +47,7 @@ class Todo < ActiveRecord::Base ...@@ -44,7 +47,7 @@ class Todo < ActiveRecord::Base
state :done state :done
end end
after_save :keep_around_commit after_save :keep_around_commit, if: :commit_id
class << self class << self
# Priority sorting isn't displayed in the dropdown, because we don't show # Priority sorting isn't displayed in the dropdown, because we don't show
...@@ -79,6 +82,10 @@ class Todo < ActiveRecord::Base ...@@ -79,6 +82,10 @@ class Todo < ActiveRecord::Base
end end
end end
def parent
project
end
def unmergeable? def unmergeable?
action == UNMERGEABLE action == UNMERGEABLE
end end
......
...@@ -1032,7 +1032,7 @@ class User < ActiveRecord::Base ...@@ -1032,7 +1032,7 @@ class User < ActiveRecord::Base
union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids]) union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids])
Ci::Runner.specific.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection Ci::Runner.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end end
end end
......
...@@ -18,6 +18,10 @@ module Ci ...@@ -18,6 +18,10 @@ module Ci
@subject.project.branch_allows_collaboration?(@user, @subject.ref) @subject.project.branch_allows_collaboration?(@user, @subject.ref)
end end
condition(:terminal, scope: :subject) do
@subject.has_terminal?
end
rule { protected_ref }.policy do rule { protected_ref }.policy do
prevent :update_build prevent :update_build
prevent :erase_build prevent :erase_build
...@@ -29,5 +33,7 @@ module Ci ...@@ -29,5 +33,7 @@ module Ci
enable :update_build enable :update_build
enable :update_commit_status enable :update_commit_status
end end
rule { can?(:update_build) & terminal }.enable :create_build_terminal
end end
end end
...@@ -25,6 +25,8 @@ class DiffFileEntity < Grape::Entity ...@@ -25,6 +25,8 @@ class DiffFileEntity < Grape::Entity
expose :can_modify_blob do |diff_file| expose :can_modify_blob do |diff_file|
merge_request = options[:merge_request] merge_request = options[:merge_request]
next unless diff_file.blob
if merge_request&.source_project && current_user if merge_request&.source_project && current_user
can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch) can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
else else
...@@ -108,6 +110,7 @@ class DiffFileEntity < Grape::Entity ...@@ -108,6 +110,7 @@ class DiffFileEntity < Grape::Entity
project = merge_request.target_project project = merge_request.target_project
next unless project next unless project
next unless diff_file.content_sha
project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path)) project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
end end
...@@ -125,6 +128,8 @@ class DiffFileEntity < Grape::Entity ...@@ -125,6 +128,8 @@ class DiffFileEntity < Grape::Entity
end end
expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file| expose :context_lines_path, if: -> (diff_file, _) { diff_file.text? } do |diff_file|
next unless diff_file.content_sha
project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path)) project_blob_diff_path(diff_file.repository.project, tree_join(diff_file.content_sha, diff_file.file_path))
end end
......
...@@ -3,7 +3,7 @@ class DiscussionEntity < Grape::Entity ...@@ -3,7 +3,7 @@ class DiscussionEntity < Grape::Entity
include NotesHelper include NotesHelper
expose :id, :reply_id expose :id, :reply_id
expose :position, if: -> (d, _) { d.diff_discussion? } expose :position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? }
expose :line_code, if: -> (d, _) { d.diff_discussion? } expose :line_code, if: -> (d, _) { d.diff_discussion? }
expose :expanded?, as: :expanded expose :expanded?, as: :expanded
expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? } expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? }
......
...@@ -4,7 +4,7 @@ class RunnerEntity < Grape::Entity ...@@ -4,7 +4,7 @@ class RunnerEntity < Grape::Entity
expose :id, :description expose :id, :description
expose :edit_path, expose :edit_path,
if: -> (*) { can?(request.current_user, :admin_build, project) && runner.specific? } do |runner| if: -> (*) { can?(request.current_user, :admin_build, project) && runner.project_type? } do |runner|
edit_project_runner_path(project, runner) edit_project_runner_path(project, runner)
end end
......
...@@ -13,9 +13,9 @@ module Ci ...@@ -13,9 +13,9 @@ module Ci
@runner = runner @runner = runner
end end
def execute def execute(params = {})
builds = builds =
if runner.shared? if runner.instance_type?
builds_for_shared_runner builds_for_shared_runner
elsif runner.group_type? elsif runner.group_type?
builds_for_group_runner builds_for_group_runner
...@@ -41,6 +41,8 @@ module Ci ...@@ -41,6 +41,8 @@ module Ci
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
begin begin
build.runner_id = runner.id build.runner_id = runner.id
build.runner_session_attributes = params[:session] if params[:session].present?
build.run! build.run!
register_success(build) register_success(build)
...@@ -99,7 +101,7 @@ module Ci ...@@ -99,7 +101,7 @@ module Ci
end end
def running_builds_for_shared_runners def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared) Ci::Build.running.where(runner: Ci::Runner.instance_type)
.group(:project_id).select(:project_id, 'count(*) AS running_builds') .group(:project_id).select(:project_id, 'count(*) AS running_builds')
end end
...@@ -115,7 +117,7 @@ module Ci ...@@ -115,7 +117,7 @@ module Ci
end end
def register_success(job) def register_success(job)
labels = { shared_runner: runner.shared?, labels = { shared_runner: runner.instance_type?,
jobs_running_for_project: jobs_running_for_project(job) } jobs_running_for_project: jobs_running_for_project(job) }
job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil? job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil?
...@@ -123,10 +125,10 @@ module Ci ...@@ -123,10 +125,10 @@ module Ci
end end
def jobs_running_for_project(job) def jobs_running_for_project(job)
return '+Inf' unless runner.shared? return '+Inf' unless runner.instance_type?
# excluding currently started job # excluding currently started job
running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared) running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.instance_type)
.limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1 .limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+" running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
end end
......
...@@ -32,8 +32,9 @@ module Issues ...@@ -32,8 +32,9 @@ module Issues
def filter_assignee(issuable) def filter_assignee(issuable)
return if params[:assignee_ids].blank? return if params[:assignee_ids].blank?
# The number of assignees is limited by one for GitLab CE unless issuable.allows_multiple_assignees?
params[:assignee_ids] = params[:assignee_ids][0, 1] params[:assignee_ids] = params[:assignee_ids].take(1)
end
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) } assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
......
...@@ -20,6 +20,7 @@ module Labels ...@@ -20,6 +20,7 @@ module Labels
@available_labels ||= LabelsFinder.new( @available_labels ||= LabelsFinder.new(
current_user, current_user,
"#{parent_type}_id".to_sym => parent.id, "#{parent_type}_id".to_sym => parent.id,
include_ancestor_groups: include_ancestor_groups?,
only_group_labels: parent_is_group? only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization) ).execute(skip_authorization: skip_authorization)
end end
...@@ -30,7 +31,8 @@ module Labels ...@@ -30,7 +31,8 @@ module Labels
new_label = available_labels.find_by(title: title) new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent)) if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent) create_params = params.except(:include_ancestor_groups)
new_label = Labels::CreateService.new(create_params).execute(parent_type.to_sym => parent)
end end
new_label new_label
...@@ -47,5 +49,9 @@ module Labels ...@@ -47,5 +49,9 @@ module Labels
def parent_is_group? def parent_is_group?
parent_type == "group" parent_type == "group"
end end
def include_ancestor_groups?
params[:include_ancestor_groups] == true
end
end end
end end
...@@ -2,6 +2,8 @@ module Projects ...@@ -2,6 +2,8 @@ module Projects
class CreateService < BaseService class CreateService < BaseService
def initialize(user, params) def initialize(user, params)
@current_user, @params = user, params.dup @current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki)
@initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme))
end end
def execute def execute
...@@ -11,7 +13,6 @@ module Projects ...@@ -11,7 +13,6 @@ module Projects
forked_from_project_id = params.delete(:forked_from_project_id) forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data) import_data = params.delete(:import_data)
@skip_wiki = params.delete(:skip_wiki)
@project = Project.new(params) @project = Project.new(params)
...@@ -102,6 +103,8 @@ module Projects ...@@ -102,6 +103,8 @@ module Projects
setup_authorizations setup_authorizations
current_user.invalidate_personal_projects_count current_user.invalidate_personal_projects_count
create_readme if @initialize_with_readme
end end
# Refresh the current user's authorizations inline (so they can access the # Refresh the current user's authorizations inline (so they can access the
...@@ -116,6 +119,17 @@ module Projects ...@@ -116,6 +119,17 @@ module Projects
end end
end end
def create_readme
commit_attrs = {
branch_name: 'master',
commit_message: 'Initial commit',
file_path: 'README.md',
file_content: "# #{@project.name}\n\n#{@project.description}"
}
Files::CreateService.new(@project, current_user, commit_attrs).execute
end
def skip_wiki? def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki !@project.feature_available?(:wiki, current_user) || @skip_wiki
end end
......
...@@ -260,15 +260,15 @@ class TodoService ...@@ -260,15 +260,15 @@ class TodoService
end end
end end
def create_mention_todos(project, target, author, note = nil, skip_users = []) def create_mention_todos(parent, target, author, note = nil, skip_users = [])
# Create Todos for directly addressed users # Create Todos for directly addressed users
directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users) directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note) attributes = attributes_for_todo(parent, target, author, Todo::DIRECTLY_ADDRESSED, note)
create_todos(directly_addressed_users, attributes) create_todos(directly_addressed_users, attributes)
# Create Todos for mentioned users # Create Todos for mentioned users
mentioned_users = filter_mentioned_users(project, note || target, author, skip_users) mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note) attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes) create_todos(mentioned_users, attributes)
end end
...@@ -299,36 +299,36 @@ class TodoService ...@@ -299,36 +299,36 @@ class TodoService
def attributes_for_todo(project, target, author, action, note = nil) def attributes_for_todo(project, target, author, action, note = nil)
attributes_for_target(target).merge!( attributes_for_target(target).merge!(
project_id: project.id, project_id: project&.id,
author_id: author.id, author_id: author.id,
action: action, action: action,
note: note note: note
) )
end end
def filter_todo_users(users, project, target) def filter_todo_users(users, parent, target)
reject_users_without_access(users, project, target).uniq reject_users_without_access(users, parent, target).uniq
end end
def filter_mentioned_users(project, target, author, skip_users = []) def filter_mentioned_users(parent, target, author, skip_users = [])
mentioned_users = target.mentioned_users(author) - skip_users mentioned_users = target.mentioned_users(author) - skip_users
filter_todo_users(mentioned_users, project, target) filter_todo_users(mentioned_users, parent, target)
end end
def filter_directly_addressed_users(project, target, author, skip_users = []) def filter_directly_addressed_users(parent, target, author, skip_users = [])
directly_addressed_users = target.directly_addressed_users(author) - skip_users directly_addressed_users = target.directly_addressed_users(author) - skip_users
filter_todo_users(directly_addressed_users, project, target) filter_todo_users(directly_addressed_users, parent, target)
end end
def reject_users_without_access(users, project, target) def reject_users_without_access(users, parent, target)
if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?) if target.is_a?(Note) && target.for_issuable?
target = target.noteable target = target.noteable
end end
if target.is_a?(Issuable) if target.is_a?(Issuable)
select_users(users, :"read_#{target.to_ability_name}", target) select_users(users, :"read_#{target.to_ability_name}", target)
else else
select_users(users, :read_project, project) select_users(users, :read_project, parent)
end end
end end
......
# frozen_string_literal: true
class AttachmentUploader < GitlabUploader class AttachmentUploader < GitlabUploader
include RecordsUploads::Concern include RecordsUploads::Concern
include ObjectStorage::Concern include ObjectStorage::Concern
......
# frozen_string_literal: true
class AvatarUploader < GitlabUploader class AvatarUploader < GitlabUploader
include UploaderHelper include UploaderHelper
include RecordsUploads::Concern include RecordsUploads::Concern
......
# frozen_string_literal: true
class FaviconUploader < AttachmentUploader class FaviconUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[png ico].freeze EXTENSION_WHITELIST = %w[png ico].freeze
......
# frozen_string_literal: true
class FileMover class FileMover
attr_reader :secret, :file_name, :model, :update_field attr_reader :secret, :file_name, :model, :update_field
......
# frozen_string_literal: true
# This class breaks the actual CarrierWave concept. # This class breaks the actual CarrierWave concept.
# Every uploader should use a base_dir that is model agnostic so we can build # Every uploader should use a base_dir that is model agnostic so we can build
# back URLs from base_dir-relative paths saved in the `Upload` model. # back URLs from base_dir-relative paths saved in the `Upload` model.
...@@ -117,7 +119,7 @@ class FileUploader < GitlabUploader ...@@ -117,7 +119,7 @@ class FileUploader < GitlabUploader
end end
def markdown_link def markdown_link
markdown = "[#{markdown_name}](#{secure_url})" markdown = +"[#{markdown_name}](#{secure_url})"
markdown.prepend("!") if image_or_video? || dangerous? markdown.prepend("!") if image_or_video? || dangerous?
markdown markdown
end end
......
# frozen_string_literal: true
class GitlabUploader < CarrierWave::Uploader::Base class GitlabUploader < CarrierWave::Uploader::Base
class_attribute :options class_attribute :options
......
# frozen_string_literal: true
class JobArtifactUploader < GitlabUploader class JobArtifactUploader < GitlabUploader
extend Workhorse::UploadPath extend Workhorse::UploadPath
include ObjectStorage::Concern include ObjectStorage::Concern
......
# frozen_string_literal: true
class LegacyArtifactUploader < GitlabUploader class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath extend Workhorse::UploadPath
include ObjectStorage::Concern include ObjectStorage::Concern
......
# frozen_string_literal: true
class LfsObjectUploader < GitlabUploader class LfsObjectUploader < GitlabUploader
extend Workhorse::UploadPath extend Workhorse::UploadPath
include ObjectStorage::Concern include ObjectStorage::Concern
......
# frozen_string_literal: true
class NamespaceFileUploader < FileUploader class NamespaceFileUploader < FileUploader
# Re-Override # Re-Override
def self.root def self.root
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment