Merge branch 'master' into build-chunks-on-object-storage
This commit is contained in:
commit
25bd541320
|
@ -1,11 +1,18 @@
|
|||
*.erb
|
||||
lib/gitlab/sanitizers/svg/whitelist.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/models/concerns/relative_positioning.rb
|
||||
app/workers/stuck_merge_jobs_worker.rb
|
||||
lib/gitlab/redis/*.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/*
|
||||
app/models/project_services/kubernetes_service.rb
|
||||
lib/gitlab/workhorse.rb
|
||||
|
@ -19,6 +26,8 @@ ee/db/**/*
|
|||
ee/app/serializers/ee/merge_request_widget_entity.rb
|
||||
ee/lib/api/epics.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/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
|
||||
|
|
|
@ -269,10 +269,10 @@ package-and-qa:
|
|||
<<: *single-script-job-variables
|
||||
SCRIPT_NAME: trigger-build-docs
|
||||
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
|
||||
# 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
|
||||
|
||||
# Trigger a manual docs build in gitlab-docs only on non docs-only branches.
|
||||
|
@ -298,9 +298,8 @@ review-docs-deploy:
|
|||
- gem install gitlab --no-ri --no-rdoc
|
||||
- ./$SCRIPT_NAME deploy
|
||||
only:
|
||||
- /(^docs[\/-].*|.*-docs$)/
|
||||
- branches@gitlab-org/gitlab-ce
|
||||
- branches@gitlab-org/gitlab-ee
|
||||
- /(^docs[\/-].*|.*-docs$)/@gitlab-org/gitlab-ce
|
||||
- /(^docs[\/-].*|.*-docs$)/@gitlab-org/gitlab-ee
|
||||
<<: *except-qa
|
||||
|
||||
# Cleanup remote environment of gitlab-docs
|
||||
|
@ -308,7 +307,7 @@ review-docs-cleanup:
|
|||
<<: *review-docs
|
||||
stage: post-cleanup
|
||||
environment:
|
||||
name: review-docs/$CI_COMMIT_REF_NAME
|
||||
name: review-docs/$CI_COMMIT_REF_SLUG
|
||||
action: stop
|
||||
when: manual
|
||||
script:
|
||||
|
@ -326,11 +325,9 @@ cloud-native-image:
|
|||
variables:
|
||||
GIT_DEPTH: "1"
|
||||
cache: {}
|
||||
before_script:
|
||||
- gem install gitlab --no-rdoc --no-ri
|
||||
- chmod 755 ./scripts/trigger-build-cloud-native
|
||||
script:
|
||||
- ./scripts/trigger-build-cloud-native
|
||||
- gem install gitlab --no-ri --no-rdoc
|
||||
- ./trigger-build cng
|
||||
only:
|
||||
- tags@gitlab-org/gitlab-ce
|
||||
- tags@gitlab-org/gitlab-ee
|
||||
|
|
|
@ -10,10 +10,6 @@
|
|||
Capybara/CurrentPathExpectation:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 956
|
||||
Capybara/FeatureMethods:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 23
|
||||
FactoryBot/DynamicAttributeDefinedStatically:
|
||||
Exclude:
|
||||
|
|
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -2,6 +2,26 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
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)
|
||||
|
||||
### Fixed (8 changes, 1 of them is from the community)
|
||||
|
|
|
@ -1 +1 @@
|
|||
4.3.1
|
||||
5.0.0
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -47,7 +47,7 @@ gem 'omniauth-google-oauth2', '~> 0.5.3'
|
|||
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
|
||||
gem 'omniauth-oauth2-generic', '~> 0.2.2'
|
||||
gem 'omniauth-saml', '~> 1.10'
|
||||
gem 'omniauth-shibboleth', '~> 1.2.0'
|
||||
gem 'omniauth-shibboleth', '~> 1.3.0'
|
||||
gem 'omniauth-twitter', '~> 1.4'
|
||||
gem 'omniauth_crowd', '~> 2.2.0'
|
||||
gem 'omniauth-authentiq', '~> 0.3.3'
|
||||
|
@ -132,7 +132,7 @@ gem 'unf', '~> 0.1.4'
|
|||
gem 'seed-fu', '~> 2.3.7'
|
||||
|
||||
# Markdown and HTML processing
|
||||
gem 'html-pipeline', '~> 2.7.1'
|
||||
gem 'html-pipeline', '~> 2.8'
|
||||
gem 'deckar01-task_list', '2.0.0'
|
||||
gem 'gitlab-markup', '~> 1.6.4'
|
||||
gem 'redcarpet', '~> 3.4'
|
||||
|
|
10
Gemfile.lock
10
Gemfile.lock
|
@ -108,7 +108,7 @@ GEM
|
|||
capybara-screenshot (1.0.14)
|
||||
capybara (>= 1.0, < 3)
|
||||
launchy
|
||||
carrierwave (1.2.1)
|
||||
carrierwave (1.2.3)
|
||||
activemodel (>= 4.0.0)
|
||||
activesupport (>= 4.0.0)
|
||||
mime-types (>= 1.16)
|
||||
|
@ -394,7 +394,7 @@ GEM
|
|||
hipchat (1.5.2)
|
||||
httparty
|
||||
mimemagic
|
||||
html-pipeline (2.7.1)
|
||||
html-pipeline (2.8.3)
|
||||
activesupport (>= 2)
|
||||
nokogiri (>= 1.4)
|
||||
html2text (0.2.0)
|
||||
|
@ -568,7 +568,7 @@ GEM
|
|||
omniauth-saml (1.10.0)
|
||||
omniauth (~> 1.3, >= 1.3.2)
|
||||
ruby-saml (~> 1.7)
|
||||
omniauth-shibboleth (1.2.1)
|
||||
omniauth-shibboleth (1.3.0)
|
||||
omniauth (>= 1.0.0)
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
|
@ -1061,7 +1061,7 @@ DEPENDENCIES
|
|||
hashie-forbidden_attributes
|
||||
health_check (~> 2.6.0)
|
||||
hipchat (~> 1.5.0)
|
||||
html-pipeline (~> 2.7.1)
|
||||
html-pipeline (~> 2.8)
|
||||
html2text
|
||||
httparty (~> 0.13.3)
|
||||
icalendar
|
||||
|
@ -1101,7 +1101,7 @@ DEPENDENCIES
|
|||
omniauth-kerberos (~> 0.3.0)
|
||||
omniauth-oauth2-generic (~> 0.2.2)
|
||||
omniauth-saml (~> 1.10)
|
||||
omniauth-shibboleth (~> 1.2.0)
|
||||
omniauth-shibboleth (~> 1.3.0)
|
||||
omniauth-twitter (~> 1.4)
|
||||
omniauth_crowd (~> 2.2.0)
|
||||
org-ruby (~> 0.9.12)
|
||||
|
|
|
@ -397,7 +397,7 @@ GEM
|
|||
hipchat (1.5.2)
|
||||
httparty
|
||||
mimemagic
|
||||
html-pipeline (2.7.1)
|
||||
html-pipeline (2.8.3)
|
||||
activesupport (>= 2)
|
||||
nokogiri (>= 1.4)
|
||||
html2text (0.2.0)
|
||||
|
@ -572,7 +572,7 @@ GEM
|
|||
omniauth-saml (1.10.0)
|
||||
omniauth (~> 1.3, >= 1.3.2)
|
||||
ruby-saml (~> 1.7)
|
||||
omniauth-shibboleth (1.2.1)
|
||||
omniauth-shibboleth (1.3.0)
|
||||
omniauth (>= 1.0.0)
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
|
@ -1071,7 +1071,7 @@ DEPENDENCIES
|
|||
hashie-forbidden_attributes
|
||||
health_check (~> 2.6.0)
|
||||
hipchat (~> 1.5.0)
|
||||
html-pipeline (~> 2.7.1)
|
||||
html-pipeline (~> 2.8)
|
||||
html2text
|
||||
httparty (~> 0.13.3)
|
||||
icalendar
|
||||
|
@ -1111,7 +1111,7 @@ DEPENDENCIES
|
|||
omniauth-kerberos (~> 0.3.0)
|
||||
omniauth-oauth2-generic (~> 0.2.2)
|
||||
omniauth-saml (~> 1.10)
|
||||
omniauth-shibboleth (~> 1.2.0)
|
||||
omniauth-shibboleth (~> 1.3.0)
|
||||
omniauth-twitter (~> 1.4)
|
||||
omniauth_crowd (~> 2.2.0)
|
||||
org-ruby (~> 0.9.12)
|
||||
|
|
|
@ -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.
|
||||
|
||||
## Why?
|
||||
|
||||
[Read here](https://about.gitlab.com/why/)
|
||||
|
||||
## Is it any good?
|
||||
|
||||
[Yes](https://news.ycombinator.com/item?id=3067434)
|
||||
|
|
|
@ -28,23 +28,29 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
buildUpdateRequest(list) {
|
||||
return {
|
||||
add_label_ids: [list.label.id],
|
||||
};
|
||||
},
|
||||
addIssues() {
|
||||
const firstListIndex = 1;
|
||||
const list = this.modal.selectedList || this.state.lists[firstListIndex];
|
||||
const selectedIssues = ModalStore.getSelectedIssues();
|
||||
const issueIds = selectedIssues.map(issue => issue.id);
|
||||
const req = this.buildUpdateRequest(list);
|
||||
|
||||
// Post the data to the backend
|
||||
gl.boardService.bulkUpdate(issueIds, {
|
||||
add_label_ids: [list.label.id],
|
||||
}).catch(() => {
|
||||
Flash(__('Failed to update issues, please try again.'));
|
||||
gl.boardService
|
||||
.bulkUpdate(issueIds, req)
|
||||
.catch(() => {
|
||||
Flash(__('Failed to update issues, please try again.'));
|
||||
|
||||
selectedIssues.forEach((issue) => {
|
||||
list.removeIssue(issue);
|
||||
list.issuesSize -= 1;
|
||||
selectedIssues.forEach((issue) => {
|
||||
list.removeIssue(issue);
|
||||
list.issuesSize -= 1;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add the issues on the frontend
|
||||
selectedIssues.forEach((issue) => {
|
||||
|
|
|
@ -121,8 +121,7 @@
|
|||
<div
|
||||
v-if="issuesCount > 0 && issues.length === 0"
|
||||
class="empty-state add-issues-empty-state-filter text-center">
|
||||
<div
|
||||
class="svg-content">
|
||||
<div class="svg-content">
|
||||
<img :src="emptyStateSvg" />
|
||||
</div>
|
||||
<div class="text-content">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
export default {
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
issue: {
|
||||
type: Object,
|
||||
|
@ -25,19 +25,16 @@
|
|||
removeIssue() {
|
||||
const { issue } = this;
|
||||
const lists = issue.getLists();
|
||||
const listLabelIds = lists.map(list => list.label.id);
|
||||
|
||||
let labelIds = issue.labels.map(label => label.id).filter(id => !listLabelIds.includes(id));
|
||||
if (labelIds.length === 0) {
|
||||
labelIds = [''];
|
||||
}
|
||||
const req = this.buildPatchRequest(issue, lists);
|
||||
|
||||
const data = {
|
||||
issue: {
|
||||
label_ids: labelIds,
|
||||
},
|
||||
issue: this.seedPatchRequest(issue, req),
|
||||
};
|
||||
|
||||
if (data.issue.label_ids.length === 0) {
|
||||
data.issue.label_ids = [''];
|
||||
}
|
||||
|
||||
// Post the remove data
|
||||
Vue.http.patch(this.updateUrl, data).catch(() => {
|
||||
Flash(__('Failed to remove issue from board, please try again.'));
|
||||
|
@ -54,8 +51,30 @@
|
|||
|
||||
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>
|
||||
<template>
|
||||
<div
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
/* global ListAssignee */
|
||||
|
||||
import Vue from 'vue';
|
||||
import '~/vue_shared/models/label';
|
||||
import IssueProject from './project';
|
||||
|
||||
class ListIssue {
|
||||
|
|
|
@ -7,6 +7,24 @@ import queryData from '../utils/query_data';
|
|||
|
||||
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 {
|
||||
constructor(obj, defaultAvatar) {
|
||||
this.id = obj.id;
|
||||
|
@ -14,8 +32,10 @@ class List {
|
|||
this.position = obj.position;
|
||||
this.title = obj.title;
|
||||
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.page = 1;
|
||||
this.loading = true;
|
||||
|
@ -31,7 +51,7 @@ class List {
|
|||
this.title = this.assignee.name;
|
||||
}
|
||||
|
||||
if (this.type !== 'blank' && this.id) {
|
||||
if (!typeInfo.isBlank && this.id) {
|
||||
this.getIssues().catch(() => {
|
||||
// TODO: handle request error
|
||||
});
|
||||
|
@ -107,7 +127,7 @@ class List {
|
|||
return gl.boardService
|
||||
.getIssuesForList(this.id, data)
|
||||
.then(res => res.data)
|
||||
.then((data) => {
|
||||
.then(data => {
|
||||
this.loading = false;
|
||||
this.issuesSize = data.size;
|
||||
|
||||
|
@ -126,18 +146,7 @@ class List {
|
|||
return gl.boardService
|
||||
.newIssue(this.id, issue)
|
||||
.then(res => res.data)
|
||||
.then((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);
|
||||
}
|
||||
});
|
||||
.then(data => this.onNewIssueResponse(issue, data));
|
||||
}
|
||||
|
||||
createIssues(data) {
|
||||
|
@ -217,6 +226,25 @@ class List {
|
|||
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;
|
||||
|
||||
export default List;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import _ from 'underscore';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.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 { truncateSha } from '~/lib/utils/text_utility';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
|
@ -12,6 +13,7 @@ export default {
|
|||
ClipboardButton,
|
||||
EditButton,
|
||||
Icon,
|
||||
FileIcon,
|
||||
},
|
||||
directives: {
|
||||
Tooltip,
|
||||
|
@ -139,18 +141,20 @@ export default {
|
|||
:name="collapseIcon"
|
||||
:size="16"
|
||||
aria-hidden="true"
|
||||
class="diff-toggle-caret"
|
||||
class="diff-toggle-caret append-right-5"
|
||||
@click.stop="handleToggle"
|
||||
/>
|
||||
<a
|
||||
ref="titleWrapper"
|
||||
:href="titleLink"
|
||||
class="append-right-4"
|
||||
>
|
||||
<i
|
||||
:class="`fa-${icon}`"
|
||||
class="fa fa-fw"
|
||||
<file-icon
|
||||
:file-name="filePath"
|
||||
:size="18"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
css-classes="js-file-icon append-right-5"
|
||||
/>
|
||||
<span v-if="diffFile.renamedFile">
|
||||
<strong
|
||||
v-tooltip
|
||||
|
|
|
@ -24,19 +24,21 @@ export default {
|
|||
...mapGetters(['commit']),
|
||||
parallelDiffLines() {
|
||||
return this.diffLines.map(line => {
|
||||
const parallelLine = Object.assign({}, line);
|
||||
|
||||
if (line.left) {
|
||||
Object.assign(line, { left: trimFirstCharOfLineContent(line.left) });
|
||||
parallelLine.left = trimFirstCharOfLineContent(line.left);
|
||||
} else {
|
||||
Object.assign(line, { left: { type: EMPTY_CELL_TYPE } });
|
||||
parallelLine.left = { type: EMPTY_CELL_TYPE };
|
||||
}
|
||||
|
||||
if (line.right) {
|
||||
Object.assign(line, { right: trimFirstCharOfLineContent(line.right) });
|
||||
parallelLine.right = trimFirstCharOfLineContent(line.right);
|
||||
} else {
|
||||
Object.assign(line, { right: { type: EMPTY_CELL_TYPE } });
|
||||
parallelLine.right = { type: EMPTY_CELL_TYPE };
|
||||
}
|
||||
|
||||
return line;
|
||||
return parallelLine;
|
||||
});
|
||||
},
|
||||
diffLinesLength() {
|
||||
|
|
|
@ -155,18 +155,21 @@ export function addContextLines(options) {
|
|||
}
|
||||
}
|
||||
|
||||
export function trimFirstCharOfLineContent(line) {
|
||||
if (!line.richText) {
|
||||
return line;
|
||||
/**
|
||||
* Trims the first char of the `richText` property when it's either a space or a diff symbol.
|
||||
* @param {Object} line
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function trimFirstCharOfLineContent(line = {}) {
|
||||
const parsedLine = Object.assign({}, line);
|
||||
|
||||
if (line.richText) {
|
||||
const firstChar = parsedLine.richText.charAt(0);
|
||||
|
||||
if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
|
||||
parsedLine.richText = line.richText.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
const firstChar = line.richText.charAt(0);
|
||||
|
||||
if (firstChar === ' ' || firstChar === '+' || firstChar === '-') {
|
||||
Object.assign(line, {
|
||||
richText: line.richText.substring(1),
|
||||
});
|
||||
}
|
||||
|
||||
return line;
|
||||
return parsedLine;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
import flash from '~/flash';
|
||||
import { stripHtml } from '~/lib/utils/text_utility';
|
||||
import * as rootTypes from '../../mutation_types';
|
||||
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
|
||||
import router from '../../../ide_router';
|
||||
|
@ -198,11 +197,18 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
|
|||
if (err.response.status === 400) {
|
||||
$('#ide-create-branch-modal').modal('show');
|
||||
} else {
|
||||
let errMsg = __('Error committing changes. Please try again.');
|
||||
if (err.response.data && err.response.data.message) {
|
||||
errMsg += ` (${stripHtml(err.response.data.message)})`;
|
||||
}
|
||||
flash(errMsg, 'alert', document, null, false, true);
|
||||
dispatch(
|
||||
'setErrorMessage',
|
||||
{
|
||||
text: __('An error accured whilst committing your changes.'),
|
||||
action: () =>
|
||||
dispatch('commitChanges').then(() =>
|
||||
dispatch('setErrorMessage', null, { root: true }),
|
||||
),
|
||||
actionText: __('Please try again'),
|
||||
},
|
||||
{ root: true },
|
||||
);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { __ } from '../../../../locale';
|
||||
import Api from '../../../../api';
|
||||
import flash from '../../../../flash';
|
||||
import router from '../../../ide_router';
|
||||
import { scopes } from './constants';
|
||||
import * as types from './mutation_types';
|
||||
|
@ -8,8 +7,20 @@ import * as rootTypes from '../../mutation_types';
|
|||
|
||||
export const requestMergeRequests = ({ commit }, type) =>
|
||||
commit(types.REQUEST_MERGE_REQUESTS, type);
|
||||
export const receiveMergeRequestsError = ({ commit }, type) => {
|
||||
flash(__('Error loading merge requests.'));
|
||||
export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => {
|
||||
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);
|
||||
};
|
||||
export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) =>
|
||||
|
@ -22,7 +33,7 @@ export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, searc
|
|||
|
||||
Api.mergeRequests({ scope, state, search })
|
||||
.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);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import axios from 'axios';
|
||||
import httpStatus from '../../../../lib/utils/http_status';
|
||||
import { __ } from '../../../../locale';
|
||||
import flash from '../../../../flash';
|
||||
import Poll from '../../../../lib/utils/poll';
|
||||
import service from '../../../services';
|
||||
import { rightSidebarViews } from '../../../constants';
|
||||
|
@ -18,10 +18,27 @@ export const stopPipelinePolling = () => {
|
|||
export const restartPipelinePolling = () => {
|
||||
if (eTagPoll) eTagPoll.restart();
|
||||
};
|
||||
export const forcePipelineRequest = () => {
|
||||
if (eTagPoll) eTagPoll.makeRequest();
|
||||
};
|
||||
|
||||
export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
|
||||
export const receiveLatestPipelineError = ({ commit, dispatch }) => {
|
||||
flash(__('There was an error loading latest pipeline'));
|
||||
export const receiveLatestPipelineError = ({ commit, dispatch }, err) => {
|
||||
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);
|
||||
dispatch('stopPipelinePolling');
|
||||
};
|
||||
|
@ -46,7 +63,7 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
|
|||
method: 'lastCommitPipelines',
|
||||
data: { getters: rootGetters },
|
||||
successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data),
|
||||
errorCallback: () => dispatch('receiveLatestPipelineError'),
|
||||
errorCallback: err => dispatch('receiveLatestPipelineError', err),
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
|
@ -63,9 +80,21 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
|
|||
};
|
||||
|
||||
export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id);
|
||||
export const receiveJobsError = ({ commit }, id) => {
|
||||
flash(__('There was an error loading jobs'));
|
||||
commit(types.RECEIVE_JOBS_ERROR, id);
|
||||
export const receiveJobsError = ({ commit, dispatch }, stage) => {
|
||||
dispatch(
|
||||
'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 }) =>
|
||||
commit(types.RECEIVE_JOBS_SUCCESS, { id, data });
|
||||
|
@ -76,7 +105,7 @@ export const fetchJobs = ({ dispatch }, stage) => {
|
|||
axios
|
||||
.get(stage.dropdownPath)
|
||||
.then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
|
||||
.catch(() => dispatch('receiveJobsError', stage.id));
|
||||
.catch(() => dispatch('receiveJobsError', stage));
|
||||
};
|
||||
|
||||
export const toggleStageCollapsed = ({ commit }, stageId) =>
|
||||
|
@ -90,8 +119,18 @@ export const setDetailJob = ({ commit, dispatch }, job) => {
|
|||
};
|
||||
|
||||
export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE);
|
||||
export const receiveJobTraceError = ({ commit }) => {
|
||||
flash(__('Error fetching job trace'));
|
||||
export const receiveJobTraceError = ({ commit, dispatch }) => {
|
||||
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);
|
||||
};
|
||||
export const receiveJobTraceSuccess = ({ commit }, data) =>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import { __ } from '~/locale';
|
||||
import '~/gl_dropdown';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
import { timeFor } from './lib/utils/datetime_utility';
|
||||
import ModalStore from './boards/stores/modal_store';
|
||||
|
@ -251,3 +252,5 @@ export default class MilestoneSelect {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.MilestoneSelect = MilestoneSelect;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<script>
|
||||
import _ from 'underscore';
|
||||
import { s__ } from '~/locale';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import Flash from '../../flash';
|
||||
import MonitoringService from '../services/monitoring_service';
|
||||
import GraphGroup from './graph_group.vue';
|
||||
|
@ -13,6 +15,7 @@ export default {
|
|||
Graph,
|
||||
GraphGroup,
|
||||
EmptyState,
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
hasMetrics: {
|
||||
|
@ -80,6 +83,14 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
environmentsEndpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
currentEnvironmentName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -96,6 +107,7 @@ export default {
|
|||
this.service = new MonitoringService({
|
||||
metricsEndpoint: this.metricsEndpoint,
|
||||
deploymentEndpoint: this.deploymentEndpoint,
|
||||
environmentsEndpoint: this.environmentsEndpoint,
|
||||
});
|
||||
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
|
||||
eventHub.$on('hoverChanged', this.hoverChanged);
|
||||
|
@ -122,7 +134,11 @@ export default {
|
|||
this.service
|
||||
.getDeploymentData()
|
||||
.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(() => {
|
||||
if (this.store.groups.length < 1) {
|
||||
|
@ -155,8 +171,41 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
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
|
||||
v-for="(groupData, index) in store.groups"
|
||||
:key="index"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import axios from '../../lib/utils/axios_utils';
|
||||
import statusCodes from '../../lib/utils/http_status';
|
||||
import { backOff } from '../../lib/utils/common_utils';
|
||||
import { s__ } from '../../locale';
|
||||
|
||||
const MAX_REQUESTS = 3;
|
||||
|
||||
|
@ -23,9 +24,10 @@ function backOffRequest(makeRequestCallback) {
|
|||
}
|
||||
|
||||
export default class MonitoringService {
|
||||
constructor({ metricsEndpoint, deploymentEndpoint }) {
|
||||
constructor({ metricsEndpoint, deploymentEndpoint, environmentsEndpoint }) {
|
||||
this.metricsEndpoint = metricsEndpoint;
|
||||
this.deploymentEndpoint = deploymentEndpoint;
|
||||
this.environmentsEndpoint = environmentsEndpoint;
|
||||
}
|
||||
|
||||
getGraphsData() {
|
||||
|
@ -33,7 +35,7 @@ export default class MonitoringService {
|
|||
.then(resp => resp.data)
|
||||
.then((response) => {
|
||||
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;
|
||||
});
|
||||
|
@ -47,9 +49,20 @@ export default class MonitoringService {
|
|||
.then(resp => resp.data)
|
||||
.then((response) => {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
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 {
|
|||
constructor() {
|
||||
this.groups = [];
|
||||
this.deploymentData = [];
|
||||
this.environmentsData = [];
|
||||
}
|
||||
|
||||
storeMetrics(groups = []) {
|
||||
|
@ -37,6 +38,10 @@ export default class MonitoringStore {
|
|||
this.deploymentData = deploymentData;
|
||||
}
|
||||
|
||||
storeEnvironmentsData(environmentsData = []) {
|
||||
this.environmentsData = environmentsData;
|
||||
}
|
||||
|
||||
getMetricsCount() {
|
||||
return this.groups.reduce((count, group) => count + group.metrics.length, 0);
|
||||
}
|
||||
|
|
|
@ -1,89 +1,94 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import imageDiffHelper from '~/image_diff/helpers/index';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import imageDiffHelper from '~/image_diff/helpers/index';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
|
||||
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
|
||||
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DiffFileHeader,
|
||||
SkeletonLoadingContainer,
|
||||
},
|
||||
props: {
|
||||
discussion: {
|
||||
type: Object,
|
||||
required: true,
|
||||
export default {
|
||||
components: {
|
||||
DiffFileHeader,
|
||||
SkeletonLoadingContainer,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
noteableData: state => state.notes.noteableData,
|
||||
}),
|
||||
hasTruncatedDiffLines() {
|
||||
return this.discussion.truncatedDiffLines && this.discussion.truncatedDiffLines.length !== 0;
|
||||
props: {
|
||||
discussion: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
isDiscussionsExpanded() {
|
||||
return true; // TODO: @fatihacet - Fix this.
|
||||
data() {
|
||||
return {
|
||||
error: false,
|
||||
};
|
||||
},
|
||||
isCollapsed() {
|
||||
return this.diffFile.collapsed || false;
|
||||
},
|
||||
isImageDiff() {
|
||||
return !this.diffFile.text;
|
||||
},
|
||||
diffFileClass() {
|
||||
const { text } = this.diffFile;
|
||||
return text ? 'text-file' : 'js-image-file';
|
||||
},
|
||||
diffFile() {
|
||||
return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true });
|
||||
},
|
||||
imageDiffHtml() {
|
||||
return this.discussion.imageDiffHtml;
|
||||
},
|
||||
currentUser() {
|
||||
return this.noteableData.current_user;
|
||||
},
|
||||
userColorScheme() {
|
||||
return window.gon.user_color_scheme;
|
||||
},
|
||||
normalizedDiffLines() {
|
||||
const lines = this.discussion.truncatedDiffLines || [];
|
||||
computed: {
|
||||
...mapState({
|
||||
noteableData: state => state.notes.noteableData,
|
||||
}),
|
||||
hasTruncatedDiffLines() {
|
||||
return this.discussion.truncatedDiffLines &&
|
||||
this.discussion.truncatedDiffLines.length !== 0;
|
||||
},
|
||||
isDiscussionsExpanded() {
|
||||
return true; // TODO: @fatihacet - Fix this.
|
||||
},
|
||||
isCollapsed() {
|
||||
return this.diffFile.collapsed || false;
|
||||
},
|
||||
isImageDiff() {
|
||||
return !this.diffFile.text;
|
||||
},
|
||||
diffFileClass() {
|
||||
const { text } = this.diffFile;
|
||||
return text ? 'text-file' : 'js-image-file';
|
||||
},
|
||||
diffFile() {
|
||||
return convertObjectPropsToCamelCase(this.discussion.diffFile, { deep: true });
|
||||
},
|
||||
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() {
|
||||
if (this.isImageDiff) {
|
||||
const canCreateNote = false;
|
||||
const renderCommentBadge = true;
|
||||
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
|
||||
} else if (!this.hasTruncatedDiffLines) {
|
||||
this.fetchDiff();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchDiscussionDiffLines']),
|
||||
rowTag(html) {
|
||||
return html.outerHTML ? 'tr' : 'template';
|
||||
mounted() {
|
||||
if (this.isImageDiff) {
|
||||
const canCreateNote = false;
|
||||
const renderCommentBadge = true;
|
||||
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
|
||||
} else if (!this.hasTruncatedDiffLines) {
|
||||
this.fetchDiff();
|
||||
}
|
||||
},
|
||||
fetchDiff() {
|
||||
this.error = false;
|
||||
this.fetchDiscussionDiffLines(this.discussion)
|
||||
.then(this.highlight)
|
||||
.catch(() => {
|
||||
this.error = true;
|
||||
});
|
||||
methods: {
|
||||
...mapActions(['fetchDiscussionDiffLines']),
|
||||
rowTag(html) {
|
||||
return html.outerHTML ? 'tr' : 'template';
|
||||
},
|
||||
fetchDiff() {
|
||||
this.error = false;
|
||||
this.fetchDiscussionDiffLines(this.discussion)
|
||||
.then(this.highlight)
|
||||
.catch(() => {
|
||||
this.error = true;
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -85,9 +85,9 @@ export const allDiscussions = (state, getters) => {
|
|||
export const resolvedDiscussionsById = state => {
|
||||
const map = {};
|
||||
|
||||
state.discussions.forEach(n => {
|
||||
state.discussions.filter(d => d.resolvable).forEach(n => {
|
||||
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) {
|
||||
map[n.id] = n;
|
||||
|
|
|
@ -39,6 +39,7 @@ export default class Todos {
|
|||
}
|
||||
|
||||
initFilters() {
|
||||
this.initFilterDropdown($('.js-group-search'), 'group_id', ['text']);
|
||||
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
|
||||
this.initFilterDropdown($('.js-type-search'), 'type');
|
||||
this.initFilterDropdown($('.js-action-search'), 'action_id');
|
||||
|
@ -53,7 +54,16 @@ export default class Todos {
|
|||
filterable: searchFields ? true : false,
|
||||
search: { fields: searchFields },
|
||||
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();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
|
||||
|
||||
gcpSignupOffer();
|
|
@ -1,3 +0,0 @@
|
|||
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
|
||||
|
||||
gcpSignupOffer();
|
|
@ -1,7 +1,21 @@
|
|||
import gcpSignupOffer from '~/clusters/components/gcp_signup_offer';
|
||||
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
|
||||
import Project from './project';
|
||||
import ShortcutsNavigation from '../../shortcuts_navigation';
|
||||
|
||||
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 ShortcutsNavigation(); // eslint-disable-line no-new
|
||||
});
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import initTerminal from '~/terminal/';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initTerminal);
|
|
@ -20,6 +20,7 @@ export default class ShortcutsNavigation extends Shortcuts {
|
|||
Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
|
||||
Mousetrap.bind('g k', () => findAndFollowLink('.shortcuts-kubernetes'));
|
||||
Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-environments'));
|
||||
Mousetrap.bind('g l', () => findAndFollowLink('.shortcuts-metrics'));
|
||||
Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
|
||||
|
||||
this.enabledHelp.push('.hidden-shortcut.project');
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
<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 @@
|
|||
methods: {
|
||||
isValid(form) {
|
||||
return !form ||
|
||||
form.find('.js-vue-markdown-field').length ||
|
||||
$(this.$el).closest('form') === form[0];
|
||||
form.find('.js-vue-markdown-field').length &&
|
||||
$(this.$el).closest('form')[0] === form[0];
|
||||
},
|
||||
|
||||
previewMarkdownTab(event, form) {
|
||||
|
|
|
@ -12,6 +12,11 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
cssClasses: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tooltipLabel() {
|
||||
|
@ -30,10 +35,12 @@ export default {
|
|||
<button
|
||||
v-tooltip
|
||||
:title="tooltipLabel"
|
||||
:class="cssClasses"
|
||||
type="button"
|
||||
class="btn btn-blank gutter-toggle btn-sidebar-action"
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
data-boundary="viewport"
|
||||
@click="toggle"
|
||||
>
|
||||
<i
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
}
|
||||
|
||||
svg {
|
||||
vertical-align: text-bottom;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
.external-url,
|
||||
.monitoring-url,
|
||||
|
|
|
@ -449,6 +449,7 @@
|
|||
|
||||
.todo-undone {
|
||||
color: $gl-link-color;
|
||||
fill: $gl-link-color;
|
||||
}
|
||||
|
||||
.author {
|
||||
|
|
|
@ -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 {
|
||||
.card {
|
||||
.card-toggle {
|
||||
|
|
|
@ -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) {
|
||||
.todo {
|
||||
.avatar {
|
||||
|
@ -199,6 +211,10 @@
|
|||
}
|
||||
|
||||
.todos-filters {
|
||||
.filter-categories {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.dropdown-menu-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
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
|
|||
send_upload(uploader, attachment: uploader.filename, disposition: disposition)
|
||||
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
|
||||
|
||||
# Explicitly set the format.
|
||||
|
|
|
@ -70,7 +70,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def redirect_out_of_range(todos)
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
class Groups::UploadsController < Groups::ApplicationController
|
||||
include UploadsActions
|
||||
include WorkhorseRequest
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
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
|
|
@ -1,40 +0,0 @@
|
|||
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
|
|
@ -1,10 +1,15 @@
|
|||
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 :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_update_cluster!, only: [:update]
|
||||
before_action :authorize_admin_cluster!, only: [:destroy]
|
||||
before_action :update_applications_status, only: [:status]
|
||||
helper_method :token_in_session
|
||||
|
||||
STATUS_POLLING_INTERVAL = 10_000
|
||||
|
||||
|
@ -64,6 +69,38 @@ class Projects::ClustersController < Projects::ApplicationController
|
|||
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
|
||||
|
||||
def cluster
|
||||
|
@ -95,6 +132,80 @@ class Projects::ClustersController < Projects::ApplicationController
|
|||
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!
|
||||
access_denied! unless can?(current_user, :update_cluster, cluster)
|
||||
end
|
||||
|
|
|
@ -120,6 +120,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def metrics_redirect
|
||||
environment = project.default_environment
|
||||
|
||||
if environment
|
||||
redirect_to environment_metrics_path(environment)
|
||||
else
|
||||
render :empty
|
||||
end
|
||||
end
|
||||
|
||||
def metrics
|
||||
# Currently, this acts as a hint to load the metrics details into the cache
|
||||
# if they aren't there already
|
||||
|
|
|
@ -2,11 +2,12 @@ class Projects::JobsController < Projects::ApplicationController
|
|||
include SendFileUpload
|
||||
|
||||
before_action :build, except: [:index, :cancel_all]
|
||||
before_action :authorize_read_build!,
|
||||
only: [:index, :show, :status, :raw, :trace]
|
||||
before_action :authorize_read_build!
|
||||
before_action :authorize_update_build!,
|
||||
except: [:index, :show, :status, :raw, :trace, :cancel_all, :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'
|
||||
|
||||
|
@ -44,12 +45,10 @@ class Projects::JobsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def show
|
||||
@builds = @project.pipelines
|
||||
.find_by_sha(@build.sha)
|
||||
.builds
|
||||
@pipeline = @build.pipeline
|
||||
@builds = @pipeline.builds
|
||||
.order('id DESC')
|
||||
.present(current_user: current_user)
|
||||
@pipeline = @build.pipeline
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
@ -136,6 +135,15 @@ class Projects::JobsController < Projects::ApplicationController
|
|||
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
|
||||
|
||||
def authorize_update_build!
|
||||
|
@ -146,6 +154,14 @@ class Projects::JobsController < Projects::ApplicationController
|
|||
return access_denied! unless can?(current_user, :erase_build, build)
|
||||
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
|
||||
{ type: 'text/plain; charset=utf-8', disposition: 'inline' }
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
def index
|
||||
@scope = params[:scope]
|
||||
@pipelines = PipelinesFinder
|
||||
.new(project, scope: @scope)
|
||||
.new(project, current_user, scope: @scope)
|
||||
.execute
|
||||
.page(params[:page])
|
||||
.per(30)
|
||||
|
@ -178,7 +178,7 @@ class Projects::PipelinesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
|
|
@ -74,7 +74,7 @@ module Projects
|
|||
.ordered
|
||||
.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)
|
||||
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
class Projects::TodosController < Projects::ApplicationController
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include TodosActions
|
||||
|
||||
before_action :authenticate_user!, only: [:create]
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
|
||||
def issuable
|
||||
@issuable ||= begin
|
||||
strong_memoize(:issuable) do
|
||||
case params[:issuable_type]
|
||||
when "issue"
|
||||
IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id])
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
class Projects::UploadsController < Projects::ApplicationController
|
||||
include UploadsActions
|
||||
include WorkhorseRequest
|
||||
|
||||
# These will kick you out if you don't have access.
|
||||
skip_before_action :project, :repository,
|
||||
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
|
||||
|
||||
|
|
|
@ -347,6 +347,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
:visibility_level,
|
||||
:template_name,
|
||||
:merge_method,
|
||||
:initialize_with_readme,
|
||||
|
||||
project_feature_attributes: %i[
|
||||
builds_access_level
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
class PipelinesFinder
|
||||
attr_reader :project, :pipelines, :params
|
||||
attr_reader :project, :pipelines, :params, :current_user
|
||||
|
||||
ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze
|
||||
|
||||
def initialize(project, params = {})
|
||||
def initialize(project, current_user, params = {})
|
||||
@project = project
|
||||
@current_user = current_user
|
||||
@pipelines = project.pipelines
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
unless Ability.allowed?(current_user, :read_pipeline, project)
|
||||
return Ci::Pipeline.none
|
||||
end
|
||||
|
||||
items = pipelines
|
||||
items = by_scope(items)
|
||||
items = by_status(items)
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
class TodosFinder
|
||||
prepend FinderWithCrossProjectAccess
|
||||
include FinderMethods
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
requires_cross_project_access unless: -> { project? }
|
||||
|
||||
|
@ -34,9 +35,11 @@ class TodosFinder
|
|||
items = by_author(items)
|
||||
items = by_state(items)
|
||||
items = by_type(items)
|
||||
items = by_group(items)
|
||||
# Filtering by project HAS TO be the last because we use
|
||||
# the project IDs yielded by the todos query thus far
|
||||
items = by_project(items)
|
||||
items = visible_to_user(items)
|
||||
|
||||
sort(items)
|
||||
end
|
||||
|
@ -82,6 +85,10 @@ class TodosFinder
|
|||
params[:project_id].present?
|
||||
end
|
||||
|
||||
def group?
|
||||
params[:group_id].present?
|
||||
end
|
||||
|
||||
def project
|
||||
return @project if defined?(@project)
|
||||
|
||||
|
@ -100,18 +107,14 @@ class TodosFinder
|
|||
@project
|
||||
end
|
||||
|
||||
def project_ids(items)
|
||||
ids = items.except(:order).select(:project_id)
|
||||
if Gitlab::Database.mysql?
|
||||
# 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")
|
||||
def group
|
||||
strong_memoize(:group) do
|
||||
Group.find(params[:group_id])
|
||||
end
|
||||
|
||||
ids
|
||||
end
|
||||
|
||||
def type?
|
||||
type.present? && %w(Issue MergeRequest).include?(type)
|
||||
type.present? && %w(Issue MergeRequest Epic).include?(type)
|
||||
end
|
||||
|
||||
def type
|
||||
|
@ -148,12 +151,37 @@ class TodosFinder
|
|||
|
||||
def by_project(items)
|
||||
if project?
|
||||
items.where(project: project)
|
||||
else
|
||||
projects = Project.public_or_visible_to_user(current_user)
|
||||
|
||||
items.joins(:project).merge(projects)
|
||||
items = items.where(project: project)
|
||||
end
|
||||
|
||||
items
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def by_state(items)
|
||||
|
|
|
@ -2,7 +2,10 @@ class GitlabSchema < GraphQL::Schema
|
|||
use BatchLoader::GraphQL
|
||||
use Gitlab::Graphql::Authorize
|
||||
use Gitlab::Graphql::Present
|
||||
use Gitlab::Graphql::Connections
|
||||
|
||||
query(Types::QueryType)
|
||||
|
||||
default_max_page_size 100
|
||||
# mutation(Types::MutationType)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
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
|
|
@ -0,0 +1,16 @@
|
|||
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
|
|
@ -0,0 +1,11 @@
|
|||
module Resolvers
|
||||
class ProjectPipelinesResolver < BaseResolver
|
||||
include ResolvesPipelines
|
||||
|
||||
alias_method :project, :object
|
||||
|
||||
def resolve(**args)
|
||||
resolve_pipelines(project, args)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
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
|
|
@ -0,0 +1,31 @@
|
|||
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
|
|||
field :upvotes, GraphQL::INT_TYPE, null: false
|
||||
field :downvotes, GraphQL::INT_TYPE, 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
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
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
|
|||
resolver: Resolvers::MergeRequestResolver do
|
||||
authorize :read_merge_request
|
||||
end
|
||||
|
||||
field :pipelines,
|
||||
Types::Ci::PipelineType.connection_type,
|
||||
null: false,
|
||||
resolver: Resolvers::ProjectPipelinesResolver
|
||||
end
|
||||
end
|
||||
|
|
|
@ -122,7 +122,7 @@ module CiStatusHelper
|
|||
|
||||
def no_runners_for_project?(project)
|
||||
project.runners.blank? &&
|
||||
Ci::Runner.shared.blank?
|
||||
Ci::Runner.instance_type.blank?
|
||||
end
|
||||
|
||||
def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body')
|
||||
|
|
|
@ -131,6 +131,19 @@ module IssuablesHelper
|
|||
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")
|
||||
title =
|
||||
case milestone_title
|
||||
|
|
|
@ -43,7 +43,7 @@ module TodosHelper
|
|||
project_commit_path(todo.project,
|
||||
todo.target, anchor: anchor)
|
||||
else
|
||||
path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
|
||||
path = [todo.parent, todo.target]
|
||||
|
||||
path.unshift(:pipelines) if todo.build_failed?
|
||||
|
||||
|
@ -167,4 +167,12 @@ module TodosHelper
|
|||
def show_todo_state?(todo)
|
||||
(todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state)
|
||||
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
|
||||
|
|
|
@ -26,4 +26,8 @@ class Board < ActiveRecord::Base
|
|||
def closed_list
|
||||
lists.merge(List.closed).take
|
||||
end
|
||||
|
||||
def scoped?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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 :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 :url, to: :runner_session, prefix: true, allow_nil: true
|
||||
delegate :terminal_specification, to: :runner_session, allow_nil: true
|
||||
delegate :gitlab_deploy_token, to: :project
|
||||
|
||||
##
|
||||
|
@ -174,6 +180,10 @@ module Ci
|
|||
after_transition pending: :running do |build|
|
||||
build.ensure_metadata.update_timeout_state
|
||||
end
|
||||
|
||||
after_transition running: any do |build|
|
||||
Ci::BuildRunnerSession.where(build: build).delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_metadata
|
||||
|
@ -584,6 +594,10 @@ module Ci
|
|||
super(options).merge(when: read_attribute(:when))
|
||||
end
|
||||
|
||||
def has_terminal?
|
||||
running? && runner_session_url.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_artifacts_size
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
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
|
|||
class Runner < ActiveRecord::Base
|
||||
extend Gitlab::Ci::Model
|
||||
include Gitlab::SQL::Pattern
|
||||
include IgnorableColumn
|
||||
include RedisCacheable
|
||||
include ChronicDurationAttribute
|
||||
|
||||
|
@ -11,6 +12,8 @@ module Ci
|
|||
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
|
||||
|
||||
ignore_column :is_shared
|
||||
|
||||
has_many :builds
|
||||
has_many :runner_projects, inverse_of: :runner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :projects, through: :runner_projects
|
||||
|
@ -21,13 +24,16 @@ module Ci
|
|||
|
||||
before_validation :set_default_values
|
||||
|
||||
scope :specific, -> { where(is_shared: false) }
|
||||
scope :shared, -> { where(is_shared: true) }
|
||||
scope :active, -> { where(active: true) }
|
||||
scope :paused, -> { where(active: false) }
|
||||
scope :online, -> { where('contacted_at > ?', contact_time_deadline) }
|
||||
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) {
|
||||
joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
|
||||
}
|
||||
|
@ -39,9 +45,9 @@ module Ci
|
|||
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(
|
||||
[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
|
||||
)
|
||||
from("(#{union.to_sql}) ci_runners")
|
||||
|
@ -63,7 +69,6 @@ module Ci
|
|||
validate :no_groups, unless: :group_type?
|
||||
validate :any_project, if: :project_type?
|
||||
validate :exactly_one_group, if: :group_type?
|
||||
validate :validate_is_shared
|
||||
|
||||
acts_as_taggable
|
||||
|
||||
|
@ -113,8 +118,7 @@ module Ci
|
|||
end
|
||||
|
||||
def assign_to(project, current_user = nil)
|
||||
if shared?
|
||||
self.is_shared = false if shared?
|
||||
if instance_type?
|
||||
self.runner_type = :project_type
|
||||
elsif group_type?
|
||||
raise ArgumentError, 'Transitioning a group runner to a project runner is not supported'
|
||||
|
@ -137,10 +141,6 @@ module Ci
|
|||
description
|
||||
end
|
||||
|
||||
def shared?
|
||||
is_shared
|
||||
end
|
||||
|
||||
def online?
|
||||
contacted_at && contacted_at > self.class.contact_time_deadline
|
||||
end
|
||||
|
@ -159,10 +159,6 @@ module Ci
|
|||
runner_projects.count == 1
|
||||
end
|
||||
|
||||
def specific?
|
||||
!shared?
|
||||
end
|
||||
|
||||
def assigned_to_group?
|
||||
runner_namespaces.any?
|
||||
end
|
||||
|
@ -260,7 +256,7 @@ module Ci
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def no_projects
|
||||
|
@ -287,12 +283,6 @@ module Ci
|
|||
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)
|
||||
(run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty?
|
||||
end
|
||||
|
|
|
@ -243,6 +243,12 @@ module Issuable
|
|||
opened?
|
||||
end
|
||||
|
||||
def overdue?
|
||||
return false unless respond_to?(:due_date)
|
||||
|
||||
due_date.try(:past?) || false
|
||||
end
|
||||
|
||||
def user_notes_count
|
||||
if notes.loaded?
|
||||
# Use the in-memory association to select and count to avoid hitting the db
|
||||
|
|
|
@ -39,6 +39,8 @@ class Group < Namespace
|
|||
has_many :boards
|
||||
has_many :badges, class_name: 'GroupBadge'
|
||||
|
||||
has_many :todos
|
||||
|
||||
accepts_nested_attributes_for :variables, allow_destroy: true
|
||||
|
||||
validate :visibility_level_allowed_by_projects
|
||||
|
@ -82,6 +84,12 @@ class Group < Namespace
|
|||
where(id: user.authorized_groups.select(:id).reorder(nil))
|
||||
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
|
||||
if current_scope.joins_values.include?(:shared_projects)
|
||||
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
|
||||
|
|
|
@ -275,10 +275,6 @@ class Issue < ActiveRecord::Base
|
|||
user ? readable_by?(user) : publicly_visible?
|
||||
end
|
||||
|
||||
def overdue?
|
||||
due_date.try(:past?) || false
|
||||
end
|
||||
|
||||
def check_for_spam?
|
||||
project.public? && (title_changed? || description_changed?)
|
||||
end
|
||||
|
|
|
@ -229,6 +229,10 @@ class Note < ActiveRecord::Base
|
|||
!for_personal_snippet?
|
||||
end
|
||||
|
||||
def for_issuable?
|
||||
for_issue? || for_merge_request?
|
||||
end
|
||||
|
||||
def skip_project_check?
|
||||
!for_project_noteable?
|
||||
end
|
||||
|
|
|
@ -1422,7 +1422,7 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def group_runners
|
||||
|
@ -1774,6 +1774,15 @@ class Project < ActiveRecord::Base
|
|||
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)
|
||||
# EE would use the environment
|
||||
if protected_for?(ref)
|
||||
|
|
|
@ -240,7 +240,7 @@ class KubernetesService < DeploymentService
|
|||
end
|
||||
|
||||
def deprecation_validation
|
||||
return if active_changed?(from: true, to: false)
|
||||
return if active_changed?(from: true, to: false) || (new_record? && !active?)
|
||||
|
||||
if deprecated?
|
||||
errors[:base] << deprecation_message
|
||||
|
|
|
@ -281,9 +281,9 @@ class Service < ActiveRecord::Base
|
|||
|
||||
def self.build_from_template(project_id, template)
|
||||
service = template.dup
|
||||
service.active = false unless service.valid?
|
||||
service.template = false
|
||||
service.project_id = project_id
|
||||
service.active = false if service.active? && !service.valid?
|
||||
service
|
||||
end
|
||||
|
||||
|
|
|
@ -22,15 +22,18 @@ class Todo < ActiveRecord::Base
|
|||
belongs_to :author, class_name: "User"
|
||||
belongs_to :note
|
||||
belongs_to :project
|
||||
belongs_to :group
|
||||
belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
|
||||
belongs_to :user
|
||||
|
||||
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 :target_id, presence: true, unless: :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 :done, -> { with_state(:done) }
|
||||
|
@ -44,7 +47,7 @@ class Todo < ActiveRecord::Base
|
|||
state :done
|
||||
end
|
||||
|
||||
after_save :keep_around_commit
|
||||
after_save :keep_around_commit, if: :commit_id
|
||||
|
||||
class << self
|
||||
# Priority sorting isn't displayed in the dropdown, because we don't show
|
||||
|
@ -79,6 +82,10 @@ class Todo < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def parent
|
||||
project
|
||||
end
|
||||
|
||||
def unmergeable?
|
||||
action == UNMERGEABLE
|
||||
end
|
||||
|
|
|
@ -1032,7 +1032,7 @@ class User < ActiveRecord::Base
|
|||
|
||||
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
|
||||
|
||||
|
|
|
@ -18,6 +18,10 @@ module Ci
|
|||
@subject.project.branch_allows_collaboration?(@user, @subject.ref)
|
||||
end
|
||||
|
||||
condition(:terminal, scope: :subject) do
|
||||
@subject.has_terminal?
|
||||
end
|
||||
|
||||
rule { protected_ref }.policy do
|
||||
prevent :update_build
|
||||
prevent :erase_build
|
||||
|
@ -29,5 +33,7 @@ module Ci
|
|||
enable :update_build
|
||||
enable :update_commit_status
|
||||
end
|
||||
|
||||
rule { can?(:update_build) & terminal }.enable :create_build_terminal
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,6 +25,8 @@ class DiffFileEntity < Grape::Entity
|
|||
expose :can_modify_blob do |diff_file|
|
||||
merge_request = options[:merge_request]
|
||||
|
||||
next unless diff_file.blob
|
||||
|
||||
if merge_request&.source_project && current_user
|
||||
can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
|
||||
else
|
||||
|
@ -108,6 +110,7 @@ class DiffFileEntity < Grape::Entity
|
|||
project = merge_request.target_project
|
||||
|
||||
next unless project
|
||||
next unless diff_file.content_sha
|
||||
|
||||
project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path))
|
||||
end
|
||||
|
@ -125,6 +128,8 @@ class DiffFileEntity < Grape::Entity
|
|||
end
|
||||
|
||||
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))
|
||||
end
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ class DiscussionEntity < Grape::Entity
|
|||
include NotesHelper
|
||||
|
||||
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 :expanded?, as: :expanded
|
||||
expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? }
|
||||
|
|
|
@ -4,7 +4,7 @@ class RunnerEntity < Grape::Entity
|
|||
expose :id, :description
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@ module Ci
|
|||
@runner = runner
|
||||
end
|
||||
|
||||
def execute
|
||||
def execute(params = {})
|
||||
builds =
|
||||
if runner.shared?
|
||||
if runner.instance_type?
|
||||
builds_for_shared_runner
|
||||
elsif runner.group_type?
|
||||
builds_for_group_runner
|
||||
|
@ -41,6 +41,8 @@ module Ci
|
|||
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
|
||||
begin
|
||||
build.runner_id = runner.id
|
||||
build.runner_session_attributes = params[:session] if params[:session].present?
|
||||
|
||||
build.run!
|
||||
register_success(build)
|
||||
|
||||
|
@ -99,7 +101,7 @@ module Ci
|
|||
end
|
||||
|
||||
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')
|
||||
end
|
||||
|
||||
|
@ -115,7 +117,7 @@ module Ci
|
|||
end
|
||||
|
||||
def register_success(job)
|
||||
labels = { shared_runner: runner.shared?,
|
||||
labels = { shared_runner: runner.instance_type?,
|
||||
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?
|
||||
|
@ -123,10 +125,10 @@ module Ci
|
|||
end
|
||||
|
||||
def jobs_running_for_project(job)
|
||||
return '+Inf' unless runner.shared?
|
||||
return '+Inf' unless runner.instance_type?
|
||||
|
||||
# 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
|
||||
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
|
||||
end
|
||||
|
|
|
@ -32,8 +32,9 @@ module Issues
|
|||
def filter_assignee(issuable)
|
||||
return if params[:assignee_ids].blank?
|
||||
|
||||
# The number of assignees is limited by one for GitLab CE
|
||||
params[:assignee_ids] = params[:assignee_ids][0, 1]
|
||||
unless issuable.allows_multiple_assignees?
|
||||
params[:assignee_ids] = params[:assignee_ids].take(1)
|
||||
end
|
||||
|
||||
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ module Labels
|
|||
@available_labels ||= LabelsFinder.new(
|
||||
current_user,
|
||||
"#{parent_type}_id".to_sym => parent.id,
|
||||
include_ancestor_groups: include_ancestor_groups?,
|
||||
only_group_labels: parent_is_group?
|
||||
).execute(skip_authorization: skip_authorization)
|
||||
end
|
||||
|
@ -30,7 +31,8 @@ module Labels
|
|||
new_label = available_labels.find_by(title: title)
|
||||
|
||||
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
|
||||
|
||||
new_label
|
||||
|
@ -47,5 +49,9 @@ module Labels
|
|||
def parent_is_group?
|
||||
parent_type == "group"
|
||||
end
|
||||
|
||||
def include_ancestor_groups?
|
||||
params[:include_ancestor_groups] == true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,6 +2,8 @@ module Projects
|
|||
class CreateService < BaseService
|
||||
def initialize(user, params)
|
||||
@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
|
||||
|
||||
def execute
|
||||
|
@ -11,7 +13,6 @@ module Projects
|
|||
|
||||
forked_from_project_id = params.delete(:forked_from_project_id)
|
||||
import_data = params.delete(:import_data)
|
||||
@skip_wiki = params.delete(:skip_wiki)
|
||||
|
||||
@project = Project.new(params)
|
||||
|
||||
|
@ -102,6 +103,8 @@ module Projects
|
|||
setup_authorizations
|
||||
|
||||
current_user.invalidate_personal_projects_count
|
||||
|
||||
create_readme if @initialize_with_readme
|
||||
end
|
||||
|
||||
# Refresh the current user's authorizations inline (so they can access the
|
||||
|
@ -116,6 +119,17 @@ module Projects
|
|||
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?
|
||||
!@project.feature_available?(:wiki, current_user) || @skip_wiki
|
||||
end
|
||||
|
|
|
@ -260,15 +260,15 @@ class TodoService
|
|||
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
|
||||
directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users)
|
||||
attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
|
||||
directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users)
|
||||
attributes = attributes_for_todo(parent, target, author, Todo::DIRECTLY_ADDRESSED, note)
|
||||
create_todos(directly_addressed_users, attributes)
|
||||
|
||||
# Create Todos for mentioned users
|
||||
mentioned_users = filter_mentioned_users(project, note || target, author, skip_users)
|
||||
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
|
||||
mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users)
|
||||
attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note)
|
||||
create_todos(mentioned_users, attributes)
|
||||
end
|
||||
|
||||
|
@ -299,36 +299,36 @@ class TodoService
|
|||
|
||||
def attributes_for_todo(project, target, author, action, note = nil)
|
||||
attributes_for_target(target).merge!(
|
||||
project_id: project.id,
|
||||
project_id: project&.id,
|
||||
author_id: author.id,
|
||||
action: action,
|
||||
note: note
|
||||
)
|
||||
end
|
||||
|
||||
def filter_todo_users(users, project, target)
|
||||
reject_users_without_access(users, project, target).uniq
|
||||
def filter_todo_users(users, parent, target)
|
||||
reject_users_without_access(users, parent, target).uniq
|
||||
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
|
||||
filter_todo_users(mentioned_users, project, target)
|
||||
filter_todo_users(mentioned_users, parent, target)
|
||||
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
|
||||
filter_todo_users(directly_addressed_users, project, target)
|
||||
filter_todo_users(directly_addressed_users, parent, target)
|
||||
end
|
||||
|
||||
def reject_users_without_access(users, project, target)
|
||||
if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?)
|
||||
def reject_users_without_access(users, parent, target)
|
||||
if target.is_a?(Note) && target.for_issuable?
|
||||
target = target.noteable
|
||||
end
|
||||
|
||||
if target.is_a?(Issuable)
|
||||
select_users(users, :"read_#{target.to_ability_name}", target)
|
||||
else
|
||||
select_users(users, :read_project, project)
|
||||
select_users(users, :read_project, parent)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AttachmentUploader < GitlabUploader
|
||||
include RecordsUploads::Concern
|
||||
include ObjectStorage::Concern
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AvatarUploader < GitlabUploader
|
||||
include UploaderHelper
|
||||
include RecordsUploads::Concern
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FaviconUploader < AttachmentUploader
|
||||
EXTENSION_WHITELIST = %w[png ico].freeze
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FileMover
|
||||
attr_reader :secret, :file_name, :model, :update_field
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This class breaks the actual CarrierWave concept.
|
||||
# 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.
|
||||
|
@ -117,7 +119,7 @@ class FileUploader < GitlabUploader
|
|||
end
|
||||
|
||||
def markdown_link
|
||||
markdown = "[#{markdown_name}](#{secure_url})"
|
||||
markdown = +"[#{markdown_name}](#{secure_url})"
|
||||
markdown.prepend("!") if image_or_video? || dangerous?
|
||||
markdown
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GitlabUploader < CarrierWave::Uploader::Base
|
||||
class_attribute :options
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class JobArtifactUploader < GitlabUploader
|
||||
extend Workhorse::UploadPath
|
||||
include ObjectStorage::Concern
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LegacyArtifactUploader < GitlabUploader
|
||||
extend Workhorse::UploadPath
|
||||
include ObjectStorage::Concern
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class LfsObjectUploader < GitlabUploader
|
||||
extend Workhorse::UploadPath
|
||||
include ObjectStorage::Concern
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class NamespaceFileUploader < FileUploader
|
||||
# Re-Override
|
||||
def self.root
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue