Merge branch 'master' into ide-temp-file-folder-fixes
This commit is contained in:
commit
9990afb92c
6
.babelrc
6
.babelrc
|
@ -1,6 +1,9 @@
|
||||||
{
|
{
|
||||||
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
|
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
|
||||||
"env": {
|
"env": {
|
||||||
|
"karma": {
|
||||||
|
"plugins": ["rewire"]
|
||||||
|
},
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
[
|
[
|
||||||
|
@ -14,7 +17,8 @@
|
||||||
{
|
{
|
||||||
"process.env.BABEL_ENV": "coverage"
|
"process.env.BABEL_ENV": "coverage"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"rewire"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
|
||||||
Lint/NestedPercentLiteral:
|
Lint/NestedPercentLiteral:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/gitlab/git/repository.rb'
|
- 'lib/gitlab/git/repository.rb'
|
||||||
- 'spec/support/email_format_shared_examples.rb'
|
- 'spec/support/shared_examples/email_format_shared_examples.rb'
|
||||||
|
|
||||||
# Offense count: 1
|
# Offense count: 1
|
||||||
Lint/ReturnInVoidContext:
|
Lint/ReturnInVoidContext:
|
||||||
|
@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
|
||||||
- 'spec/lib/gitlab/diff/parser_spec.rb'
|
- 'spec/lib/gitlab/diff/parser_spec.rb'
|
||||||
- 'spec/lib/json_web_token/rsa_token_spec.rb'
|
- 'spec/lib/json_web_token/rsa_token_spec.rb'
|
||||||
- 'spec/models/commit_spec.rb'
|
- 'spec/models/commit_spec.rb'
|
||||||
- 'spec/support/repo_helpers.rb'
|
- 'spec/support/helpers/repo_helpers.rb'
|
||||||
- 'spec/support/seed_repo.rb'
|
- 'spec/support/helpers/seed_repo.rb'
|
||||||
|
|
||||||
# Offense count: 112
|
# Offense count: 112
|
||||||
# Configuration parameters: Blacklist.
|
# Configuration parameters: Blacklist.
|
||||||
|
@ -496,7 +496,7 @@ Style/EmptyLiteral:
|
||||||
- 'spec/lib/gitlab/request_context_spec.rb'
|
- 'spec/lib/gitlab/request_context_spec.rb'
|
||||||
- 'spec/lib/gitlab/workhorse_spec.rb'
|
- 'spec/lib/gitlab/workhorse_spec.rb'
|
||||||
- 'spec/requests/api/jobs_spec.rb'
|
- 'spec/requests/api/jobs_spec.rb'
|
||||||
- 'spec/support/chat_slash_commands_shared_examples.rb'
|
- 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
|
||||||
|
|
||||||
# Offense count: 102
|
# Offense count: 102
|
||||||
# Cop supports --auto-correct.
|
# Cop supports --auto-correct.
|
||||||
|
|
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -2,6 +2,34 @@
|
||||||
documentation](doc/development/changelog.md) for instructions on adding your own
|
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||||
entry.
|
entry.
|
||||||
|
|
||||||
|
## 10.7.1 (2018-04-23)
|
||||||
|
|
||||||
|
### Fixed (11 changes)
|
||||||
|
|
||||||
|
- [API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors` when no value is passed for `order_by` or `sort`. !18393
|
||||||
|
- Fix a case with secret variables being empty sometimes. !18400
|
||||||
|
- Fix `Trace::HttpIO` can not render multi-byte chars. !18417
|
||||||
|
- Fix specifying a non-default ref when requesting an archive using the legacy URL. !18468
|
||||||
|
- Respect visibility options and description when importing project from template. !18473
|
||||||
|
- Removes 'No Job log' message from build trace. !18523
|
||||||
|
- Align action icons in pipeline graph.
|
||||||
|
- Fix direct_upload when records with null file_store are used.
|
||||||
|
- Removed alert box in IDE when redirecting to new merge request.
|
||||||
|
- Fixed IDE not loading for sub groups.
|
||||||
|
- Fixed IDE not showing loading state when tree is loading.
|
||||||
|
|
||||||
|
### Performance (4 changes)
|
||||||
|
|
||||||
|
- Validate project path prior to hitting the database. !18322
|
||||||
|
- Add index to file_store on ci_job_artifacts. !18444
|
||||||
|
- Fix N+1 queries when loading participants for a commit note.
|
||||||
|
- Support Markdown rendering using multiple projects.
|
||||||
|
|
||||||
|
### Added (1 change)
|
||||||
|
|
||||||
|
- Add an API endpoint to download git repository snapshots. !18173
|
||||||
|
|
||||||
|
|
||||||
## 10.7.0 (2018-04-22)
|
## 10.7.0 (2018-04-22)
|
||||||
|
|
||||||
### Security (6 changes, 2 of them are from the community)
|
### Security (6 changes, 2 of them are from the community)
|
||||||
|
|
|
@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
|
||||||
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
|
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
|
||||||
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
|
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
|
||||||
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
|
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
|
||||||
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release)
|
- [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
|
||||||
|
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
|
||||||
|
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
|
||||||
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
|
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
|
||||||
- [Implement design & UI elements](#implement-design-ui-elements)
|
- [Implement design & UI elements](#implement-design-ui-elements)
|
||||||
- [Issue tracker](#issue-tracker)
|
- [Issue tracker](#issue-tracker)
|
||||||
|
@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following:
|
||||||
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
|
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
|
||||||
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
|
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
|
||||||
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
|
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
|
||||||
|
- Priority: ~P1, ~P2, ~P3, ~P4
|
||||||
|
- Severity: ~S1, ~S2, ~S3, ~S4
|
||||||
|
|
||||||
All labels, their meaning and priority are defined on the
|
All labels, their meaning and priority are defined on the
|
||||||
[labels page][labels-page].
|
[labels page][labels-page].
|
||||||
|
@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga
|
||||||
|
|
||||||
| Label | Meaning | Estimate time to fix | Guidance |
|
| Label | Meaning | Estimate time to fix | Guidance |
|
||||||
|-------|-----------------|------------------------------------------------------------------|----------|
|
|-------|-----------------|------------------------------------------------------------------|----------|
|
||||||
| ~P1 | Urgent Priority | The current release | |
|
| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
|
||||||
| ~P2 | High Priority | The next release | |
|
| ~P2 | High Priority | The next release | |
|
||||||
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
|
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
|
||||||
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
|
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
|
||||||
|
|
11
Gemfile.lock
11
Gemfile.lock
|
@ -178,7 +178,7 @@ GEM
|
||||||
docile (1.1.5)
|
docile (1.1.5)
|
||||||
domain_name (0.5.20170404)
|
domain_name (0.5.20170404)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (4.3.1)
|
doorkeeper (4.3.2)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
doorkeeper-openid_connect (1.3.0)
|
doorkeeper-openid_connect (1.3.0)
|
||||||
doorkeeper (~> 4.3)
|
doorkeeper (~> 4.3)
|
||||||
|
@ -483,10 +483,11 @@ GEM
|
||||||
logging (2.2.2)
|
logging (2.2.2)
|
||||||
little-plugger (~> 1.1)
|
little-plugger (~> 1.1)
|
||||||
multi_json (~> 1.10)
|
multi_json (~> 1.10)
|
||||||
lograge (0.5.1)
|
lograge (0.10.0)
|
||||||
actionpack (>= 4, < 5.2)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4, < 5.2)
|
activesupport (>= 4)
|
||||||
railties (>= 4, < 5.2)
|
railties (>= 4)
|
||||||
|
request_store (~> 1.0)
|
||||||
loofah (2.2.2)
|
loofah (2.2.2)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
|
|
|
@ -0,0 +1,245 @@
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||||
|
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||||
|
import VirtualList from 'vue-virtual-scroll-list';
|
||||||
|
import Item from './item.vue';
|
||||||
|
import router from '../../ide_router';
|
||||||
|
import {
|
||||||
|
MAX_FILE_FINDER_RESULTS,
|
||||||
|
FILE_FINDER_ROW_HEIGHT,
|
||||||
|
FILE_FINDER_EMPTY_ROW_HEIGHT,
|
||||||
|
} from '../../constants';
|
||||||
|
import {
|
||||||
|
UP_KEY_CODE,
|
||||||
|
DOWN_KEY_CODE,
|
||||||
|
ENTER_KEY_CODE,
|
||||||
|
ESC_KEY_CODE,
|
||||||
|
} from '../../../lib/utils/keycodes';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Item,
|
||||||
|
VirtualList,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
focusedIndex: 0,
|
||||||
|
searchText: '',
|
||||||
|
mouseOver: false,
|
||||||
|
cancelMouseOver: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['allBlobs']),
|
||||||
|
...mapState(['fileFindVisible', 'loading']),
|
||||||
|
filteredBlobs() {
|
||||||
|
const searchText = this.searchText.trim();
|
||||||
|
|
||||||
|
if (searchText === '') {
|
||||||
|
return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fuzzaldrinPlus
|
||||||
|
.filter(this.allBlobs, searchText, {
|
||||||
|
key: 'path',
|
||||||
|
maxResults: MAX_FILE_FINDER_RESULTS,
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
|
||||||
|
},
|
||||||
|
filteredBlobsLength() {
|
||||||
|
return this.filteredBlobs.length;
|
||||||
|
},
|
||||||
|
listShowCount() {
|
||||||
|
return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
|
||||||
|
},
|
||||||
|
listHeight() {
|
||||||
|
return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
|
||||||
|
},
|
||||||
|
showClearInputButton() {
|
||||||
|
return this.searchText.trim() !== '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
fileFindVisible() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (!this.fileFindVisible) {
|
||||||
|
this.searchText = '';
|
||||||
|
} else {
|
||||||
|
this.focusedIndex = 0;
|
||||||
|
|
||||||
|
if (this.$refs.searchInput) {
|
||||||
|
this.$refs.searchInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
searchText() {
|
||||||
|
this.focusedIndex = 0;
|
||||||
|
},
|
||||||
|
focusedIndex() {
|
||||||
|
if (!this.mouseOver) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const el = this.$refs.virtualScrollList.$el;
|
||||||
|
const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
|
||||||
|
const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
|
||||||
|
|
||||||
|
if (this.focusedIndex === 0) {
|
||||||
|
// if index is the first index, scroll straight to start
|
||||||
|
el.scrollTop = 0;
|
||||||
|
} else if (this.focusedIndex === this.filteredBlobsLength - 1) {
|
||||||
|
// if index is the last index, scroll to the end
|
||||||
|
el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
|
||||||
|
} else if (scrollTop >= bottom + el.scrollTop) {
|
||||||
|
// if element is off the bottom of the scroll list, scroll down one item
|
||||||
|
el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
|
||||||
|
} else if (scrollTop < el.scrollTop) {
|
||||||
|
// if element is off the top of the scroll list, scroll up one item
|
||||||
|
el.scrollTop = scrollTop;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(['toggleFileFinder']),
|
||||||
|
clearSearchInput() {
|
||||||
|
this.searchText = '';
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.searchInput.focus();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onKeydown(e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case UP_KEY_CODE:
|
||||||
|
e.preventDefault();
|
||||||
|
this.mouseOver = false;
|
||||||
|
this.cancelMouseOver = true;
|
||||||
|
if (this.focusedIndex > 0) {
|
||||||
|
this.focusedIndex -= 1;
|
||||||
|
} else {
|
||||||
|
this.focusedIndex = this.filteredBlobsLength - 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DOWN_KEY_CODE:
|
||||||
|
e.preventDefault();
|
||||||
|
this.mouseOver = false;
|
||||||
|
this.cancelMouseOver = true;
|
||||||
|
if (this.focusedIndex < this.filteredBlobsLength - 1) {
|
||||||
|
this.focusedIndex += 1;
|
||||||
|
} else {
|
||||||
|
this.focusedIndex = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyup(e) {
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case ENTER_KEY_CODE:
|
||||||
|
this.openFile(this.filteredBlobs[this.focusedIndex]);
|
||||||
|
break;
|
||||||
|
case ESC_KEY_CODE:
|
||||||
|
this.toggleFileFinder(false);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openFile(file) {
|
||||||
|
this.toggleFileFinder(false);
|
||||||
|
router.push(`/project${file.url}`);
|
||||||
|
},
|
||||||
|
onMouseOver(index) {
|
||||||
|
if (!this.cancelMouseOver) {
|
||||||
|
this.mouseOver = true;
|
||||||
|
this.focusedIndex = index;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMouseMove(index) {
|
||||||
|
this.cancelMouseOver = false;
|
||||||
|
this.onMouseOver(index);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="ide-file-finder-overlay"
|
||||||
|
@mousedown.self="toggleFileFinder(false)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dropdown-menu diff-file-changes ide-file-finder show"
|
||||||
|
>
|
||||||
|
<div class="dropdown-input">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
class="dropdown-input-field"
|
||||||
|
:placeholder="__('Search files')"
|
||||||
|
autocomplete="off"
|
||||||
|
v-model="searchText"
|
||||||
|
ref="searchInput"
|
||||||
|
@keydown="onKeydown($event)"
|
||||||
|
@keyup="onKeyup($event)"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
aria-hidden="true"
|
||||||
|
class="fa fa-search dropdown-input-search"
|
||||||
|
:class="{
|
||||||
|
hidden: showClearInputButton
|
||||||
|
}"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
role="button"
|
||||||
|
:aria-label="__('Clear search input')"
|
||||||
|
class="fa fa-times dropdown-input-clear"
|
||||||
|
:class="{
|
||||||
|
show: showClearInputButton
|
||||||
|
}"
|
||||||
|
@click="clearSearchInput"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<virtual-list
|
||||||
|
:size="listHeight"
|
||||||
|
:remain="listShowCount"
|
||||||
|
wtag="ul"
|
||||||
|
ref="virtualScrollList"
|
||||||
|
>
|
||||||
|
<template v-if="filteredBlobsLength">
|
||||||
|
<li
|
||||||
|
v-for="(file, index) in filteredBlobs"
|
||||||
|
:key="file.key"
|
||||||
|
>
|
||||||
|
<item
|
||||||
|
class="disable-hover"
|
||||||
|
:file="file"
|
||||||
|
:search-text="searchText"
|
||||||
|
:focused="index === focusedIndex"
|
||||||
|
:index="index"
|
||||||
|
@click="openFile"
|
||||||
|
@mouseover="onMouseOver"
|
||||||
|
@mousemove="onMouseMove"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<li
|
||||||
|
v-else
|
||||||
|
class="dropdown-menu-empty-item"
|
||||||
|
>
|
||||||
|
<div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
|
||||||
|
<template v-if="loading">
|
||||||
|
{{ __('Loading...') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ __('No files found.') }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</virtual-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -0,0 +1,113 @@
|
||||||
|
<script>
|
||||||
|
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||||
|
import FileIcon from '../../../vue_shared/components/file_icon.vue';
|
||||||
|
import ChangedFileIcon from '../changed_file_icon.vue';
|
||||||
|
|
||||||
|
const MAX_PATH_LENGTH = 60;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ChangedFileIcon,
|
||||||
|
FileIcon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
file: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
focused: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
searchText: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pathWithEllipsis() {
|
||||||
|
const path = this.file.path;
|
||||||
|
|
||||||
|
return path.length < MAX_PATH_LENGTH
|
||||||
|
? path
|
||||||
|
: `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
|
||||||
|
},
|
||||||
|
nameSearchTextOccurences() {
|
||||||
|
return fuzzaldrinPlus.match(this.file.name, this.searchText);
|
||||||
|
},
|
||||||
|
pathSearchTextOccurences() {
|
||||||
|
return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickRow() {
|
||||||
|
this.$emit('click', this.file);
|
||||||
|
},
|
||||||
|
mouseOverRow() {
|
||||||
|
this.$emit('mouseover', this.index);
|
||||||
|
},
|
||||||
|
mouseMove() {
|
||||||
|
this.$emit('mousemove', this.index);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="diff-changed-file"
|
||||||
|
:class="{
|
||||||
|
'is-focused': focused,
|
||||||
|
}"
|
||||||
|
@click.prevent="clickRow"
|
||||||
|
@mouseover="mouseOverRow"
|
||||||
|
@mousemove="mouseMove"
|
||||||
|
>
|
||||||
|
<file-icon
|
||||||
|
:file-name="file.name"
|
||||||
|
:size="16"
|
||||||
|
css-classes="diff-file-changed-icon append-right-8"
|
||||||
|
/>
|
||||||
|
<span class="diff-changed-file-content append-right-8">
|
||||||
|
<strong
|
||||||
|
class="diff-changed-file-name"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="(char, index) in file.name.split('')"
|
||||||
|
:key="index + char"
|
||||||
|
:class="{
|
||||||
|
highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
|
||||||
|
}"
|
||||||
|
v-text="char"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</strong>
|
||||||
|
<span
|
||||||
|
class="diff-changed-file-path prepend-top-5"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="(char, index) in pathWithEllipsis.split('')"
|
||||||
|
:key="index + char"
|
||||||
|
:class="{
|
||||||
|
highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
|
||||||
|
}"
|
||||||
|
v-text="char"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="file.changed || file.tempFile"
|
||||||
|
class="diff-changed-stats"
|
||||||
|
>
|
||||||
|
<changed-file-icon
|
||||||
|
:file="file"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
|
@ -1,55 +1,91 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from 'vuex';
|
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||||
import ideSidebar from './ide_side_bar.vue';
|
import Mousetrap from 'mousetrap';
|
||||||
import ideContextbar from './ide_context_bar.vue';
|
import ideSidebar from './ide_side_bar.vue';
|
||||||
import repoTabs from './repo_tabs.vue';
|
import ideContextbar from './ide_context_bar.vue';
|
||||||
import ideStatusBar from './ide_status_bar.vue';
|
import repoTabs from './repo_tabs.vue';
|
||||||
import repoEditor from './repo_editor.vue';
|
import ideStatusBar from './ide_status_bar.vue';
|
||||||
|
import repoEditor from './repo_editor.vue';
|
||||||
|
import FindFile from './file_finder/index.vue';
|
||||||
|
|
||||||
export default {
|
const originalStopCallback = Mousetrap.stopCallback;
|
||||||
components: {
|
|
||||||
ideSidebar,
|
|
||||||
ideContextbar,
|
|
||||||
repoTabs,
|
|
||||||
ideStatusBar,
|
|
||||||
repoEditor,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
emptyStateSvgPath: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
noChangesStateSvgPath: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
committedStateSvgPath: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
|
|
||||||
...mapGetters(['activeFile', 'hasChanges']),
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
|
||||||
window.onbeforeunload = e => {
|
|
||||||
if (!this.changedFiles.length) return undefined;
|
|
||||||
|
|
||||||
Object.assign(e, {
|
export default {
|
||||||
returnValue,
|
components: {
|
||||||
|
ideSidebar,
|
||||||
|
ideContextbar,
|
||||||
|
repoTabs,
|
||||||
|
ideStatusBar,
|
||||||
|
repoEditor,
|
||||||
|
FindFile,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
emptyStateSvgPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
noChangesStateSvgPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
committedStateSvgPath: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState([
|
||||||
|
'changedFiles',
|
||||||
|
'openFiles',
|
||||||
|
'viewer',
|
||||||
|
'currentMergeRequestId',
|
||||||
|
'fileFindVisible',
|
||||||
|
]),
|
||||||
|
...mapGetters(['activeFile', 'hasChanges']),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const returnValue = 'Are you sure you want to lose unsaved changes?';
|
||||||
|
window.onbeforeunload = e => {
|
||||||
|
if (!this.changedFiles.length) return undefined;
|
||||||
|
|
||||||
|
Object.assign(e, {
|
||||||
|
returnValue,
|
||||||
|
});
|
||||||
|
return returnValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
|
||||||
|
if (e.preventDefault) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toggleFileFinder(!this.fileFindVisible);
|
||||||
});
|
});
|
||||||
return returnValue;
|
|
||||||
};
|
Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
|
||||||
},
|
},
|
||||||
};
|
methods: {
|
||||||
|
...mapActions(['toggleFileFinder']),
|
||||||
|
mousetrapStopCallback(e, el, combo) {
|
||||||
|
if (combo === 't' && el.classList.contains('dropdown-input-field')) {
|
||||||
|
return true;
|
||||||
|
} else if (combo === 'command+p' || combo === 'ctrl+p') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalStopCallback(e, el, combo);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="ide-view"
|
class="ide-view"
|
||||||
>
|
>
|
||||||
|
<find-file
|
||||||
|
v-show="fileFindVisible"
|
||||||
|
/>
|
||||||
<ide-sidebar />
|
<ide-sidebar />
|
||||||
<div
|
<div
|
||||||
class="multi-file-edit-pane"
|
class="multi-file-edit-pane"
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
// Fuzzy file finder
|
// Fuzzy file finder
|
||||||
|
export const MAX_FILE_FINDER_RESULTS = 40;
|
||||||
|
export const FILE_FINDER_ROW_HEIGHT = 55;
|
||||||
|
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
|
||||||
|
|
||||||
|
// Commit message textarea
|
||||||
export const MAX_TITLE_LENGTH = 50;
|
export const MAX_TITLE_LENGTH = 50;
|
||||||
export const MAX_BODY_LENGTH = 72;
|
export const MAX_BODY_LENGTH = 72;
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import _ from 'underscore';
|
import _ from 'underscore';
|
||||||
|
import store from '../stores';
|
||||||
import DecorationsController from './decorations/controller';
|
import DecorationsController from './decorations/controller';
|
||||||
import DirtyDiffController from './diff/controller';
|
import DirtyDiffController from './diff/controller';
|
||||||
import Disposable from './common/disposable';
|
import Disposable from './common/disposable';
|
||||||
import ModelManager from './common/model_manager';
|
import ModelManager from './common/model_manager';
|
||||||
import editorOptions, { defaultEditorOptions } from './editor_options';
|
import editorOptions, { defaultEditorOptions } from './editor_options';
|
||||||
import gitlabTheme from './themes/gl_theme';
|
import gitlabTheme from './themes/gl_theme';
|
||||||
|
import keymap from './keymap.json';
|
||||||
|
|
||||||
export const clearDomElement = el => {
|
export const clearDomElement = el => {
|
||||||
if (!el || !el.firstChild) return;
|
if (!el || !el.firstChild) return;
|
||||||
|
@ -53,6 +55,8 @@ export default class Editor {
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.addCommands();
|
||||||
|
|
||||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,6 +77,8 @@ export default class Editor {
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.addCommands();
|
||||||
|
|
||||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,4 +195,31 @@ export default class Editor {
|
||||||
static renderSideBySide(domElement) {
|
static renderSideBySide(domElement) {
|
||||||
return domElement.offsetWidth >= 700;
|
return domElement.offsetWidth >= 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addCommands() {
|
||||||
|
const getKeyCode = key => {
|
||||||
|
const monacoKeyMod = key.indexOf('KEY_') === 0;
|
||||||
|
|
||||||
|
return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
keymap.forEach(command => {
|
||||||
|
const keybindings = command.bindings.map(binding => {
|
||||||
|
const keys = binding.split('+');
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.instance.addAction({
|
||||||
|
id: command.id,
|
||||||
|
label: command.label,
|
||||||
|
keybindings,
|
||||||
|
run() {
|
||||||
|
store.dispatch(command.action.name, command.action.params);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "file-finder",
|
||||||
|
"label": "File finder",
|
||||||
|
"bindings": ["CtrlCmd+KEY_P"],
|
||||||
|
"action": {
|
||||||
|
"name": "toggleFileFinder",
|
||||||
|
"params": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -146,7 +146,13 @@ export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, temp
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toggleFileFinder = ({ commit }, fileFindVisible) =>
|
||||||
|
commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
|
||||||
|
|
||||||
export * from './actions/tree';
|
export * from './actions/tree';
|
||||||
export * from './actions/file';
|
export * from './actions/file';
|
||||||
export * from './actions/project';
|
export * from './actions/project';
|
||||||
export * from './actions/merge_request';
|
export * from './actions/merge_request';
|
||||||
|
|
||||||
|
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||||
|
export default () => {};
|
||||||
|
|
|
@ -42,4 +42,20 @@ export const collapseButtonTooltip = state =>
|
||||||
|
|
||||||
export const hasMergeRequest = state => !!state.currentMergeRequestId;
|
export const hasMergeRequest = state => !!state.currentMergeRequestId;
|
||||||
|
|
||||||
|
export const allBlobs = state =>
|
||||||
|
Object.keys(state.entries)
|
||||||
|
.reduce((acc, key) => {
|
||||||
|
const entry = state.entries[key];
|
||||||
|
|
||||||
|
if (entry.type === 'blob') {
|
||||||
|
acc.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [])
|
||||||
|
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
|
||||||
|
|
||||||
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
|
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
|
||||||
|
|
||||||
|
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||||
|
export default () => {};
|
||||||
|
|
|
@ -196,3 +196,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
|
||||||
commit(types.UPDATE_LOADING, false);
|
commit(types.UPDATE_LOADING, false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||||
|
export default () => {};
|
||||||
|
|
|
@ -27,3 +27,6 @@ export const branchName = (state, getters, rootState) => {
|
||||||
|
|
||||||
return rootState.currentBranchId;
|
return rootState.currentBranchId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||||
|
export default () => {};
|
||||||
|
|
|
@ -60,3 +60,4 @@ export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
|
||||||
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
|
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
|
||||||
|
|
||||||
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
|
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
|
||||||
|
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
|
||||||
|
|
|
@ -107,6 +107,11 @@ export default {
|
||||||
delayViewerUpdated,
|
delayViewerUpdated,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
|
||||||
|
Object.assign(state, {
|
||||||
|
fileFindVisible,
|
||||||
|
});
|
||||||
|
},
|
||||||
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
|
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
|
||||||
const changedFile = state.changedFiles.find(f => f.path === file.path);
|
const changedFile = state.changedFiles.find(f => f.path === file.path);
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ export default {
|
||||||
[types.SET_FILE_ACTIVE](state, { path, active }) {
|
[types.SET_FILE_ACTIVE](state, { path, active }) {
|
||||||
Object.assign(state.entries[path], {
|
Object.assign(state.entries[path], {
|
||||||
active,
|
active,
|
||||||
|
lastOpenedAt: new Date().getTime(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (active && !state.entries[path].pending) {
|
if (active && !state.entries[path].pending) {
|
||||||
|
|
|
@ -18,4 +18,5 @@ export default () => ({
|
||||||
entries: {},
|
entries: {},
|
||||||
viewer: 'editor',
|
viewer: 'editor',
|
||||||
delayViewerUpdated: false,
|
delayViewerUpdated: false,
|
||||||
|
fileFindVisible: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,6 +43,7 @@ export const dataStructure = () => ({
|
||||||
viewMode: 'edit',
|
viewMode: 'edit',
|
||||||
previewMode: null,
|
previewMode: null,
|
||||||
size: 0,
|
size: 0,
|
||||||
|
lastOpenedAt: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const decorateData = entity => {
|
export const decorateData = entity => {
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default {
|
||||||
return timeIntervalInWords(this.job.queued);
|
return timeIntervalInWords(this.job.queued);
|
||||||
},
|
},
|
||||||
runnerId() {
|
runnerId() {
|
||||||
return `#${this.job.runner.id}`;
|
return `${this.job.runner.description} (#${this.job.runner.id})`;
|
||||||
},
|
},
|
||||||
retryButtonClass() {
|
retryButtonClass() {
|
||||||
let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
|
let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const UP_KEY_CODE = 38;
|
||||||
|
export const DOWN_KEY_CODE = 40;
|
||||||
|
export const ENTER_KEY_CODE = 13;
|
||||||
|
export const ESC_KEY_CODE = 27;
|
|
@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => {
|
||||||
scrollToElement(el);
|
scrollToElement(el);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||||
|
export default () => {};
|
||||||
|
|
|
@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => {
|
||||||
|
|
||||||
return Object.keys(resolvedMap).length;
|
return Object.keys(resolvedMap).length;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||||
|
export default () => {};
|
||||||
|
|
|
@ -32,26 +32,38 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
buttonDisabled: {
|
requestFinishedFor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isDisabled: false,
|
||||||
|
linkRequested: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
cssClass() {
|
cssClass() {
|
||||||
const actionIconDash = dasherize(this.actionIcon);
|
const actionIconDash = dasherize(this.actionIcon);
|
||||||
return `${actionIconDash} js-icon-${actionIconDash}`;
|
return `${actionIconDash} js-icon-${actionIconDash}`;
|
||||||
},
|
},
|
||||||
isDisabled() {
|
},
|
||||||
return this.buttonDisabled === this.link;
|
watch: {
|
||||||
|
requestFinishedFor() {
|
||||||
|
if (this.requestFinishedFor === this.linkRequested) {
|
||||||
|
this.isDisabled = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onClickAction() {
|
onClickAction() {
|
||||||
$(this.$el).tooltip('hide');
|
$(this.$el).tooltip('hide');
|
||||||
eventHub.$emit('graphAction', this.link);
|
eventHub.$emit('graphAction', this.link);
|
||||||
|
this.linkRequested = this.link;
|
||||||
|
this.isDisabled = true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -62,7 +74,8 @@ export default {
|
||||||
@click="onClickAction"
|
@click="onClickAction"
|
||||||
v-tooltip
|
v-tooltip
|
||||||
:title="tooltipText"
|
:title="tooltipText"
|
||||||
class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
|
class="js-ci-action btn btn-blank
|
||||||
|
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
|
||||||
:class="cssClass"
|
:class="cssClass"
|
||||||
data-container="body"
|
data-container="body"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
<script>
|
|
||||||
import icon from '../../../vue_shared/components/icon.vue';
|
|
||||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders either a cancel, retry or play icon pointing to the given path.
|
|
||||||
* TODO: Remove UJS from here and use an async request instead.
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
icon,
|
|
||||||
},
|
|
||||||
|
|
||||||
directives: {
|
|
||||||
tooltip,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
tooltipText: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
link: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
actionMethod: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
actionIcon: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<a
|
|
||||||
v-tooltip
|
|
||||||
:data-method="actionMethod"
|
|
||||||
:title="tooltipText"
|
|
||||||
:href="link"
|
|
||||||
rel="nofollow"
|
|
||||||
class="ci-action-icon-wrapper js-ci-status-icon"
|
|
||||||
data-container="body"
|
|
||||||
aria-label="Job's action"
|
|
||||||
>
|
|
||||||
<icon :name="actionIcon" />
|
|
||||||
</a>
|
|
||||||
</template>
|
|
|
@ -1,77 +1,83 @@
|
||||||
<script>
|
<script>
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import jobNameComponent from './job_name_component.vue';
|
import JobNameComponent from './job_name_component.vue';
|
||||||
import jobComponent from './job_component.vue';
|
import JobComponent from './job_component.vue';
|
||||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the dropdown for the pipeline graph.
|
* Renders the dropdown for the pipeline graph.
|
||||||
*
|
*
|
||||||
* The following object should be provided as `job`:
|
* The following object should be provided as `job`:
|
||||||
*
|
*
|
||||||
* {
|
* {
|
||||||
* "id": 4256,
|
* "id": 4256,
|
||||||
* "name": "test",
|
* "name": "test",
|
||||||
* "status": {
|
* "status": {
|
||||||
* "icon": "icon_status_success",
|
* "icon": "icon_status_success",
|
||||||
* "text": "passed",
|
* "text": "passed",
|
||||||
* "label": "passed",
|
* "label": "passed",
|
||||||
* "group": "success",
|
* "group": "success",
|
||||||
* "details_path": "/root/ci-mock/builds/4256",
|
* "details_path": "/root/ci-mock/builds/4256",
|
||||||
* "action": {
|
* "action": {
|
||||||
* "icon": "retry",
|
* "icon": "retry",
|
||||||
* "title": "Retry",
|
* "title": "Retry",
|
||||||
* "path": "/root/ci-mock/builds/4256/retry",
|
* "path": "/root/ci-mock/builds/4256/retry",
|
||||||
* "method": "post"
|
* "method": "post"
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
directives: {
|
directives: {
|
||||||
tooltip,
|
tooltip,
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
JobComponent,
|
||||||
|
JobNameComponent,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
job: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
|
requestFinishedFor: {
|
||||||
components: {
|
type: String,
|
||||||
jobComponent,
|
required: false,
|
||||||
jobNameComponent,
|
default: '',
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
computed: {
|
||||||
job: {
|
tooltipText() {
|
||||||
type: Object,
|
return `${this.job.name} - ${this.job.status.label}`;
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
mounted() {
|
||||||
tooltipText() {
|
this.stopDropdownClickPropagation();
|
||||||
return `${this.job.name} - ${this.job.status.label}`;
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
methods: {
|
||||||
this.stopDropdownClickPropagation();
|
/**
|
||||||
},
|
* When the user right clicks or cmd/ctrl + click in the job name or the action icon
|
||||||
|
* the dropdown should not be closed so we stop propagation
|
||||||
methods: {
|
* of the click event inside the dropdown.
|
||||||
/**
|
|
||||||
* When the user right clicks or cmd/ctrl + click in the job name
|
|
||||||
* the dropdown should not be closed and the link should open in another tab,
|
|
||||||
* so we stop propagation of the click event inside the dropdown.
|
|
||||||
*
|
*
|
||||||
* Since this component is rendered multiple times per page we need to guarantee we only
|
* Since this component is rendered multiple times per page we need to guarantee we only
|
||||||
* target the click event of this component.
|
* target the click event of this component.
|
||||||
*/
|
*/
|
||||||
stopDropdownClickPropagation() {
|
stopDropdownClickPropagation() {
|
||||||
$(this.$el
|
$(
|
||||||
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
|
'.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
|
||||||
.on('click', (e) => {
|
this.$el,
|
||||||
e.stopPropagation();
|
).on('click', e => {
|
||||||
});
|
e.stopPropagation();
|
||||||
},
|
});
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="ci-job-dropdown-container">
|
<div class="ci-job-dropdown-container">
|
||||||
|
@ -101,8 +107,8 @@
|
||||||
:key="i">
|
:key="i">
|
||||||
<job-component
|
<job-component
|
||||||
:job="item"
|
:job="item"
|
||||||
:is-dropdown="true"
|
|
||||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||||
|
:request-finished-for="requestFinishedFor"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -7,7 +7,6 @@ export default {
|
||||||
StageColumnComponent,
|
StageColumnComponent,
|
||||||
LoadingIcon,
|
LoadingIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
isLoading: {
|
isLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -17,10 +16,10 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
actionDisabled: {
|
requestFinishedFor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -75,7 +74,7 @@ export default {
|
||||||
:key="stage.name"
|
:key="stage.name"
|
||||||
:stage-connector-class="stageConnectorClass(index, stage)"
|
:stage-connector-class="stageConnectorClass(index, stage)"
|
||||||
:is-first-column="isFirstColumn(index)"
|
:is-first-column="isFirstColumn(index)"
|
||||||
:action-disabled="actionDisabled"
|
:request-finished-for="requestFinishedFor"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import ActionComponent from './action_component.vue';
|
import ActionComponent from './action_component.vue';
|
||||||
import DropdownActionComponent from './dropdown_action_component.vue';
|
|
||||||
import JobNameComponent from './job_name_component.vue';
|
import JobNameComponent from './job_name_component.vue';
|
||||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||||
|
|
||||||
|
@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ActionComponent,
|
ActionComponent,
|
||||||
DropdownActionComponent,
|
|
||||||
JobNameComponent,
|
JobNameComponent,
|
||||||
},
|
},
|
||||||
|
|
||||||
directives: {
|
directives: {
|
||||||
tooltip,
|
tooltip,
|
||||||
},
|
},
|
||||||
|
@ -44,26 +41,17 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
cssClassJobName: {
|
cssClassJobName: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
requestFinishedFor: {
|
||||||
isDropdown: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
actionDisabled: {
|
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
status() {
|
status() {
|
||||||
return this.job && this.job.status ? this.job.status : {};
|
return this.job && this.job.status ? this.job.status : {};
|
||||||
|
@ -134,19 +122,11 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<action-component
|
<action-component
|
||||||
v-if="hasAction && !isDropdown"
|
v-if="hasAction"
|
||||||
:tooltip-text="status.action.title"
|
:tooltip-text="status.action.title"
|
||||||
:link="status.action.path"
|
:link="status.action.path"
|
||||||
:action-icon="status.action.icon"
|
:action-icon="status.action.icon"
|
||||||
:button-disabled="actionDisabled"
|
:request-finished-for="requestFinishedFor"
|
||||||
/>
|
|
||||||
|
|
||||||
<dropdown-action-component
|
|
||||||
v-if="hasAction && isDropdown"
|
|
||||||
:tooltip-text="status.action.title"
|
|
||||||
:link="status.action.path"
|
|
||||||
:action-icon="status.action.icon"
|
|
||||||
:action-method="status.action.method"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -29,10 +29,11 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
actionDisabled: {
|
|
||||||
|
requestFinishedFor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -74,12 +75,12 @@ export default {
|
||||||
v-if="job.size === 1"
|
v-if="job.size === 1"
|
||||||
:job="job"
|
:job="job"
|
||||||
css-class-job-name="build-content"
|
css-class-job-name="build-content"
|
||||||
:action-disabled="actionDisabled"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<dropdown-job-component
|
<dropdown-job-component
|
||||||
v-if="job.size > 1"
|
v-if="job.size > 1"
|
||||||
:job="job"
|
:job="job"
|
||||||
|
:request-finished-for="requestFinishedFor"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default () => {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
mediator,
|
mediator,
|
||||||
actionDisabled: null,
|
requestFinishedFor: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -36,15 +36,17 @@ export default () => {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
postAction(action) {
|
postAction(action) {
|
||||||
this.actionDisabled = action;
|
// Click was made, reset this variable
|
||||||
|
this.requestFinishedFor = null;
|
||||||
|
|
||||||
this.mediator.service.postAction(action)
|
this.mediator.service
|
||||||
|
.postAction(action)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.mediator.refreshPipeline();
|
this.mediator.refreshPipeline();
|
||||||
this.actionDisabled = null;
|
this.requestFinishedFor = action;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.actionDisabled = null;
|
this.requestFinishedFor = action;
|
||||||
Flash(__('An error occurred while making the request.'));
|
Flash(__('An error occurred while making the request.'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -54,7 +56,7 @@ export default () => {
|
||||||
props: {
|
props: {
|
||||||
isLoading: this.mediator.state.isLoading,
|
isLoading: this.mediator.state.isLoading,
|
||||||
pipeline: this.mediator.store.state.pipeline,
|
pipeline: this.mediator.store.state.pipeline,
|
||||||
actionDisabled: this.actionDisabled,
|
requestFinishedFor: this.requestFinishedFor,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -79,7 +81,8 @@ export default () => {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
postAction(action) {
|
postAction(action) {
|
||||||
this.mediator.service.postAction(action.path)
|
this.mediator.service
|
||||||
|
.postAction(action.path)
|
||||||
.then(() => this.mediator.refreshPipeline())
|
.then(() => this.mediator.refreshPipeline())
|
||||||
.catch(() => Flash(__('An error occurred while making the request.')));
|
.catch(() => Flash(__('An error occurred while making the request.')));
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr
|
||||||
|
|
||||||
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
|
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
|
||||||
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
|
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
|
||||||
|
|
||||||
|
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||||
|
export default () => {};
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
export const isLoading = state => state.isLoading;
|
export const isLoading = state => state.isLoading;
|
||||||
export const repos = state => state.repos;
|
export const repos = state => state.repos;
|
||||||
|
|
||||||
|
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||||
|
export default () => {};
|
||||||
|
|
|
@ -1,52 +1,52 @@
|
||||||
<script>
|
<script>
|
||||||
import ciIcon from './ci_icon.vue';
|
import CiIcon from './ci_icon.vue';
|
||||||
import tooltip from '../directives/tooltip';
|
import tooltip from '../directives/tooltip';
|
||||||
/**
|
/**
|
||||||
* Renders CI Badge link with CI icon and status text based on
|
* Renders CI Badge link with CI icon and status text based on
|
||||||
* API response shared between all places where it is used.
|
* API response shared between all places where it is used.
|
||||||
*
|
*
|
||||||
* Receives status object containing:
|
* Receives status object containing:
|
||||||
* status: {
|
* status: {
|
||||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||||
* group:"running" // used for CSS class
|
* group:"running" // used for CSS class
|
||||||
* icon: "icon_status_running" // used to render the icon
|
* icon: "icon_status_running" // used to render the icon
|
||||||
* label:"running" // used for potential tooltip
|
* label:"running" // used for potential tooltip
|
||||||
* text:"running" // text rendered
|
* text:"running" // text rendered
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Used in:
|
* Used in:
|
||||||
* - Pipelines table - first column
|
* - Pipelines table - first column
|
||||||
* - Jobs table - first column
|
* - Jobs table - first column
|
||||||
* - Pipeline show view - header
|
* - Pipeline show view - header
|
||||||
* - Job show view - header
|
* - Job show view - header
|
||||||
* - MR widget
|
* - MR widget
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ciIcon,
|
CiIcon,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
tooltip,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
status: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
directives: {
|
showText: {
|
||||||
tooltip,
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
},
|
},
|
||||||
props: {
|
},
|
||||||
status: {
|
computed: {
|
||||||
type: Object,
|
cssClass() {
|
||||||
required: true,
|
const className = this.status.group;
|
||||||
},
|
return className ? `ci-status ci-${className}` : 'ci-status';
|
||||||
showText: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
},
|
||||||
cssClass() {
|
};
|
||||||
const className = this.status.group;
|
|
||||||
return className ? `ci-status ci-${className}` : 'ci-status';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -1,45 +1,44 @@
|
||||||
<script>
|
<script>
|
||||||
import icon from '../../vue_shared/components/icon.vue';
|
import Icon from '../../vue_shared/components/icon.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders CI icon based on API response shared between all places where it is used.
|
* Renders CI icon based on API response shared between all places where it is used.
|
||||||
*
|
*
|
||||||
* Receives status object containing:
|
* Receives status object containing:
|
||||||
* status: {
|
* status: {
|
||||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||||
* group:"running" // used for CSS class
|
* group:"running" // used for CSS class
|
||||||
* icon: "icon_status_running" // used to render the icon
|
* icon: "icon_status_running" // used to render the icon
|
||||||
* label:"running" // used for potential tooltip
|
* label:"running" // used for potential tooltip
|
||||||
* text:"running" // text rendered
|
* text:"running" // text rendered
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
* Used in:
|
* Used in:
|
||||||
* - Pipelines table Badge
|
* - Pipelines table Badge
|
||||||
* - Pipelines table mini graph
|
* - Pipelines table mini graph
|
||||||
* - Pipeline graph
|
* - Pipeline graph
|
||||||
* - Pipeline show view badge
|
* - Pipeline show view badge
|
||||||
* - Jobs table
|
* - Jobs table
|
||||||
* - Jobs show view header
|
* - Jobs show view header
|
||||||
* - Jobs show view sidebar
|
* - Jobs show view sidebar
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
icon,
|
Icon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
status: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
props: {
|
},
|
||||||
status: {
|
computed: {
|
||||||
type: Object,
|
cssClass() {
|
||||||
required: true,
|
const status = this.status.group;
|
||||||
},
|
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
|
||||||
},
|
},
|
||||||
|
},
|
||||||
computed: {
|
};
|
||||||
cssClass() {
|
|
||||||
const status = this.status.group;
|
|
||||||
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<span :class="cssClass">
|
<span :class="cssClass">
|
||||||
|
|
|
@ -1,40 +1,50 @@
|
||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* Falls back to the code used in `copy_to_clipboard.js`
|
* Falls back to the code used in `copy_to_clipboard.js`
|
||||||
*/
|
*
|
||||||
import tooltip from '../directives/tooltip';
|
* Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
|
||||||
|
* when clicked.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* <clipboard-button
|
||||||
|
* title="Copy to clipbard"
|
||||||
|
* text="Content to be copied"
|
||||||
|
* css-class="btn-transparent"
|
||||||
|
* />
|
||||||
|
*/
|
||||||
|
import tooltip from '../directives/tooltip';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ClipboardButton',
|
name: 'ClipboardButton',
|
||||||
directives: {
|
directives: {
|
||||||
tooltip,
|
tooltip,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
props: {
|
title: {
|
||||||
text: {
|
type: String,
|
||||||
type: String,
|
required: true,
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
tooltipPlacement: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: 'top',
|
|
||||||
},
|
|
||||||
tooltipContainer: {
|
|
||||||
type: [String, Boolean],
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
cssClass: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: 'btn-default',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
tooltipPlacement: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'top',
|
||||||
|
},
|
||||||
|
tooltipContainer: {
|
||||||
|
type: [String, Boolean],
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
cssClass: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'btn-default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -1,119 +1,111 @@
|
||||||
<script>
|
<script>
|
||||||
import commitIconSvg from 'icons/_icon_commit.svg';
|
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
|
||||||
import userAvatarLink from './user_avatar/user_avatar_link.vue';
|
import tooltip from '../directives/tooltip';
|
||||||
import tooltip from '../directives/tooltip';
|
import Icon from '../../vue_shared/components/icon.vue';
|
||||||
import icon from '../../vue_shared/components/icon.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
directives: {
|
directives: {
|
||||||
tooltip,
|
tooltip,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
UserAvatarLink,
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Indicates the existance of a tag.
|
||||||
|
* Used to render the correct icon, if true will render `fa-tag` icon,
|
||||||
|
* if false will render a svg sprite fork icon
|
||||||
|
*/
|
||||||
|
tag: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
components: {
|
/**
|
||||||
userAvatarLink,
|
* If provided is used to render the branch name and url.
|
||||||
icon,
|
* Should contain the following properties:
|
||||||
|
* name
|
||||||
|
* ref_url
|
||||||
|
*/
|
||||||
|
commitRef: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Used to link to the commit sha.
|
||||||
|
*/
|
||||||
|
commitUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
props: {
|
|
||||||
/**
|
|
||||||
* Indicates the existance of a tag.
|
|
||||||
* Used to render the correct icon, if true will render `fa-tag` icon,
|
|
||||||
* if false will render a svg sprite fork icon
|
|
||||||
*/
|
|
||||||
tag: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* If provided is used to render the branch name and url.
|
|
||||||
* Should contain the following properties:
|
|
||||||
* name
|
|
||||||
* ref_url
|
|
||||||
*/
|
|
||||||
commitRef: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Used to link to the commit sha.
|
|
||||||
*/
|
|
||||||
commitUrl: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to show the commit short sha that links to the commit url.
|
* Used to show the commit short sha that links to the commit url.
|
||||||
*/
|
*/
|
||||||
shortSha: {
|
shortSha: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
|
||||||
/**
|
|
||||||
* If provided shows the commit tile.
|
|
||||||
*/
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* If provided renders information about the author of the commit.
|
|
||||||
* When provided should include:
|
|
||||||
* `avatar_url` to render the avatar icon
|
|
||||||
* `web_url` to link to user profile
|
|
||||||
* `username` to render alt and title tags
|
|
||||||
*/
|
|
||||||
author: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
showBranch: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
/**
|
||||||
/**
|
* If provided shows the commit tile.
|
||||||
* Used to verify if all the properties needed to render the commit
|
*/
|
||||||
* ref section were provided.
|
title: {
|
||||||
*
|
type: String,
|
||||||
* @returns {Boolean}
|
required: false,
|
||||||
*/
|
default: '',
|
||||||
hasCommitRef() {
|
|
||||||
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Used to verify if all the properties needed to render the commit
|
|
||||||
* author section were provided.
|
|
||||||
*
|
|
||||||
* @returns {Boolean}
|
|
||||||
*/
|
|
||||||
hasAuthor() {
|
|
||||||
return this.author &&
|
|
||||||
this.author.avatar_url &&
|
|
||||||
this.author.path &&
|
|
||||||
this.author.username;
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* If information about the author is provided will return a string
|
|
||||||
* to be rendered as the alt attribute of the img tag.
|
|
||||||
*
|
|
||||||
* @returns {String}
|
|
||||||
*/
|
|
||||||
userImageAltDescription() {
|
|
||||||
return this.author &&
|
|
||||||
this.author.username ? `${this.author.username}'s avatar` : null;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
created() {
|
/**
|
||||||
this.commitIconSvg = commitIconSvg;
|
* If provided renders information about the author of the commit.
|
||||||
|
* When provided should include:
|
||||||
|
* `avatar_url` to render the avatar icon
|
||||||
|
* `web_url` to link to user profile
|
||||||
|
* `username` to render alt and title tags
|
||||||
|
*/
|
||||||
|
author: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
};
|
showBranch: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Used to verify if all the properties needed to render the commit
|
||||||
|
* ref section were provided.
|
||||||
|
*
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
hasCommitRef() {
|
||||||
|
return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Used to verify if all the properties needed to render the commit
|
||||||
|
* author section were provided.
|
||||||
|
*
|
||||||
|
* @returns {Boolean}
|
||||||
|
*/
|
||||||
|
hasAuthor() {
|
||||||
|
return this.author && this.author.avatar_url && this.author.path && this.author.username;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* If information about the author is provided will return a string
|
||||||
|
* to be rendered as the alt attribute of the img tag.
|
||||||
|
*
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
userImageAltDescription() {
|
||||||
|
return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="branch-commit">
|
<div class="branch-commit">
|
||||||
|
@ -141,11 +133,10 @@
|
||||||
{{ commitRef.name }}
|
{{ commitRef.name }}
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<div
|
<icon
|
||||||
v-html="commitIconSvg"
|
name="commit"
|
||||||
class="commit-icon js-commit-icon"
|
class="commit-icon js-commit-icon"
|
||||||
>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="commit-sha"
|
class="commit-sha"
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
<script>
|
<script>
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
/**
|
/**
|
||||||
* Port of detail_behavior expand button.
|
* Port of detail_behavior expand button.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* <expand-button>
|
* <expand-button>
|
||||||
* <template slot="expanded">
|
* <template slot="expanded">
|
||||||
* Text goes here.
|
* Text goes here.
|
||||||
* </template>
|
* </template>
|
||||||
* </expand-button>
|
* </expand-button>
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
name: 'ExpandButton',
|
name: 'ExpandButton',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isCollapsed: true,
|
isCollapsed: true,
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
ariaLabel() {
|
||||||
|
return __('Click to expand text');
|
||||||
},
|
},
|
||||||
computed: {
|
},
|
||||||
ariaLabel() {
|
methods: {
|
||||||
return __('Click to expand text');
|
onClick() {
|
||||||
},
|
this.isCollapsed = !this.isCollapsed;
|
||||||
},
|
},
|
||||||
methods: {
|
},
|
||||||
onClick() {
|
};
|
||||||
this.isCollapsed = !this.isCollapsed;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
|
|
|
@ -1,78 +1,78 @@
|
||||||
<script>
|
<script>
|
||||||
import ciIconBadge from './ci_badge_link.vue';
|
import CiIconBadge from './ci_badge_link.vue';
|
||||||
import loadingIcon from './loading_icon.vue';
|
import LoadingIcon from './loading_icon.vue';
|
||||||
import timeagoTooltip from './time_ago_tooltip.vue';
|
import TimeagoTooltip from './time_ago_tooltip.vue';
|
||||||
import tooltip from '../directives/tooltip';
|
import tooltip from '../directives/tooltip';
|
||||||
import userAvatarImage from './user_avatar/user_avatar_image.vue';
|
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders header component for job and pipeline page based on UI mockups
|
* Renders header component for job and pipeline page based on UI mockups
|
||||||
*
|
*
|
||||||
* Used in:
|
* Used in:
|
||||||
* - job show page
|
* - job show page
|
||||||
* - pipeline show page
|
* - pipeline show page
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ciIconBadge,
|
CiIconBadge,
|
||||||
loadingIcon,
|
LoadingIcon,
|
||||||
timeagoTooltip,
|
TimeagoTooltip,
|
||||||
userAvatarImage,
|
UserAvatarImage,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
tooltip,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
status: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
directives: {
|
itemName: {
|
||||||
tooltip,
|
type: String,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
props: {
|
itemId: {
|
||||||
status: {
|
type: Number,
|
||||||
type: Object,
|
required: true,
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
itemName: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
itemId: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
time: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
hasSidebarButton: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
shouldRenderTriggeredLabel: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
time: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
hasSidebarButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
shouldRenderTriggeredLabel: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
userAvatarAltText() {
|
userAvatarAltText() {
|
||||||
return `${this.user.name}'s avatar`;
|
return `${this.user.name}'s avatar`;
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onClickAction(action) {
|
onClickAction(action) {
|
||||||
this.$emit('actionClicked', action);
|
this.$emit('actionClicked', action);
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -1,76 +1,75 @@
|
||||||
<script>
|
<script>
|
||||||
|
/* This is a re-usable vue component for rendering a svg sprite
|
||||||
|
icon
|
||||||
|
|
||||||
/* This is a re-usable vue component for rendering a svg sprite
|
Sample configuration:
|
||||||
icon
|
|
||||||
|
|
||||||
Sample configuration:
|
<icon
|
||||||
|
name="retry"
|
||||||
|
:size="32"
|
||||||
|
css-classes="top"
|
||||||
|
/>
|
||||||
|
|
||||||
<icon
|
*/
|
||||||
name="retry"
|
// only allow classes in images.scss e.g. s12
|
||||||
:size="32"
|
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
|
||||||
css-classes="top"
|
|
||||||
/>
|
|
||||||
|
|
||||||
*/
|
export default {
|
||||||
// only allow classes in images.scss e.g. s12
|
props: {
|
||||||
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
|
||||||
export default {
|
size: {
|
||||||
props: {
|
type: Number,
|
||||||
name: {
|
required: false,
|
||||||
type: String,
|
default: 16,
|
||||||
required: true,
|
validator(value) {
|
||||||
},
|
return validSizes.includes(value);
|
||||||
|
|
||||||
size: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: 16,
|
|
||||||
validator(value) {
|
|
||||||
return validSizes.includes(value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
cssClasses: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
|
|
||||||
width: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
y: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
x: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
cssClasses: {
|
||||||
spriteHref() {
|
type: String,
|
||||||
return `${gon.sprite_icons}#${this.name}`;
|
required: false,
|
||||||
},
|
default: '',
|
||||||
iconSizeClass() {
|
|
||||||
return this.size ? `s${this.size}` : '';
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
y: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
x: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
spriteHref() {
|
||||||
|
return `${gon.sprite_icons}#${this.name}`;
|
||||||
|
},
|
||||||
|
iconSizeClass() {
|
||||||
|
return this.size ? `s${this.size}` : '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -79,7 +78,8 @@
|
||||||
:width="width"
|
:width="width"
|
||||||
:height="height"
|
:height="height"
|
||||||
:x="x"
|
:x="x"
|
||||||
:y="y">
|
:y="y"
|
||||||
|
>
|
||||||
<use v-bind="{ 'xlink:href':spriteHref }" />
|
<use v-bind="{ 'xlink:href':spriteHref }" />
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import $ from 'jquery';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import LabelsSelect from '~/labels_select';
|
import LabelsSelect from '~/labels_select';
|
||||||
import LoadingIcon from '../../loading_icon.vue';
|
import LoadingIcon from '../../loading_icon.vue';
|
||||||
|
@ -98,11 +99,18 @@ export default {
|
||||||
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
|
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
|
||||||
handleClick: this.handleClick,
|
handleClick: this.handleClick,
|
||||||
});
|
});
|
||||||
|
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleClick(label) {
|
handleClick(label) {
|
||||||
this.$emit('onLabelClick', label);
|
this.$emit('onLabelClick', label);
|
||||||
},
|
},
|
||||||
|
handleCollapsedValueClick() {
|
||||||
|
this.$emit('toggleCollapse');
|
||||||
|
},
|
||||||
|
handleDropdownHidden() {
|
||||||
|
this.$emit('onDropdownClose');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -112,6 +120,7 @@ export default {
|
||||||
<dropdown-value-collapsed
|
<dropdown-value-collapsed
|
||||||
v-if="showCreate"
|
v-if="showCreate"
|
||||||
:labels="context.labels"
|
:labels="context.labels"
|
||||||
|
@onValueClick="handleCollapsedValueClick"
|
||||||
/>
|
/>
|
||||||
<dropdown-title
|
<dropdown-title
|
||||||
:can-edit="canEdit"
|
:can-edit="canEdit"
|
||||||
|
@ -133,7 +142,10 @@ export default {
|
||||||
:name="hiddenInputName"
|
:name="hiddenInputName"
|
||||||
:label="label"
|
:label="label"
|
||||||
/>
|
/>
|
||||||
<div class="dropdown">
|
<div
|
||||||
|
class="dropdown"
|
||||||
|
ref="dropdown"
|
||||||
|
>
|
||||||
<dropdown-button
|
<dropdown-button
|
||||||
:ability-name="abilityName"
|
:ability-name="abilityName"
|
||||||
:field-name="hiddenInputName"
|
:field-name="hiddenInputName"
|
||||||
|
|
|
@ -26,6 +26,11 @@ export default {
|
||||||
return labelsString;
|
return labelsString;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
handleClick() {
|
||||||
|
this.$emit('onValueClick');
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -36,6 +41,7 @@ export default {
|
||||||
data-placement="left"
|
data-placement="left"
|
||||||
data-container="body"
|
data-container="body"
|
||||||
:title="labelsList"
|
:title="labelsList"
|
||||||
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|
|
@ -472,6 +472,7 @@ img.emoji {
|
||||||
.append-right-20 { margin-right: 20px; }
|
.append-right-20 { margin-right: 20px; }
|
||||||
.append-bottom-0 { margin-bottom: 0; }
|
.append-bottom-0 { margin-bottom: 0; }
|
||||||
.append-bottom-5 { margin-bottom: 5px; }
|
.append-bottom-5 { margin-bottom: 5px; }
|
||||||
|
.append-bottom-8 { margin-bottom: $grid-size; }
|
||||||
.append-bottom-10 { margin-bottom: 10px; }
|
.append-bottom-10 { margin-bottom: 10px; }
|
||||||
.append-bottom-15 { margin-bottom: 15px; }
|
.append-bottom-15 { margin-bottom: 15px; }
|
||||||
.append-bottom-20 { margin-bottom: 20px; }
|
.append-bottom-20 { margin-bottom: 20px; }
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
border-color: $gray-darkest;
|
border-color: $gray-darkest;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-toggle="dropdown"] {
|
[data-toggle='dropdown'] {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -172,7 +172,11 @@
|
||||||
color: $brand-danger;
|
color: $brand-danger;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover,
|
&.disable-hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.disable-hover):hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&.is-focused {
|
&.is-focused {
|
||||||
|
@ -508,17 +512,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-indeterminate::before {
|
&.is-indeterminate::before {
|
||||||
content: "\f068";
|
content: '\f068';
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-active::before {
|
&.is-active::before {
|
||||||
content: "\f00c";
|
content: '\f00c';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dropdown-title {
|
.dropdown-title {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 2px 25px 10px;
|
padding: 2px 25px 10px;
|
||||||
|
@ -724,7 +727,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.dropdown-menu-due-date {
|
.dropdown-menu-due-date {
|
||||||
.dropdown-content {
|
.dropdown-content {
|
||||||
max-height: 230px;
|
max-height: 230px;
|
||||||
|
@ -854,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-list-frequent-container,
|
.projects-list-frequent-container,
|
||||||
.projects-list-search-container, {
|
.projects-list-search-container {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
li.section-empty.section-failure {
|
||||||
|
color: $callout-danger-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header,
|
.section-header,
|
||||||
|
@ -867,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
|
||||||
font-size: $gl-font-size;
|
font-size: $gl-font-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-list-frequent-container,
|
|
||||||
.projects-list-search-container {
|
|
||||||
li.section-empty.section-failure {
|
|
||||||
color: $callout-danger-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-container {
|
.search-input-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 4px $gl-padding;
|
padding: 4px $gl-padding;
|
||||||
|
@ -905,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-list-item-container {
|
.projects-list-item-container {
|
||||||
.project-item-avatar-container
|
.project-item-avatar-container .project-item-metadata-container {
|
||||||
.project-item-metadata-container {
|
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,10 +40,6 @@
|
||||||
.project-home-panel {
|
.project-home-panel {
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
|
|
||||||
.project-avatar {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-repo-buttons,
|
.project-repo-buttons,
|
||||||
.git-clone-holder {
|
.git-clone-holder {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -468,6 +468,14 @@
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
|
||||||
|
.ci-job-dropdown-container {
|
||||||
|
// override dropdown.scss
|
||||||
|
.dropdown-menu li button {
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ensure .build-content has hover style when action-icon is hovered
|
// ensure .build-content has hover style when action-icon is hovered
|
||||||
.ci-job-dropdown-container:hover .build-content {
|
.ci-job-dropdown-container:hover .build-content {
|
||||||
@extend .build-content:hover;
|
@extend .build-content:hover;
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ide-view {
|
.ide-view {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100vh - #{$header-height});
|
height: calc(100vh - #{$header-height});
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
@ -880,6 +881,26 @@
|
||||||
font-weight: $gl-font-weight-bold;
|
font-weight: $gl-font-weight-bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ide-file-finder-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ide-file-finder {
|
||||||
|
top: 10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
.highlighted {
|
||||||
|
color: $blue-500;
|
||||||
|
font-weight: $gl-font-weight-bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ide-commit-message-field {
|
.ide-commit-message-field {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
background-color: $white-light;
|
background-color: $white-light;
|
||||||
|
|
|
@ -23,6 +23,9 @@ module AuthenticatesWithTwoFactor
|
||||||
#
|
#
|
||||||
# Returns nil
|
# Returns nil
|
||||||
def prompt_for_two_factor(user)
|
def prompt_for_two_factor(user)
|
||||||
|
# Set @user for Devise views
|
||||||
|
@user = user # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||||
|
|
||||||
return locked_user_redirect(user) unless user.can?(:log_in)
|
return locked_user_redirect(user) unless user.can?(:log_in)
|
||||||
|
|
||||||
session[:otp_user_id] = user.id
|
session[:otp_user_id] = user.id
|
||||||
|
|
|
@ -165,8 +165,8 @@ module IssuableCollections
|
||||||
[:project, :author, :assignees, :labels, :milestone, project: :namespace]
|
[:project, :author, :assignees, :labels, :milestone, project: :namespace]
|
||||||
when 'MergeRequest'
|
when 'MergeRequest'
|
||||||
[
|
[
|
||||||
:source_project, :target_project, :author, :assignee, :labels, :milestone,
|
:target_project, :author, :assignee, :labels, :milestone,
|
||||||
head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
|
source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
|
||||||
|
extend ::Gitlab::Utils::Override
|
||||||
|
|
||||||
|
def self.define_providers!
|
||||||
|
return unless Gitlab::Auth::LDAP::Config.enabled?
|
||||||
|
|
||||||
|
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
|
||||||
|
alias_method server['provider_name'], :ldap
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# We only find ourselves here
|
||||||
|
# if the authentication to LDAP was successful.
|
||||||
|
def ldap
|
||||||
|
sign_in_user_flow(Gitlab::Auth::LDAP::User)
|
||||||
|
end
|
||||||
|
|
||||||
|
define_providers!
|
||||||
|
|
||||||
|
override :set_remember_me
|
||||||
|
def set_remember_me(user)
|
||||||
|
user.remember_me = params[:remember_me] if user.persisted?
|
||||||
|
end
|
||||||
|
|
||||||
|
override :fail_login
|
||||||
|
def fail_login(user)
|
||||||
|
flash[:alert] = 'Access denied for your LDAP account.'
|
||||||
|
|
||||||
|
redirect_to new_user_session_path
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,18 +4,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
|
|
||||||
protect_from_forgery except: [:kerberos, :saml, :cas3]
|
protect_from_forgery except: [:kerberos, :saml, :cas3]
|
||||||
|
|
||||||
Gitlab.config.omniauth.providers.each do |provider|
|
def handle_omniauth
|
||||||
define_method provider['name'] do
|
omniauth_flow(Gitlab::Auth::OAuth)
|
||||||
handle_omniauth
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if Gitlab::Auth::LDAP::Config.enabled?
|
Gitlab.config.omniauth.providers.each do |provider|
|
||||||
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
|
alias_method provider['name'], :handle_omniauth
|
||||||
define_method server['provider_name'] do
|
|
||||||
ldap
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Extend the standard implementation to also increment
|
# Extend the standard implementation to also increment
|
||||||
|
@ -37,51 +31,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
error ||= exception.error if exception.respond_to?(:error)
|
error ||= exception.error if exception.respond_to?(:error)
|
||||||
error ||= exception.message if exception.respond_to?(:message)
|
error ||= exception.message if exception.respond_to?(:message)
|
||||||
error ||= env["omniauth.error.type"].to_s
|
error ||= env["omniauth.error.type"].to_s
|
||||||
|
|
||||||
error.to_s.humanize if error
|
error.to_s.humanize if error
|
||||||
end
|
end
|
||||||
|
|
||||||
# We only find ourselves here
|
|
||||||
# if the authentication to LDAP was successful.
|
|
||||||
def ldap
|
|
||||||
ldap_user = Gitlab::Auth::LDAP::User.new(oauth)
|
|
||||||
ldap_user.save if ldap_user.changed? # will also save new users
|
|
||||||
|
|
||||||
@user = ldap_user.gl_user
|
|
||||||
@user.remember_me = params[:remember_me] if ldap_user.persisted?
|
|
||||||
|
|
||||||
# Do additional LDAP checks for the user filter and EE features
|
|
||||||
if ldap_user.allowed?
|
|
||||||
if @user.two_factor_enabled?
|
|
||||||
prompt_for_two_factor(@user)
|
|
||||||
else
|
|
||||||
log_audit_event(@user, with: oauth['provider'])
|
|
||||||
sign_in_and_redirect(@user)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
fail_ldap_login
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def saml
|
def saml
|
||||||
if current_user
|
omniauth_flow(Gitlab::Auth::Saml)
|
||||||
log_audit_event(current_user, with: :saml)
|
|
||||||
# Update SAML identity if data has changed.
|
|
||||||
identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
|
|
||||||
if identity.nil?
|
|
||||||
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
|
|
||||||
redirect_to profile_account_path, notice: 'Authentication method updated'
|
|
||||||
else
|
|
||||||
redirect_to after_sign_in_path_for(current_user)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
saml_user = Gitlab::Auth::Saml::User.new(oauth)
|
|
||||||
saml_user.save if saml_user.changed?
|
|
||||||
@user = saml_user.gl_user
|
|
||||||
|
|
||||||
continue_login_process
|
|
||||||
end
|
|
||||||
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
|
|
||||||
handle_signup_error
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def omniauth_error
|
def omniauth_error
|
||||||
|
@ -117,25 +72,36 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def handle_omniauth
|
def omniauth_flow(auth_module, identity_linker: nil)
|
||||||
if current_user
|
if current_user
|
||||||
# Add new authentication method
|
|
||||||
current_user.identities
|
|
||||||
.with_extern_uid(oauth['provider'], oauth['uid'])
|
|
||||||
.first_or_create(extern_uid: oauth['uid'])
|
|
||||||
log_audit_event(current_user, with: oauth['provider'])
|
log_audit_event(current_user, with: oauth['provider'])
|
||||||
redirect_to profile_account_path, notice: 'Authentication method updated'
|
|
||||||
else
|
|
||||||
oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
|
|
||||||
oauth_user.save
|
|
||||||
@user = oauth_user.gl_user
|
|
||||||
|
|
||||||
continue_login_process
|
identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth)
|
||||||
|
|
||||||
|
identity_linker.link
|
||||||
|
|
||||||
|
if identity_linker.changed?
|
||||||
|
redirect_identity_linked
|
||||||
|
elsif identity_linker.error_message.present?
|
||||||
|
redirect_identity_link_failed(identity_linker.error_message)
|
||||||
|
else
|
||||||
|
redirect_identity_exists
|
||||||
|
end
|
||||||
|
else
|
||||||
|
sign_in_user_flow(auth_module::User)
|
||||||
end
|
end
|
||||||
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
|
end
|
||||||
handle_disabled_provider
|
|
||||||
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
|
def redirect_identity_exists
|
||||||
handle_signup_error
|
redirect_to after_sign_in_path_for(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_identity_link_failed(error_message)
|
||||||
|
redirect_to profile_account_path, notice: "Authentication failed: #{error_message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def redirect_identity_linked
|
||||||
|
redirect_to profile_account_path, notice: 'Authentication method updated'
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_service_ticket(provider, ticket)
|
def handle_service_ticket(provider, ticket)
|
||||||
|
@ -144,21 +110,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
session[:service_tickets][provider] = ticket
|
session[:service_tickets][provider] = ticket
|
||||||
end
|
end
|
||||||
|
|
||||||
def continue_login_process
|
def sign_in_user_flow(auth_user_class)
|
||||||
# Only allow properly saved users to login.
|
auth_user = auth_user_class.new(oauth)
|
||||||
if @user.persisted? && @user.valid?
|
user = auth_user.find_and_update!
|
||||||
log_audit_event(@user, with: oauth['provider'])
|
|
||||||
|
|
||||||
if @user.two_factor_enabled?
|
if auth_user.valid_sign_in?
|
||||||
params[:remember_me] = '1' if remember_me?
|
log_audit_event(user, with: oauth['provider'])
|
||||||
prompt_for_two_factor(@user)
|
|
||||||
|
set_remember_me(user)
|
||||||
|
|
||||||
|
if user.two_factor_enabled?
|
||||||
|
prompt_for_two_factor(user)
|
||||||
else
|
else
|
||||||
remember_me(@user) if remember_me?
|
sign_in_and_redirect(user)
|
||||||
sign_in_and_redirect(@user)
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
fail_login
|
fail_login(user)
|
||||||
end
|
end
|
||||||
|
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
|
||||||
|
handle_disabled_provider
|
||||||
|
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
|
||||||
|
handle_signup_error
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_signup_error
|
def handle_signup_error
|
||||||
|
@ -178,18 +150,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
@oauth ||= request.env['omniauth.auth']
|
@oauth ||= request.env['omniauth.auth']
|
||||||
end
|
end
|
||||||
|
|
||||||
def fail_login
|
def fail_login(user)
|
||||||
error_message = @user.errors.full_messages.to_sentence
|
error_message = user.errors.full_messages.to_sentence
|
||||||
|
|
||||||
return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
|
return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fail_ldap_login
|
|
||||||
flash[:alert] = 'Access denied for your LDAP account.'
|
|
||||||
|
|
||||||
redirect_to new_user_session_path
|
|
||||||
end
|
|
||||||
|
|
||||||
def fail_auth0_login
|
def fail_auth0_login
|
||||||
flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
|
flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
|
||||||
|
|
||||||
|
@ -208,6 +174,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||||
.for_authentication.security_event
|
.for_authentication.security_event
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_remember_me(user)
|
||||||
|
return unless remember_me?
|
||||||
|
|
||||||
|
if user.two_factor_enabled?
|
||||||
|
params[:remember_me] = '1'
|
||||||
|
else
|
||||||
|
remember_me(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def remember_me?
|
def remember_me?
|
||||||
request_params = request.env['omniauth.params']
|
request_params = request.env['omniauth.params']
|
||||||
(request_params['remember_me'] == '1') if request_params.present?
|
(request_params['remember_me'] == '1') if request_params.present?
|
||||||
|
|
|
@ -32,6 +32,7 @@ class UsersFinder
|
||||||
users = by_active(users)
|
users = by_active(users)
|
||||||
users = by_external_identity(users)
|
users = by_external_identity(users)
|
||||||
users = by_external(users)
|
users = by_external(users)
|
||||||
|
users = by_2fa(users)
|
||||||
users = by_created_at(users)
|
users = by_created_at(users)
|
||||||
users = by_custom_attributes(users)
|
users = by_custom_attributes(users)
|
||||||
|
|
||||||
|
@ -76,4 +77,15 @@ class UsersFinder
|
||||||
|
|
||||||
users.external
|
users.external
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def by_2fa(users)
|
||||||
|
case params[:two_factor]
|
||||||
|
when 'enabled'
|
||||||
|
users.with_two_factor
|
||||||
|
when 'disabled'
|
||||||
|
users.without_two_factor
|
||||||
|
else
|
||||||
|
users
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -81,6 +81,14 @@ module GitlabRoutingHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def edit_milestone_path(entity, *args)
|
||||||
|
if entity.parent.is_a?(Group)
|
||||||
|
edit_group_milestone_path(entity.parent, entity, *args)
|
||||||
|
else
|
||||||
|
edit_project_milestone_path(entity.parent, entity, *args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def toggle_subscription_path(entity, *args)
|
def toggle_subscription_path(entity, *args)
|
||||||
if entity.is_a?(Issue)
|
if entity.is_a?(Issue)
|
||||||
toggle_subscription_project_issue_path(entity.project, entity)
|
toggle_subscription_project_issue_path(entity.project, entity)
|
||||||
|
|
|
@ -27,6 +27,7 @@ module Ci
|
||||||
|
|
||||||
has_one :metadata, class_name: 'Ci::BuildMetadata'
|
has_one :metadata, class_name: 'Ci::BuildMetadata'
|
||||||
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
|
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
|
||||||
|
delegate :gitlab_deploy_token, to: :project
|
||||||
|
|
||||||
##
|
##
|
||||||
# The "environment" field for builds is a String, and is the unexpanded name!
|
# The "environment" field for builds is a String, and is the unexpanded name!
|
||||||
|
@ -604,6 +605,7 @@ module Ci
|
||||||
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
|
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
|
||||||
.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
|
.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
|
||||||
.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
|
.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
|
||||||
|
.concat(deploy_token_variables)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -654,6 +656,15 @@ module Ci
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def deploy_token_variables
|
||||||
|
Gitlab::Ci::Variables::Collection.new.tap do |variables|
|
||||||
|
break variables unless gitlab_deploy_token
|
||||||
|
|
||||||
|
variables.append(key: 'CI_DEPLOY_USER', value: gitlab_deploy_token.name)
|
||||||
|
variables.append(key: 'CI_DEPLOY_PASSWORD', value: gitlab_deploy_token.token, public: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def environment_url
|
def environment_url
|
||||||
options&.dig(:environment, :url) || persisted_environment&.external_url
|
options&.dig(:environment, :url) || persisted_environment&.external_url
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,12 +31,13 @@ module Avatarable
|
||||||
|
|
||||||
asset_host = ActionController::Base.asset_host
|
asset_host = ActionController::Base.asset_host
|
||||||
use_asset_host = asset_host.present?
|
use_asset_host = asset_host.present?
|
||||||
|
use_authentication = respond_to?(:public?) && !public?
|
||||||
|
|
||||||
# Avatars for private and internal groups and projects require authentication to be viewed,
|
# Avatars for private and internal groups and projects require authentication to be viewed,
|
||||||
# which means they can only be served by Rails, on the regular GitLab host.
|
# which means they can only be served by Rails, on the regular GitLab host.
|
||||||
# If an asset host is configured, we need to return the fully qualified URL
|
# If an asset host is configured, we need to return the fully qualified URL
|
||||||
# instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
|
# instead of only the avatar path, so that Rails doesn't prefix it with the asset host.
|
||||||
if use_asset_host && respond_to?(:public?) && !public?
|
if use_asset_host && use_authentication
|
||||||
use_asset_host = false
|
use_asset_host = false
|
||||||
only_path = false
|
only_path = false
|
||||||
end
|
end
|
||||||
|
@ -49,6 +50,6 @@ module Avatarable
|
||||||
url_base << gitlab_config.relative_url_root
|
url_base << gitlab_config.relative_url_root
|
||||||
end
|
end
|
||||||
|
|
||||||
url_base + avatar.url
|
url_base + avatar.local_url
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,7 +31,7 @@ module ProtectedRef
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil)
|
def protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
|
||||||
access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
|
access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
|
||||||
access_level.check_access(user)
|
access_level.check_access(user)
|
||||||
end
|
end
|
||||||
|
|
|
@ -102,7 +102,7 @@ module Routable
|
||||||
# the route. Caching this per request ensures that even if we have multiple instances,
|
# the route. Caching this per request ensures that even if we have multiple instances,
|
||||||
# we will not have to duplicate work, avoiding N+1 queries in some cases.
|
# we will not have to duplicate work, avoiding N+1 queries in some cases.
|
||||||
def full_path
|
def full_path
|
||||||
return uncached_full_path unless RequestStore.active?
|
return uncached_full_path unless RequestStore.active? && persisted?
|
||||||
|
|
||||||
RequestStore[full_path_key] ||= uncached_full_path
|
RequestStore[full_path_key] ||= uncached_full_path
|
||||||
end
|
end
|
||||||
|
@ -124,6 +124,11 @@ module Routable
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Group would override this to check from association
|
||||||
|
def owned_by?(user)
|
||||||
|
owner == user
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_path_errors
|
def set_path_errors
|
||||||
|
|
|
@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base
|
||||||
add_authentication_token_field :token
|
add_authentication_token_field :token
|
||||||
|
|
||||||
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
|
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
|
||||||
|
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'.freeze
|
||||||
|
|
||||||
default_value_for(:expires_at) { Forever.date }
|
default_value_for(:expires_at) { Forever.date }
|
||||||
|
|
||||||
|
@ -17,6 +18,10 @@ class DeployToken < ActiveRecord::Base
|
||||||
|
|
||||||
scope :active, -> { where("revoked = false AND expires_at >= NOW()") }
|
scope :active, -> { where("revoked = false AND expires_at >= NOW()") }
|
||||||
|
|
||||||
|
def self.gitlab_deploy_token
|
||||||
|
active.find_by(name: GITLAB_DEPLOY_TOKEN_NAME)
|
||||||
|
end
|
||||||
|
|
||||||
def revoke!
|
def revoke!
|
||||||
update!(revoked: true)
|
update!(revoked: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -125,6 +125,10 @@ class Group < Namespace
|
||||||
self[:lfs_enabled]
|
self[:lfs_enabled]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def owned_by?(user)
|
||||||
|
owners.include?(user)
|
||||||
|
end
|
||||||
|
|
||||||
def add_users(users, access_level, current_user: nil, expires_at: nil)
|
def add_users(users, access_level, current_user: nil, expires_at: nil)
|
||||||
GroupMember.add_users(
|
GroupMember.add_users(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -68,6 +68,11 @@ class Project < ActiveRecord::Base
|
||||||
|
|
||||||
after_save :update_project_statistics, if: :namespace_id_changed?
|
after_save :update_project_statistics, if: :namespace_id_changed?
|
||||||
after_create :create_project_feature, unless: :project_feature
|
after_create :create_project_feature, unless: :project_feature
|
||||||
|
|
||||||
|
after_create :create_ci_cd_settings,
|
||||||
|
unless: :ci_cd_settings,
|
||||||
|
if: proc { ProjectCiCdSetting.available? }
|
||||||
|
|
||||||
after_create :set_last_activity_at
|
after_create :set_last_activity_at
|
||||||
after_create :set_last_repository_updated_at
|
after_create :set_last_repository_updated_at
|
||||||
after_update :update_forks_visibility_level
|
after_update :update_forks_visibility_level
|
||||||
|
@ -231,6 +236,7 @@ class Project < ActiveRecord::Base
|
||||||
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
|
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
|
||||||
|
|
||||||
has_many :project_badges, class_name: 'ProjectBadge'
|
has_many :project_badges, class_name: 'ProjectBadge'
|
||||||
|
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting'
|
||||||
|
|
||||||
accepts_nested_attributes_for :variables, allow_destroy: true
|
accepts_nested_attributes_for :variables, allow_destroy: true
|
||||||
accepts_nested_attributes_for :project_feature, update_only: true
|
accepts_nested_attributes_for :project_feature, update_only: true
|
||||||
|
@ -1041,13 +1047,6 @@ class Project < ActiveRecord::Base
|
||||||
"#{web_url}.git"
|
"#{web_url}.git"
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_can_push_to_empty_repo?(user)
|
|
||||||
return false unless empty_repo?
|
|
||||||
return false unless Ability.allowed?(user, :push_code, self)
|
|
||||||
|
|
||||||
!ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
|
|
||||||
end
|
|
||||||
|
|
||||||
def forked?
|
def forked?
|
||||||
return true if fork_network && fork_network.root_project != self
|
return true if fork_network && fork_network.root_project != self
|
||||||
|
|
||||||
|
@ -1879,6 +1878,10 @@ class Project < ActiveRecord::Base
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def gitlab_deploy_token
|
||||||
|
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def storage
|
def storage
|
||||||
|
@ -2004,10 +2007,11 @@ class Project < ActiveRecord::Base
|
||||||
|
|
||||||
def fetch_branch_allows_maintainer_push?(user, branch_name)
|
def fetch_branch_allows_maintainer_push?(user, branch_name)
|
||||||
check_access = -> do
|
check_access = -> do
|
||||||
|
next false if empty_repo?
|
||||||
|
|
||||||
merge_request = source_of_merge_requests.opened
|
merge_request = source_of_merge_requests.opened
|
||||||
.where(allow_maintainer_to_push: true)
|
.where(allow_maintainer_to_push: true)
|
||||||
.find_by(source_branch: branch_name)
|
.find_by(source_branch: branch_name)
|
||||||
|
|
||||||
merge_request&.can_be_merged_by?(user)
|
merge_request&.can_be_merged_by?(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
class ProjectCiCdSetting < ActiveRecord::Base
|
||||||
|
belongs_to :project
|
||||||
|
|
||||||
|
# The version of the schema that first introduced this model/table.
|
||||||
|
MINIMUM_SCHEMA_VERSION = 20180403035759
|
||||||
|
|
||||||
|
def self.available?
|
||||||
|
@available ||=
|
||||||
|
ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.reset_column_information
|
||||||
|
@available = nil
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,5 +1,51 @@
|
||||||
require "flowdock-git-hook"
|
require "flowdock-git-hook"
|
||||||
|
|
||||||
|
# Flow dock depends on Grit to compute the number of commits between two given
|
||||||
|
# commits. To make this depend on Gitaly, a monkey patch is applied
|
||||||
|
module Flowdock
|
||||||
|
class Git
|
||||||
|
# pass down a Repository all the way down
|
||||||
|
def repo
|
||||||
|
@options[:repo]
|
||||||
|
end
|
||||||
|
|
||||||
|
def config
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages
|
||||||
|
Git::Builder.new(repo: repo,
|
||||||
|
ref: @ref,
|
||||||
|
before: @from,
|
||||||
|
after: @to,
|
||||||
|
commit_url: @commit_url,
|
||||||
|
branch_url: @branch_url,
|
||||||
|
diff_url: @diff_url,
|
||||||
|
repo_url: @repo_url,
|
||||||
|
repo_name: @repo_name,
|
||||||
|
permanent_refs: @permanent_refs,
|
||||||
|
tags: tags
|
||||||
|
).to_hashes
|
||||||
|
end
|
||||||
|
|
||||||
|
class Builder
|
||||||
|
def commits
|
||||||
|
@repo.commits_between(@before, @after).map do |commit|
|
||||||
|
{
|
||||||
|
url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil,
|
||||||
|
id: commit.sha,
|
||||||
|
message: commit.message,
|
||||||
|
author: {
|
||||||
|
name: commit.author_name,
|
||||||
|
email: commit.author_email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class FlowdockService < Service
|
class FlowdockService < Service
|
||||||
prop_accessor :token
|
prop_accessor :token
|
||||||
validates :token, presence: true, if: :activated?
|
validates :token, presence: true, if: :activated?
|
||||||
|
@ -34,7 +80,7 @@ class FlowdockService < Service
|
||||||
data[:before],
|
data[:before],
|
||||||
data[:after],
|
data[:after],
|
||||||
token: token,
|
token: token,
|
||||||
repo: project.repository.path_to_repo,
|
repo: project.repository,
|
||||||
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
|
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
|
||||||
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s",
|
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s",
|
||||||
diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
|
diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
|
||||||
|
|
|
@ -4,6 +4,15 @@ class ProtectedBranch < ActiveRecord::Base
|
||||||
|
|
||||||
protected_ref_access_levels :merge, :push
|
protected_ref_access_levels :merge, :push
|
||||||
|
|
||||||
|
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
|
||||||
|
# Masters, owners and admins are allowed to create the default branch
|
||||||
|
if default_branch_protected? && project.empty_repo?
|
||||||
|
return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
# Check if branch name is marked as protected in the system
|
# Check if branch name is marked as protected in the system
|
||||||
def self.protected?(project, ref_name)
|
def self.protected?(project, ref_name)
|
||||||
return true if project.empty_repo? && default_branch_protected?
|
return true if project.empty_repo? && default_branch_protected?
|
||||||
|
|
|
@ -22,7 +22,7 @@ class GroupPolicy < BasePolicy
|
||||||
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
|
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
|
||||||
|
|
||||||
condition(:has_projects) do
|
condition(:has_projects) do
|
||||||
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
|
GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
with_options scope: :subject, score: 0
|
with_options scope: :subject, score: 0
|
||||||
|
@ -43,7 +43,11 @@ class GroupPolicy < BasePolicy
|
||||||
end
|
end
|
||||||
|
|
||||||
rule { admin } .enable :read_group
|
rule { admin } .enable :read_group
|
||||||
rule { has_projects } .enable :read_group
|
|
||||||
|
rule { has_projects }.policy do
|
||||||
|
enable :read_group
|
||||||
|
enable :read_label
|
||||||
|
end
|
||||||
|
|
||||||
rule { has_access }.enable :read_namespace
|
rule { has_access }.enable :read_namespace
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
||||||
include GitlabRoutingHelper
|
include GitlabRoutingHelper
|
||||||
include StorageHelper
|
include StorageHelper
|
||||||
include TreeHelper
|
include TreeHelper
|
||||||
|
include ChecksCollaboration
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
presents :project
|
presents :project
|
||||||
|
@ -170,9 +171,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_current_user_push_to_branch?(branch)
|
def can_current_user_push_to_branch?(branch)
|
||||||
return false unless repository.branch_exists?(branch)
|
user_access(project).can_push_to_branch?(branch)
|
||||||
|
end
|
||||||
|
|
||||||
::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch)
|
def can_current_user_push_to_default_branch?
|
||||||
|
can_current_user_push_to_branch?(default_branch)
|
||||||
end
|
end
|
||||||
|
|
||||||
def files_anchor_data
|
def files_anchor_data
|
||||||
|
@ -200,7 +203,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_file_anchor_data
|
def new_file_anchor_data
|
||||||
if current_user && can_current_user_push_code?
|
if current_user && can_current_user_push_to_default_branch?
|
||||||
OpenStruct.new(enabled: false,
|
OpenStruct.new(enabled: false,
|
||||||
label: _('New file'),
|
label: _('New file'),
|
||||||
link: project_new_blob_path(project, default_branch || 'master'),
|
link: project_new_blob_path(project, default_branch || 'master'),
|
||||||
|
@ -209,7 +212,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
||||||
end
|
end
|
||||||
|
|
||||||
def readme_anchor_data
|
def readme_anchor_data
|
||||||
if current_user && can_current_user_push_code? && repository.readme.blank?
|
if current_user && can_current_user_push_to_default_branch? && repository.readme.blank?
|
||||||
OpenStruct.new(enabled: false,
|
OpenStruct.new(enabled: false,
|
||||||
label: _('Add Readme'),
|
label: _('Add Readme'),
|
||||||
link: add_readme_path)
|
link: add_readme_path)
|
||||||
|
@ -221,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
||||||
end
|
end
|
||||||
|
|
||||||
def changelog_anchor_data
|
def changelog_anchor_data
|
||||||
if current_user && can_current_user_push_code? && repository.changelog.blank?
|
if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
|
||||||
OpenStruct.new(enabled: false,
|
OpenStruct.new(enabled: false,
|
||||||
label: _('Add Changelog'),
|
label: _('Add Changelog'),
|
||||||
link: add_changelog_path)
|
link: add_changelog_path)
|
||||||
|
@ -233,7 +236,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
||||||
end
|
end
|
||||||
|
|
||||||
def license_anchor_data
|
def license_anchor_data
|
||||||
if current_user && can_current_user_push_code? && repository.license_blob.blank?
|
if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank?
|
||||||
OpenStruct.new(enabled: false,
|
OpenStruct.new(enabled: false,
|
||||||
label: _('Add License'),
|
label: _('Add License'),
|
||||||
link: add_license_path)
|
link: add_license_path)
|
||||||
|
@ -245,7 +248,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
||||||
end
|
end
|
||||||
|
|
||||||
def contribution_guide_anchor_data
|
def contribution_guide_anchor_data
|
||||||
if current_user && can_current_user_push_code? && repository.contribution_guide.blank?
|
if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
|
||||||
OpenStruct.new(enabled: false,
|
OpenStruct.new(enabled: false,
|
||||||
label: _('Add Contribution guide'),
|
label: _('Add Contribution guide'),
|
||||||
link: add_contribution_guide_path)
|
link: add_contribution_guide_path)
|
||||||
|
@ -260,7 +263,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
||||||
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
|
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
|
||||||
OpenStruct.new(enabled: auto_devops_enabled?,
|
OpenStruct.new(enabled: auto_devops_enabled?,
|
||||||
label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
|
label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
|
||||||
link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))
|
link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
|
||||||
elsif auto_devops_enabled?
|
elsif auto_devops_enabled?
|
||||||
OpenStruct.new(enabled: true,
|
OpenStruct.new(enabled: true,
|
||||||
label: _('Auto DevOps enabled'),
|
label: _('Auto DevOps enabled'),
|
||||||
|
|
|
@ -138,8 +138,10 @@ module QuickActions
|
||||||
'Remove assignee'
|
'Remove assignee'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
explanation do
|
explanation do |users = nil|
|
||||||
"Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}."
|
assignees = issuable.assignees
|
||||||
|
assignees &= users if users.present? && issuable.allows_multiple_assignees?
|
||||||
|
"Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}."
|
||||||
end
|
end
|
||||||
params do
|
params do
|
||||||
issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
|
issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
|
||||||
|
@ -268,6 +270,26 @@ module QuickActions
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc 'Copy labels and milestone from other issue or merge request'
|
||||||
|
explanation do |source_issuable|
|
||||||
|
"Copy labels and milestone from #{source_issuable.to_reference}."
|
||||||
|
end
|
||||||
|
params '#issue | !merge_request'
|
||||||
|
condition do
|
||||||
|
issuable.persisted? &&
|
||||||
|
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
|
||||||
|
end
|
||||||
|
parse_params do |issuable_param|
|
||||||
|
extract_references(issuable_param, :issue).first ||
|
||||||
|
extract_references(issuable_param, :merge_request).first
|
||||||
|
end
|
||||||
|
command :copy_metadata do |source_issuable|
|
||||||
|
if source_issuable.present? && source_issuable.project.id == issuable.project.id
|
||||||
|
@updates[:add_label_ids] = source_issuable.labels.map(&:id)
|
||||||
|
@updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
desc 'Add a todo'
|
desc 'Add a todo'
|
||||||
explanation 'Adds a todo.'
|
explanation 'Adds a todo.'
|
||||||
condition do
|
condition do
|
||||||
|
|
|
@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
|
||||||
!!model
|
!!model
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def local_url
|
||||||
|
File.join('/', self.class.base_dir, dynamic_segment, filename)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Designed to be overridden by child uploaders that have a dynamic path
|
# Designed to be overridden by child uploaders that have a dynamic path
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
.help-block
|
.help-block
|
||||||
Manage repository storage paths. Learn more in the
|
Manage repository storage paths. Learn more in the
|
||||||
= succeed "." do
|
= succeed "." do
|
||||||
= link_to "repository storages documentation", help_page_path("administration/repository_storages")
|
= link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
|
||||||
.sub-section
|
.sub-section
|
||||||
%h4 Circuit breaker
|
%h4 Circuit breaker
|
||||||
.form-group
|
.form-group
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
%hr
|
%hr
|
||||||
%p
|
%p
|
||||||
- link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'))
|
- link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
|
||||||
- link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
|
- link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
|
||||||
= s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
|
= s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
|
||||||
|
|
||||||
|
@ -58,7 +58,9 @@
|
||||||
touch README.md
|
touch README.md
|
||||||
git add README.md
|
git add README.md
|
||||||
git commit -m "add README"
|
git commit -m "add README"
|
||||||
git push -u origin master
|
- if @project.can_current_user_push_to_default_branch?
|
||||||
|
%span><
|
||||||
|
git push -u origin master
|
||||||
|
|
||||||
%fieldset
|
%fieldset
|
||||||
%h5 Existing folder
|
%h5 Existing folder
|
||||||
|
@ -69,7 +71,9 @@
|
||||||
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
|
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Initial commit"
|
git commit -m "Initial commit"
|
||||||
git push -u origin master
|
- if @project.can_current_user_push_to_default_branch?
|
||||||
|
%span><
|
||||||
|
git push -u origin master
|
||||||
|
|
||||||
%fieldset
|
%fieldset
|
||||||
%h5 Existing Git repository
|
%h5 Existing Git repository
|
||||||
|
@ -78,8 +82,10 @@
|
||||||
cd existing_repo
|
cd existing_repo
|
||||||
git remote rename origin old-origin
|
git remote rename origin old-origin
|
||||||
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
|
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
|
||||||
git push -u origin --all
|
- if @project.can_current_user_push_to_default_branch?
|
||||||
git push -u origin --tags
|
%span><
|
||||||
|
git push -u origin --all
|
||||||
|
git push -u origin --tags
|
||||||
|
|
||||||
- if can? current_user, :remove_project, @project
|
- if can? current_user, :remove_project, @project
|
||||||
.prepend-top-20
|
.prepend-top-20
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
- unless @repository.gitlab_ci_yml
|
- unless @repository.gitlab_ci_yml
|
||||||
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
|
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
|
||||||
|
|
||||||
= link_to ci_lint_path, class: 'btn btn-default' do
|
= link_to project_ci_lint_path(@project), class: 'btn btn-default' do
|
||||||
%span CI lint
|
%span CI lint
|
||||||
|
|
||||||
.content-list.builds-content-list
|
.content-list.builds-content-list
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
%ul
|
%ul
|
||||||
- pipeline.yaml_errors.split(",").each do |error|
|
- pipeline.yaml_errors.split(",").each do |error|
|
||||||
%li= error
|
%li= error
|
||||||
You can also test your .gitlab-ci.yml in the #{link_to "Lint", ci_lint_path}
|
You can also test your .gitlab-ci.yml in the #{link_to "Lint", project_ci_lint_path(@project)}
|
||||||
|
|
||||||
- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
|
- if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file
|
||||||
.bs-callout.bs-callout-warning
|
.bs-callout.bs-callout-warning
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
docker login #{Gitlab.config.registry.host_port}
|
docker login #{Gitlab.config.registry.host_port}
|
||||||
%br
|
%br
|
||||||
%p
|
%p
|
||||||
- deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
|
- deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
|
||||||
= s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
|
= s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
|
||||||
%br
|
%br
|
||||||
%p
|
%p
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
.row.prepend-top-default
|
||||||
|
.col-lg-12
|
||||||
|
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
|
||||||
|
= form_errors(@project)
|
||||||
|
%fieldset.builds-feature
|
||||||
|
.form-group
|
||||||
|
- message = auto_devops_warning_message(@project)
|
||||||
|
- ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
|
||||||
|
- if message
|
||||||
|
%p.settings-message.text-center
|
||||||
|
= message.html_safe
|
||||||
|
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
|
||||||
|
.radio
|
||||||
|
= form.label :enabled_true do
|
||||||
|
= form.radio_button :enabled, 'true'
|
||||||
|
%strong= s_('CICD|Enable Auto DevOps')
|
||||||
|
%br
|
||||||
|
= s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
|
||||||
|
|
||||||
|
.radio
|
||||||
|
= form.label :enabled_false do
|
||||||
|
= form.radio_button :enabled, 'false'
|
||||||
|
%strong= s_('CICD|Disable Auto DevOps')
|
||||||
|
%br
|
||||||
|
= s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
|
||||||
|
|
||||||
|
.radio
|
||||||
|
= form.label :enabled_ do
|
||||||
|
= form.radio_button :enabled, ''
|
||||||
|
%strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
|
||||||
|
%br
|
||||||
|
= s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
|
||||||
|
|
||||||
|
= form.label :domain, class:"prepend-top-10" do
|
||||||
|
= _('Domain')
|
||||||
|
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
|
||||||
|
.help-block
|
||||||
|
= s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
|
||||||
|
|
||||||
|
= f.submit 'Save changes', class: "btn btn-success prepend-top-15"
|
|
@ -3,44 +3,6 @@
|
||||||
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
|
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
|
||||||
= form_errors(@project)
|
= form_errors(@project)
|
||||||
%fieldset.builds-feature
|
%fieldset.builds-feature
|
||||||
.form-group
|
|
||||||
%h5 Auto DevOps (Beta)
|
|
||||||
%p
|
|
||||||
Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
|
|
||||||
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
|
|
||||||
- message = auto_devops_warning_message(@project)
|
|
||||||
- if message
|
|
||||||
%p.settings-message.text-center
|
|
||||||
= message.html_safe
|
|
||||||
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
|
|
||||||
.radio
|
|
||||||
= form.label :enabled_true do
|
|
||||||
= form.radio_button :enabled, 'true'
|
|
||||||
%strong Enable Auto DevOps
|
|
||||||
%br
|
|
||||||
%span.descr
|
|
||||||
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
|
|
||||||
|
|
||||||
.radio
|
|
||||||
= form.label :enabled_false do
|
|
||||||
= form.radio_button :enabled, 'false'
|
|
||||||
%strong Disable Auto DevOps
|
|
||||||
%br
|
|
||||||
%span.descr
|
|
||||||
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
|
|
||||||
|
|
||||||
.radio
|
|
||||||
= form.label :enabled_ do
|
|
||||||
= form.radio_button :enabled, ''
|
|
||||||
%strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'})
|
|
||||||
%br
|
|
||||||
%span.descr
|
|
||||||
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
|
|
||||||
%p
|
|
||||||
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
|
|
||||||
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
|
|
||||||
|
|
||||||
%hr
|
|
||||||
.form-group.append-bottom-default.js-secret-runner-token
|
.form-group.append-bottom-default.js-secret-runner-token
|
||||||
= f.label :runners_token, "Runner token", class: 'label-light'
|
= f.label :runners_token, "Runner token", class: 'label-light'
|
||||||
.form-control.js-secret-value-placeholder
|
.form-control.js-secret-value-placeholder
|
||||||
|
|
|
@ -12,10 +12,22 @@
|
||||||
%button.btn.js-settings-toggle{ type: 'button' }
|
%button.btn.js-settings-toggle{ type: 'button' }
|
||||||
= expanded ? 'Collapse' : 'Expand'
|
= expanded ? 'Collapse' : 'Expand'
|
||||||
%p
|
%p
|
||||||
Update your CI/CD configuration, like job timeout or Auto DevOps.
|
Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
|
||||||
.settings-content
|
.settings-content
|
||||||
= render 'form'
|
= render 'form'
|
||||||
|
|
||||||
|
%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
|
||||||
|
.settings-header
|
||||||
|
%h4
|
||||||
|
= s_('CICD|Auto DevOps (Beta)')
|
||||||
|
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
|
||||||
|
= expanded ? _('Collapse') : _('Expand')
|
||||||
|
%p
|
||||||
|
= s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
|
||||||
|
= link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
|
||||||
|
.settings-content
|
||||||
|
= render 'autodevops_form'
|
||||||
|
|
||||||
%section.settings.no-animate{ class: ('expanded' if expanded) }
|
%section.settings.no-animate{ class: ('expanded' if expanded) }
|
||||||
.settings-header
|
.settings-header
|
||||||
%h4
|
%h4
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
|
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
|
||||||
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
|
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
|
||||||
.banner-buttons
|
.banner-buttons
|
||||||
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout'
|
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout'
|
||||||
|
|
||||||
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
|
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
|
||||||
'aria-label' => 'Dismiss Auto DevOps box' }
|
'aria-label' => 'Dismiss Auto DevOps box' }
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Introduce new ProjectCiCdSetting model with group_runners_enabled
|
||||||
|
merge_request: 18144
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Prevent pipeline actions in dropdown to redirct to a new page
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
title: Add an API endpoint to download git repository snapshots
|
|
||||||
merge_request: 18173
|
|
||||||
author:
|
|
||||||
type: added
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
title: Fix discussions API setting created_at for notable in a group or notable in
|
||||||
|
a project in a group with owners
|
||||||
|
merge_request: 18464
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Reduce queries on merge requests list page for merge requests from forks
|
||||||
|
merge_request: 18561
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Create settings section for autodevops
|
||||||
|
merge_request: 18321
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Expose Deploy Token data as environment varialbes on CI/CD jobs
|
||||||
|
merge_request: 18414
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fixed wrong avatar URL when the avatar is on object storage.
|
||||||
|
merge_request: 18092
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
title: Fix `Trace::HttpIO` can not render multi-byte chars
|
|
||||||
merge_request: 18417
|
|
||||||
author:
|
|
||||||
type: fixed
|
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
title: Align action icons in pipeline graph
|
|
||||||
merge_request:
|
|
||||||
author:
|
|
||||||
type: fixed
|
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
title: '[API] Fix URLs in the `Link` header for `GET /projects/:id/repository/contributors`
|
|
||||||
when no value is passed for `order_by` or `sort`'
|
|
||||||
merge_request: 18393
|
|
||||||
author:
|
|
||||||
type: fixed
|
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
title: Add index to file_store on ci_job_artifacts
|
|
||||||
merge_request: 18444
|
|
||||||
author:
|
|
||||||
type: performance
|
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
title: Fix specifying a non-default ref when requesting an archive using the legacy
|
|
||||||
URL
|
|
||||||
merge_request: 18468
|
|
||||||
author:
|
|
||||||
type: fixed
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix project creation for user endpoint when jobs_enabled parameter supplied
|
||||||
|
merge_request:
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
title: Removes 'No Job log' message from build trace
|
|
||||||
merge_request: 18523
|
|
||||||
author:
|
|
||||||
type: fixed
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Update links to /ci/lint with ones to project ci/lint
|
||||||
|
merge_request: 18539
|
||||||
|
author: Takuya Noguchi
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix unassign slash command preview
|
||||||
|
merge_request: 18447
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
title: Validate project path prior to hitting the database.
|
|
||||||
merge_request: 18322
|
|
||||||
author:
|
|
||||||
type: performance
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add Copy metadata quick action
|
||||||
|
merge_request: 16473
|
||||||
|
author: Mateusz Bajorski
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Align project avatar on small viewports
|
||||||
|
merge_request: 18513
|
||||||
|
author: George Tsiolis
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add missing changelog type to docs
|
||||||
|
merge_request: 18526
|
||||||
|
author: "@blackst0ne"
|
||||||
|
type: other
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix errors on pushing to an empty repository
|
||||||
|
merge_request: 18462
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add 2FA filter to users API for admins only
|
||||||
|
merge_request: 18503
|
||||||
|
author:
|
||||||
|
type: changed
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue