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"],
|
||||
"env": {
|
||||
"karma": {
|
||||
"plugins": ["rewire"]
|
||||
},
|
||||
"coverage": {
|
||||
"plugins": [
|
||||
[
|
||||
|
@ -14,7 +17,8 @@
|
|||
{
|
||||
"process.env.BABEL_ENV": "coverage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"rewire"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
|
|||
Lint/NestedPercentLiteral:
|
||||
Exclude:
|
||||
- 'lib/gitlab/git/repository.rb'
|
||||
- 'spec/support/email_format_shared_examples.rb'
|
||||
- 'spec/support/shared_examples/email_format_shared_examples.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Lint/ReturnInVoidContext:
|
||||
|
@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
|
|||
- 'spec/lib/gitlab/diff/parser_spec.rb'
|
||||
- 'spec/lib/json_web_token/rsa_token_spec.rb'
|
||||
- 'spec/models/commit_spec.rb'
|
||||
- 'spec/support/repo_helpers.rb'
|
||||
- 'spec/support/seed_repo.rb'
|
||||
- 'spec/support/helpers/repo_helpers.rb'
|
||||
- 'spec/support/helpers/seed_repo.rb'
|
||||
|
||||
# Offense count: 112
|
||||
# Configuration parameters: Blacklist.
|
||||
|
@ -496,7 +496,7 @@ Style/EmptyLiteral:
|
|||
- 'spec/lib/gitlab/request_context_spec.rb'
|
||||
- 'spec/lib/gitlab/workhorse_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
|
||||
# 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
|
||||
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)
|
||||
|
||||
### 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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [Implement design & UI elements](#implement-design-ui-elements)
|
||||
- [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.
|
||||
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
|
||||
- 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
|
||||
[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 |
|
||||
|-------|-----------------|------------------------------------------------------------------|----------|
|
||||
| ~P1 | Urgent Priority | The current release | |
|
||||
| ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
|
||||
| ~P2 | High Priority | The next release | |
|
||||
| ~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 |
|
||||
|
|
11
Gemfile.lock
11
Gemfile.lock
|
@ -178,7 +178,7 @@ GEM
|
|||
docile (1.1.5)
|
||||
domain_name (0.5.20170404)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (4.3.1)
|
||||
doorkeeper (4.3.2)
|
||||
railties (>= 4.2)
|
||||
doorkeeper-openid_connect (1.3.0)
|
||||
doorkeeper (~> 4.3)
|
||||
|
@ -483,10 +483,11 @@ GEM
|
|||
logging (2.2.2)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.10)
|
||||
lograge (0.5.1)
|
||||
actionpack (>= 4, < 5.2)
|
||||
activesupport (>= 4, < 5.2)
|
||||
railties (>= 4, < 5.2)
|
||||
lograge (0.10.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.2.2)
|
||||
crass (~> 1.0.2)
|
||||
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>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import ideSidebar from './ide_side_bar.vue';
|
||||
import ideContextbar from './ide_context_bar.vue';
|
||||
import repoTabs from './repo_tabs.vue';
|
||||
import ideStatusBar from './ide_status_bar.vue';
|
||||
import repoEditor from './repo_editor.vue';
|
||||
import { mapActions, mapState, mapGetters } from 'vuex';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import ideSidebar from './ide_side_bar.vue';
|
||||
import ideContextbar from './ide_context_bar.vue';
|
||||
import repoTabs from './repo_tabs.vue';
|
||||
import ideStatusBar from './ide_status_bar.vue';
|
||||
import repoEditor from './repo_editor.vue';
|
||||
import FindFile from './file_finder/index.vue';
|
||||
|
||||
export default {
|
||||
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;
|
||||
const originalStopCallback = Mousetrap.stopCallback;
|
||||
|
||||
Object.assign(e, {
|
||||
returnValue,
|
||||
export default {
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-view"
|
||||
>
|
||||
<find-file
|
||||
v-show="fileFindVisible"
|
||||
/>
|
||||
<ide-sidebar />
|
||||
<div
|
||||
class="multi-file-edit-pane"
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
// 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_BODY_LENGTH = 72;
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import _ from 'underscore';
|
||||
import store from '../stores';
|
||||
import DecorationsController from './decorations/controller';
|
||||
import DirtyDiffController from './diff/controller';
|
||||
import Disposable from './common/disposable';
|
||||
import ModelManager from './common/model_manager';
|
||||
import editorOptions, { defaultEditorOptions } from './editor_options';
|
||||
import gitlabTheme from './themes/gl_theme';
|
||||
import keymap from './keymap.json';
|
||||
|
||||
export const clearDomElement = el => {
|
||||
if (!el || !el.firstChild) return;
|
||||
|
@ -53,6 +55,8 @@ export default class Editor {
|
|||
)),
|
||||
);
|
||||
|
||||
this.addCommands();
|
||||
|
||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +77,8 @@ export default class Editor {
|
|||
})),
|
||||
);
|
||||
|
||||
this.addCommands();
|
||||
|
||||
window.addEventListener('resize', this.debouncedUpdate, false);
|
||||
}
|
||||
}
|
||||
|
@ -189,4 +195,31 @@ export default class Editor {
|
|||
static renderSideBySide(domElement) {
|
||||
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/file';
|
||||
export * from './actions/project';
|
||||
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 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);
|
||||
|
||||
// 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);
|
||||
});
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
// 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 UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
|
||||
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
|
||||
|
|
|
@ -107,6 +107,11 @@ export default {
|
|||
delayViewerUpdated,
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
|
||||
Object.assign(state, {
|
||||
fileFindVisible,
|
||||
});
|
||||
},
|
||||
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
|
||||
const changedFile = state.changedFiles.find(f => f.path === file.path);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ export default {
|
|||
[types.SET_FILE_ACTIVE](state, { path, active }) {
|
||||
Object.assign(state.entries[path], {
|
||||
active,
|
||||
lastOpenedAt: new Date().getTime(),
|
||||
});
|
||||
|
||||
if (active && !state.entries[path].pending) {
|
||||
|
|
|
@ -18,4 +18,5 @@ export default () => ({
|
|||
entries: {},
|
||||
viewer: 'editor',
|
||||
delayViewerUpdated: false,
|
||||
fileFindVisible: false,
|
||||
});
|
||||
|
|
|
@ -43,6 +43,7 @@ export const dataStructure = () => ({
|
|||
viewMode: 'edit',
|
||||
previewMode: null,
|
||||
size: 0,
|
||||
lastOpenedAt: 0,
|
||||
});
|
||||
|
||||
export const decorateData = entity => {
|
||||
|
|
|
@ -45,7 +45,7 @@ export default {
|
|||
return timeIntervalInWords(this.job.queued);
|
||||
},
|
||||
runnerId() {
|
||||
return `#${this.job.runner.id}`;
|
||||
return `${this.job.runner.description} (#${this.job.runner.id})`;
|
||||
},
|
||||
retryButtonClass() {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -32,26 +32,38 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
|
||||
buttonDisabled: {
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDisabled: false,
|
||||
linkRequested: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
cssClass() {
|
||||
const actionIconDash = dasherize(this.actionIcon);
|
||||
return `${actionIconDash} js-icon-${actionIconDash}`;
|
||||
},
|
||||
isDisabled() {
|
||||
return this.buttonDisabled === this.link;
|
||||
},
|
||||
watch: {
|
||||
requestFinishedFor() {
|
||||
if (this.requestFinishedFor === this.linkRequested) {
|
||||
this.isDisabled = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickAction() {
|
||||
$(this.$el).tooltip('hide');
|
||||
eventHub.$emit('graphAction', this.link);
|
||||
this.linkRequested = this.link;
|
||||
this.isDisabled = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -62,7 +74,8 @@ export default {
|
|||
@click="onClickAction"
|
||||
v-tooltip
|
||||
: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"
|
||||
data-container="body"
|
||||
: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>
|
||||
import $ from 'jquery';
|
||||
import jobNameComponent from './job_name_component.vue';
|
||||
import jobComponent from './job_component.vue';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
import $ from 'jquery';
|
||||
import JobNameComponent from './job_name_component.vue';
|
||||
import JobComponent from './job_component.vue';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
|
||||
/**
|
||||
* Renders the dropdown for the pipeline graph.
|
||||
*
|
||||
* The following object should be provided as `job`:
|
||||
*
|
||||
* {
|
||||
* "id": 4256,
|
||||
* "name": "test",
|
||||
* "status": {
|
||||
* "icon": "icon_status_success",
|
||||
* "text": "passed",
|
||||
* "label": "passed",
|
||||
* "group": "success",
|
||||
* "details_path": "/root/ci-mock/builds/4256",
|
||||
* "action": {
|
||||
* "icon": "retry",
|
||||
* "title": "Retry",
|
||||
* "path": "/root/ci-mock/builds/4256/retry",
|
||||
* "method": "post"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
/**
|
||||
* Renders the dropdown for the pipeline graph.
|
||||
*
|
||||
* The following object should be provided as `job`:
|
||||
*
|
||||
* {
|
||||
* "id": 4256,
|
||||
* "name": "test",
|
||||
* "status": {
|
||||
* "icon": "icon_status_success",
|
||||
* "text": "passed",
|
||||
* "label": "passed",
|
||||
* "group": "success",
|
||||
* "details_path": "/root/ci-mock/builds/4256",
|
||||
* "action": {
|
||||
* "icon": "retry",
|
||||
* "title": "Retry",
|
||||
* "path": "/root/ci-mock/builds/4256/retry",
|
||||
* "method": "post"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
||||
components: {
|
||||
JobComponent,
|
||||
JobNameComponent,
|
||||
},
|
||||
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
components: {
|
||||
jobComponent,
|
||||
jobNameComponent,
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
tooltipText() {
|
||||
return `${this.job.name} - ${this.job.status.label}`;
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
tooltipText() {
|
||||
return `${this.job.name} - ${this.job.status.label}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.stopDropdownClickPropagation();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.stopDropdownClickPropagation();
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* 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.
|
||||
methods: {
|
||||
/**
|
||||
* 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
|
||||
* of the click event inside the dropdown.
|
||||
*
|
||||
* Since this component is rendered multiple times per page we need to guarantee we only
|
||||
* target the click event of this component.
|
||||
*/
|
||||
stopDropdownClickPropagation() {
|
||||
$(this.$el
|
||||
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
|
||||
.on('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
stopDropdownClickPropagation() {
|
||||
$(
|
||||
'.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
|
||||
this.$el,
|
||||
).on('click', e => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="ci-job-dropdown-container">
|
||||
|
@ -101,8 +107,8 @@
|
|||
:key="i">
|
||||
<job-component
|
||||
:job="item"
|
||||
:is-dropdown="true"
|
||||
css-class-job-name="mini-pipeline-graph-dropdown-item"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -7,7 +7,6 @@ export default {
|
|||
StageColumnComponent,
|
||||
LoadingIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
|
@ -17,10 +16,10 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
actionDisabled: {
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -75,7 +74,7 @@ export default {
|
|||
:key="stage.name"
|
||||
:stage-connector-class="stageConnectorClass(index, stage)"
|
||||
:is-first-column="isFirstColumn(index)"
|
||||
:action-disabled="actionDisabled"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import ActionComponent from './action_component.vue';
|
||||
import DropdownActionComponent from './dropdown_action_component.vue';
|
||||
import JobNameComponent from './job_name_component.vue';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
|
||||
|
@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
|
|||
export default {
|
||||
components: {
|
||||
ActionComponent,
|
||||
DropdownActionComponent,
|
||||
JobNameComponent,
|
||||
},
|
||||
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
|
@ -44,26 +41,17 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
cssClassJobName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
isDropdown: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
actionDisabled: {
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
status() {
|
||||
return this.job && this.job.status ? this.job.status : {};
|
||||
|
@ -134,19 +122,11 @@ export default {
|
|||
</div>
|
||||
|
||||
<action-component
|
||||
v-if="hasAction && !isDropdown"
|
||||
v-if="hasAction"
|
||||
:tooltip-text="status.action.title"
|
||||
:link="status.action.path"
|
||||
:action-icon="status.action.icon"
|
||||
:button-disabled="actionDisabled"
|
||||
/>
|
||||
|
||||
<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"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -29,10 +29,11 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
actionDisabled: {
|
||||
|
||||
requestFinishedFor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -74,12 +75,12 @@ export default {
|
|||
v-if="job.size === 1"
|
||||
:job="job"
|
||||
css-class-job-name="build-content"
|
||||
:action-disabled="actionDisabled"
|
||||
/>
|
||||
|
||||
<dropdown-job-component
|
||||
v-if="job.size > 1"
|
||||
:job="job"
|
||||
:request-finished-for="requestFinishedFor"
|
||||
/>
|
||||
|
||||
</li>
|
||||
|
|
|
@ -25,7 +25,7 @@ export default () => {
|
|||
data() {
|
||||
return {
|
||||
mediator,
|
||||
actionDisabled: null,
|
||||
requestFinishedFor: null,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
@ -36,15 +36,17 @@ export default () => {
|
|||
},
|
||||
methods: {
|
||||
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(() => {
|
||||
this.mediator.refreshPipeline();
|
||||
this.actionDisabled = null;
|
||||
this.requestFinishedFor = action;
|
||||
})
|
||||
.catch(() => {
|
||||
this.actionDisabled = null;
|
||||
this.requestFinishedFor = action;
|
||||
Flash(__('An error occurred while making the request.'));
|
||||
});
|
||||
},
|
||||
|
@ -54,7 +56,7 @@ export default () => {
|
|||
props: {
|
||||
isLoading: this.mediator.state.isLoading,
|
||||
pipeline: this.mediator.store.state.pipeline,
|
||||
actionDisabled: this.actionDisabled,
|
||||
requestFinishedFor: this.requestFinishedFor,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -79,7 +81,8 @@ export default () => {
|
|||
},
|
||||
methods: {
|
||||
postAction(action) {
|
||||
this.mediator.service.postAction(action.path)
|
||||
this.mediator.service
|
||||
.postAction(action.path)
|
||||
.then(() => this.mediator.refreshPipeline())
|
||||
.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 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 repos = state => state.repos;
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
||||
|
|
|
@ -1,52 +1,52 @@
|
|||
<script>
|
||||
import ciIcon from './ci_icon.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
/**
|
||||
* Renders CI Badge link with CI icon and status text based on
|
||||
* API response shared between all places where it is used.
|
||||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
* text:"running" // text rendered
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Pipelines table - first column
|
||||
* - Jobs table - first column
|
||||
* - Pipeline show view - header
|
||||
* - Job show view - header
|
||||
* - MR widget
|
||||
*/
|
||||
import CiIcon from './ci_icon.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
/**
|
||||
* Renders CI Badge link with CI icon and status text based on
|
||||
* API response shared between all places where it is used.
|
||||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
* text:"running" // text rendered
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Pipelines table - first column
|
||||
* - Jobs table - first column
|
||||
* - Pipeline show view - header
|
||||
* - Job show view - header
|
||||
* - MR widget
|
||||
*/
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ciIcon,
|
||||
export default {
|
||||
components: {
|
||||
CiIcon,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
showText: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showText: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cssClass() {
|
||||
const className = this.status.group;
|
||||
return className ? `ci-status ci-${className}` : 'ci-status';
|
||||
},
|
||||
computed: {
|
||||
cssClass() {
|
||||
const className = this.status.group;
|
||||
return className ? `ci-status ci-${className}` : 'ci-status';
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a
|
||||
|
|
|
@ -1,45 +1,44 @@
|
|||
<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.
|
||||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
* text:"running" // text rendered
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Pipelines table Badge
|
||||
* - Pipelines table mini graph
|
||||
* - Pipeline graph
|
||||
* - Pipeline show view badge
|
||||
* - Jobs table
|
||||
* - Jobs show view header
|
||||
* - Jobs show view sidebar
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
/**
|
||||
* Renders CI icon based on API response shared between all places where it is used.
|
||||
*
|
||||
* Receives status object containing:
|
||||
* status: {
|
||||
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
|
||||
* group:"running" // used for CSS class
|
||||
* icon: "icon_status_running" // used to render the icon
|
||||
* label:"running" // used for potential tooltip
|
||||
* text:"running" // text rendered
|
||||
* }
|
||||
*
|
||||
* Used in:
|
||||
* - Pipelines table Badge
|
||||
* - Pipelines table mini graph
|
||||
* - Pipeline graph
|
||||
* - Pipeline show view badge
|
||||
* - Jobs table
|
||||
* - Jobs show view header
|
||||
* - Jobs show view sidebar
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cssClass() {
|
||||
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>
|
||||
<template>
|
||||
<span :class="cssClass">
|
||||
|
|
|
@ -1,40 +1,50 @@
|
|||
<script>
|
||||
/**
|
||||
* Falls back to the code used in `copy_to_clipboard.js`
|
||||
*/
|
||||
import tooltip from '../directives/tooltip';
|
||||
/**
|
||||
* Falls back to the code used in `copy_to_clipboard.js`
|
||||
*
|
||||
* 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 {
|
||||
name: 'ClipboardButton',
|
||||
directives: {
|
||||
tooltip,
|
||||
export default {
|
||||
name: 'ClipboardButton',
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
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',
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,119 +1,111 @@
|
|||
<script>
|
||||
import commitIconSvg from 'icons/_icon_commit.svg';
|
||||
import userAvatarLink from './user_avatar/user_avatar_link.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
import Icon from '../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
export default {
|
||||
directives: {
|
||||
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,
|
||||
icon,
|
||||
/**
|
||||
* 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: '',
|
||||
},
|
||||
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.
|
||||
*/
|
||||
shortSha: {
|
||||
type: String,
|
||||
required: false,
|
||||
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,
|
||||
},
|
||||
/**
|
||||
* Used to show the commit short sha that links to the commit url.
|
||||
*/
|
||||
shortSha: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
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;
|
||||
},
|
||||
/**
|
||||
* If provided shows the commit tile.
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
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>
|
||||
<template>
|
||||
<div class="branch-commit">
|
||||
|
@ -141,11 +133,10 @@
|
|||
{{ commitRef.name }}
|
||||
</a>
|
||||
</template>
|
||||
<div
|
||||
v-html="commitIconSvg"
|
||||
<icon
|
||||
name="commit"
|
||||
class="commit-icon js-commit-icon"
|
||||
>
|
||||
</div>
|
||||
/>
|
||||
|
||||
<a
|
||||
class="commit-sha"
|
||||
|
|
|
@ -1,33 +1,33 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
/**
|
||||
* Port of detail_behavior expand button.
|
||||
*
|
||||
* @example
|
||||
* <expand-button>
|
||||
* <template slot="expanded">
|
||||
* Text goes here.
|
||||
* </template>
|
||||
* </expand-button>
|
||||
*/
|
||||
export default {
|
||||
name: 'ExpandButton',
|
||||
data() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
};
|
||||
import { __ } from '~/locale';
|
||||
/**
|
||||
* Port of detail_behavior expand button.
|
||||
*
|
||||
* @example
|
||||
* <expand-button>
|
||||
* <template slot="expanded">
|
||||
* Text goes here.
|
||||
* </template>
|
||||
* </expand-button>
|
||||
*/
|
||||
export default {
|
||||
name: 'ExpandButton',
|
||||
data() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ariaLabel() {
|
||||
return __('Click to expand text');
|
||||
},
|
||||
computed: {
|
||||
ariaLabel() {
|
||||
return __('Click to expand text');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span>
|
||||
|
|
|
@ -1,78 +1,78 @@
|
|||
<script>
|
||||
import ciIconBadge from './ci_badge_link.vue';
|
||||
import loadingIcon from './loading_icon.vue';
|
||||
import timeagoTooltip from './time_ago_tooltip.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
import userAvatarImage from './user_avatar/user_avatar_image.vue';
|
||||
import CiIconBadge from './ci_badge_link.vue';
|
||||
import LoadingIcon from './loading_icon.vue';
|
||||
import TimeagoTooltip from './time_ago_tooltip.vue';
|
||||
import tooltip from '../directives/tooltip';
|
||||
import UserAvatarImage from './user_avatar/user_avatar_image.vue';
|
||||
|
||||
/**
|
||||
* Renders header component for job and pipeline page based on UI mockups
|
||||
*
|
||||
* Used in:
|
||||
* - job show page
|
||||
* - pipeline show page
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
ciIconBadge,
|
||||
loadingIcon,
|
||||
timeagoTooltip,
|
||||
userAvatarImage,
|
||||
/**
|
||||
* Renders header component for job and pipeline page based on UI mockups
|
||||
*
|
||||
* Used in:
|
||||
* - job show page
|
||||
* - pipeline show page
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
CiIconBadge,
|
||||
LoadingIcon,
|
||||
TimeagoTooltip,
|
||||
UserAvatarImage,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
itemName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
userAvatarAltText() {
|
||||
return `${this.user.name}'s avatar`;
|
||||
},
|
||||
computed: {
|
||||
userAvatarAltText() {
|
||||
return `${this.user.name}'s avatar`;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickAction(action) {
|
||||
this.$emit('actionClicked', action);
|
||||
},
|
||||
methods: {
|
||||
onClickAction(action) {
|
||||
this.$emit('actionClicked', action);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,76 +1,75 @@
|
|||
<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
|
||||
icon
|
||||
Sample configuration:
|
||||
|
||||
Sample configuration:
|
||||
<icon
|
||||
name="retry"
|
||||
:size="32"
|
||||
css-classes="top"
|
||||
/>
|
||||
|
||||
<icon
|
||||
name="retry"
|
||||
:size="32"
|
||||
css-classes="top"
|
||||
/>
|
||||
*/
|
||||
// only allow classes in images.scss e.g. s12
|
||||
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
|
||||
|
||||
*/
|
||||
// only allow classes in images.scss e.g. s12
|
||||
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
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,
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 16,
|
||||
validator(value) {
|
||||
return validSizes.includes(value);
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
spriteHref() {
|
||||
return `${gon.sprite_icons}#${this.name}`;
|
||||
},
|
||||
iconSizeClass() {
|
||||
return this.size ? `s${this.size}` : '';
|
||||
},
|
||||
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: {
|
||||
spriteHref() {
|
||||
return `${gon.sprite_icons}#${this.name}`;
|
||||
},
|
||||
iconSizeClass() {
|
||||
return this.size ? `s${this.size}` : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -79,7 +78,8 @@
|
|||
:width="width"
|
||||
:height="height"
|
||||
:x="x"
|
||||
:y="y">
|
||||
:y="y"
|
||||
>
|
||||
<use v-bind="{ 'xlink:href':spriteHref }" />
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import { __ } from '~/locale';
|
||||
import LabelsSelect from '~/labels_select';
|
||||
import LoadingIcon from '../../loading_icon.vue';
|
||||
|
@ -98,11 +99,18 @@ export default {
|
|||
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
|
||||
handleClick: this.handleClick,
|
||||
});
|
||||
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
|
||||
},
|
||||
methods: {
|
||||
handleClick(label) {
|
||||
this.$emit('onLabelClick', label);
|
||||
},
|
||||
handleCollapsedValueClick() {
|
||||
this.$emit('toggleCollapse');
|
||||
},
|
||||
handleDropdownHidden() {
|
||||
this.$emit('onDropdownClose');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -112,6 +120,7 @@ export default {
|
|||
<dropdown-value-collapsed
|
||||
v-if="showCreate"
|
||||
:labels="context.labels"
|
||||
@onValueClick="handleCollapsedValueClick"
|
||||
/>
|
||||
<dropdown-title
|
||||
:can-edit="canEdit"
|
||||
|
@ -133,7 +142,10 @@ export default {
|
|||
:name="hiddenInputName"
|
||||
:label="label"
|
||||
/>
|
||||
<div class="dropdown">
|
||||
<div
|
||||
class="dropdown"
|
||||
ref="dropdown"
|
||||
>
|
||||
<dropdown-button
|
||||
:ability-name="abilityName"
|
||||
:field-name="hiddenInputName"
|
||||
|
|
|
@ -26,6 +26,11 @@ export default {
|
|||
return labelsString;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
this.$emit('onValueClick');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -36,6 +41,7 @@ export default {
|
|||
data-placement="left"
|
||||
data-container="body"
|
||||
:title="labelsList"
|
||||
@click="handleClick"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
|
|
|
@ -472,6 +472,7 @@ img.emoji {
|
|||
.append-right-20 { margin-right: 20px; }
|
||||
.append-bottom-0 { margin-bottom: 0; }
|
||||
.append-bottom-5 { margin-bottom: 5px; }
|
||||
.append-bottom-8 { margin-bottom: $grid-size; }
|
||||
.append-bottom-10 { margin-bottom: 10px; }
|
||||
.append-bottom-15 { margin-bottom: 15px; }
|
||||
.append-bottom-20 { margin-bottom: 20px; }
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
border-color: $gray-darkest;
|
||||
}
|
||||
|
||||
[data-toggle="dropdown"] {
|
||||
[data-toggle='dropdown'] {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
@ -172,7 +172,11 @@
|
|||
color: $brand-danger;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.disable-hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:not(.disable-hover):hover,
|
||||
&:active,
|
||||
&:focus,
|
||||
&.is-focused {
|
||||
|
@ -508,17 +512,16 @@
|
|||
}
|
||||
|
||||
&.is-indeterminate::before {
|
||||
content: "\f068";
|
||||
content: '\f068';
|
||||
}
|
||||
|
||||
&.is-active::before {
|
||||
content: "\f00c";
|
||||
content: '\f00c';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.dropdown-title {
|
||||
position: relative;
|
||||
padding: 2px 25px 10px;
|
||||
|
@ -724,7 +727,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.dropdown-menu-due-date {
|
||||
.dropdown-content {
|
||||
max-height: 230px;
|
||||
|
@ -854,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
|
|||
}
|
||||
|
||||
.projects-list-frequent-container,
|
||||
.projects-list-search-container, {
|
||||
.projects-list-search-container {
|
||||
padding: 8px 0;
|
||||
overflow-y: auto;
|
||||
|
||||
li.section-empty.section-failure {
|
||||
color: $callout-danger-color;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header,
|
||||
|
@ -867,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
|
|||
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 {
|
||||
position: relative;
|
||||
padding: 4px $gl-padding;
|
||||
|
@ -905,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
|
|||
}
|
||||
|
||||
.projects-list-item-container {
|
||||
.project-item-avatar-container
|
||||
.project-item-metadata-container {
|
||||
.project-item-avatar-container .project-item-metadata-container {
|
||||
float: left;
|
||||
}
|
||||
|
||||
|
|
|
@ -40,10 +40,6 @@
|
|||
.project-home-panel {
|
||||
padding-left: 0 !important;
|
||||
|
||||
.project-avatar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.project-repo-buttons,
|
||||
.git-clone-holder {
|
||||
display: none;
|
||||
|
|
|
@ -468,6 +468,14 @@
|
|||
margin-bottom: 10px;
|
||||
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
|
||||
.ci-job-dropdown-container:hover .build-content {
|
||||
@extend .build-content:hover;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
}
|
||||
|
||||
.ide-view {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: calc(100vh - #{$header-height});
|
||||
margin-top: 0;
|
||||
|
@ -880,6 +881,26 @@
|
|||
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 {
|
||||
height: 200px;
|
||||
background-color: $white-light;
|
||||
|
|
|
@ -23,6 +23,9 @@ module AuthenticatesWithTwoFactor
|
|||
#
|
||||
# Returns nil
|
||||
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)
|
||||
|
||||
session[:otp_user_id] = user.id
|
||||
|
|
|
@ -165,8 +165,8 @@ module IssuableCollections
|
|||
[:project, :author, :assignees, :labels, :milestone, project: :namespace]
|
||||
when 'MergeRequest'
|
||||
[
|
||||
:source_project, :target_project, :author, :assignee, :labels, :milestone,
|
||||
head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
|
||||
:target_project, :author, :assignee, :labels, :milestone,
|
||||
source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
|
||||
]
|
||||
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]
|
||||
|
||||
Gitlab.config.omniauth.providers.each do |provider|
|
||||
define_method provider['name'] do
|
||||
handle_omniauth
|
||||
end
|
||||
def handle_omniauth
|
||||
omniauth_flow(Gitlab::Auth::OAuth)
|
||||
end
|
||||
|
||||
if Gitlab::Auth::LDAP::Config.enabled?
|
||||
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
|
||||
define_method server['provider_name'] do
|
||||
ldap
|
||||
end
|
||||
end
|
||||
Gitlab.config.omniauth.providers.each do |provider|
|
||||
alias_method provider['name'], :handle_omniauth
|
||||
end
|
||||
|
||||
# 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.message if exception.respond_to?(:message)
|
||||
error ||= env["omniauth.error.type"].to_s
|
||||
|
||||
error.to_s.humanize if error
|
||||
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
|
||||
if current_user
|
||||
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
|
||||
omniauth_flow(Gitlab::Auth::Saml)
|
||||
end
|
||||
|
||||
def omniauth_error
|
||||
|
@ -117,25 +72,36 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
|
||||
private
|
||||
|
||||
def handle_omniauth
|
||||
def omniauth_flow(auth_module, identity_linker: nil)
|
||||
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'])
|
||||
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
|
||||
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
|
||||
handle_disabled_provider
|
||||
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
|
||||
handle_signup_error
|
||||
end
|
||||
|
||||
def redirect_identity_exists
|
||||
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
|
||||
|
||||
def handle_service_ticket(provider, ticket)
|
||||
|
@ -144,21 +110,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
session[:service_tickets][provider] = ticket
|
||||
end
|
||||
|
||||
def continue_login_process
|
||||
# Only allow properly saved users to login.
|
||||
if @user.persisted? && @user.valid?
|
||||
log_audit_event(@user, with: oauth['provider'])
|
||||
def sign_in_user_flow(auth_user_class)
|
||||
auth_user = auth_user_class.new(oauth)
|
||||
user = auth_user.find_and_update!
|
||||
|
||||
if @user.two_factor_enabled?
|
||||
params[:remember_me] = '1' if remember_me?
|
||||
prompt_for_two_factor(@user)
|
||||
if auth_user.valid_sign_in?
|
||||
log_audit_event(user, with: oauth['provider'])
|
||||
|
||||
set_remember_me(user)
|
||||
|
||||
if user.two_factor_enabled?
|
||||
prompt_for_two_factor(user)
|
||||
else
|
||||
remember_me(@user) if remember_me?
|
||||
sign_in_and_redirect(@user)
|
||||
sign_in_and_redirect(user)
|
||||
end
|
||||
else
|
||||
fail_login
|
||||
fail_login(user)
|
||||
end
|
||||
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
|
||||
handle_disabled_provider
|
||||
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
|
||||
handle_signup_error
|
||||
end
|
||||
|
||||
def handle_signup_error
|
||||
|
@ -178,18 +150,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
@oauth ||= request.env['omniauth.auth']
|
||||
end
|
||||
|
||||
def fail_login
|
||||
error_message = @user.errors.full_messages.to_sentence
|
||||
def fail_login(user)
|
||||
error_message = user.errors.full_messages.to_sentence
|
||||
|
||||
return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
|
||||
end
|
||||
|
||||
def fail_ldap_login
|
||||
flash[:alert] = 'Access denied for your LDAP account.'
|
||||
|
||||
redirect_to new_user_session_path
|
||||
end
|
||||
|
||||
def fail_auth0_login
|
||||
flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
|
||||
|
||||
|
@ -208,6 +174,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
.for_authentication.security_event
|
||||
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?
|
||||
request_params = request.env['omniauth.params']
|
||||
(request_params['remember_me'] == '1') if request_params.present?
|
||||
|
|
|
@ -32,6 +32,7 @@ class UsersFinder
|
|||
users = by_active(users)
|
||||
users = by_external_identity(users)
|
||||
users = by_external(users)
|
||||
users = by_2fa(users)
|
||||
users = by_created_at(users)
|
||||
users = by_custom_attributes(users)
|
||||
|
||||
|
@ -76,4 +77,15 @@ class UsersFinder
|
|||
|
||||
users.external
|
||||
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
|
||||
|
|
|
@ -81,6 +81,14 @@ module GitlabRoutingHelper
|
|||
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)
|
||||
if entity.is_a?(Issue)
|
||||
toggle_subscription_project_issue_path(entity.project, entity)
|
||||
|
|
|
@ -27,6 +27,7 @@ module Ci
|
|||
|
||||
has_one :metadata, class_name: 'Ci::BuildMetadata'
|
||||
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!
|
||||
|
@ -604,6 +605,7 @@ module Ci
|
|||
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
|
||||
.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
|
||||
.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
|
||||
.concat(deploy_token_variables)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -654,6 +656,15 @@ module Ci
|
|||
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
|
||||
options&.dig(:environment, :url) || persisted_environment&.external_url
|
||||
end
|
||||
|
|
|
@ -31,12 +31,13 @@ module Avatarable
|
|||
|
||||
asset_host = ActionController::Base.asset_host
|
||||
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,
|
||||
# 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
|
||||
# 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
|
||||
only_path = false
|
||||
end
|
||||
|
@ -49,6 +50,6 @@ module Avatarable
|
|||
url_base << gitlab_config.relative_url_root
|
||||
end
|
||||
|
||||
url_base + avatar.url
|
||||
url_base + avatar.local_url
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,7 +31,7 @@ module ProtectedRef
|
|||
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_level.check_access(user)
|
||||
end
|
||||
|
|
|
@ -102,7 +102,7 @@ module Routable
|
|||
# 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.
|
||||
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
|
||||
end
|
||||
|
@ -124,6 +124,11 @@ module Routable
|
|||
end
|
||||
end
|
||||
|
||||
# Group would override this to check from association
|
||||
def owned_by?(user)
|
||||
owner == user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_path_errors
|
||||
|
|
|
@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base
|
|||
add_authentication_token_field :token
|
||||
|
||||
AVAILABLE_SCOPES = %i(read_repository read_registry).freeze
|
||||
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'.freeze
|
||||
|
||||
default_value_for(:expires_at) { Forever.date }
|
||||
|
||||
|
@ -17,6 +18,10 @@ class DeployToken < ActiveRecord::Base
|
|||
|
||||
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!
|
||||
update!(revoked: true)
|
||||
end
|
||||
|
|
|
@ -125,6 +125,10 @@ class Group < Namespace
|
|||
self[:lfs_enabled]
|
||||
end
|
||||
|
||||
def owned_by?(user)
|
||||
owners.include?(user)
|
||||
end
|
||||
|
||||
def add_users(users, access_level, current_user: nil, expires_at: nil)
|
||||
GroupMember.add_users(
|
||||
self,
|
||||
|
|
|
@ -68,6 +68,11 @@ class Project < ActiveRecord::Base
|
|||
|
||||
after_save :update_project_statistics, if: :namespace_id_changed?
|
||||
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_repository_updated_at
|
||||
after_update :update_forks_visibility_level
|
||||
|
@ -231,6 +236,7 @@ class Project < ActiveRecord::Base
|
|||
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
|
||||
|
||||
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 :project_feature, update_only: true
|
||||
|
@ -1041,13 +1047,6 @@ class Project < ActiveRecord::Base
|
|||
"#{web_url}.git"
|
||||
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?
|
||||
return true if fork_network && fork_network.root_project != self
|
||||
|
||||
|
@ -1879,6 +1878,10 @@ class Project < ActiveRecord::Base
|
|||
[]
|
||||
end
|
||||
|
||||
def gitlab_deploy_token
|
||||
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def storage
|
||||
|
@ -2004,10 +2007,11 @@ class Project < ActiveRecord::Base
|
|||
|
||||
def fetch_branch_allows_maintainer_push?(user, branch_name)
|
||||
check_access = -> do
|
||||
next false if empty_repo?
|
||||
|
||||
merge_request = source_of_merge_requests.opened
|
||||
.where(allow_maintainer_to_push: true)
|
||||
.find_by(source_branch: branch_name)
|
||||
|
||||
merge_request&.can_be_merged_by?(user)
|
||||
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"
|
||||
|
||||
# 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
|
||||
prop_accessor :token
|
||||
validates :token, presence: true, if: :activated?
|
||||
|
@ -34,7 +80,7 @@ class FlowdockService < Service
|
|||
data[:before],
|
||||
data[:after],
|
||||
token: token,
|
||||
repo: project.repository.path_to_repo,
|
||||
repo: project.repository,
|
||||
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
|
||||
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%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
|
||||
|
||||
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
|
||||
def self.protected?(project, ref_name)
|
||||
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(: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
|
||||
|
||||
with_options scope: :subject, score: 0
|
||||
|
@ -43,7 +43,11 @@ class GroupPolicy < BasePolicy
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
include GitlabRoutingHelper
|
||||
include StorageHelper
|
||||
include TreeHelper
|
||||
include ChecksCollaboration
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
presents :project
|
||||
|
@ -170,9 +171,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def files_anchor_data
|
||||
|
@ -200,7 +203,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
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,
|
||||
label: _('New file'),
|
||||
link: project_new_blob_path(project, default_branch || 'master'),
|
||||
|
@ -209,7 +212,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
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,
|
||||
label: _('Add Readme'),
|
||||
link: add_readme_path)
|
||||
|
@ -221,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
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,
|
||||
label: _('Add Changelog'),
|
||||
link: add_changelog_path)
|
||||
|
@ -233,7 +236,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
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,
|
||||
label: _('Add License'),
|
||||
link: add_license_path)
|
||||
|
@ -245,7 +248,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
end
|
||||
|
||||
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,
|
||||
label: _('Add Contribution guide'),
|
||||
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
|
||||
OpenStruct.new(enabled: auto_devops_enabled?,
|
||||
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?
|
||||
OpenStruct.new(enabled: true,
|
||||
label: _('Auto DevOps enabled'),
|
||||
|
|
|
@ -138,8 +138,10 @@ module QuickActions
|
|||
'Remove assignee'
|
||||
end
|
||||
end
|
||||
explanation do
|
||||
"Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}."
|
||||
explanation do |users = nil|
|
||||
assignees = issuable.assignees
|
||||
assignees &= users if users.present? && issuable.allows_multiple_assignees?
|
||||
"Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}."
|
||||
end
|
||||
params do
|
||||
issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
|
||||
|
@ -268,6 +270,26 @@ module QuickActions
|
|||
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'
|
||||
explanation 'Adds a todo.'
|
||||
condition do
|
||||
|
|
|
@ -65,6 +65,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
|
|||
!!model
|
||||
end
|
||||
|
||||
def local_url
|
||||
File.join('/', self.class.base_dir, dynamic_segment, filename)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Designed to be overridden by child uploaders that have a dynamic path
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
.help-block
|
||||
Manage repository storage paths. Learn more in the
|
||||
= 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
|
||||
%h4 Circuit breaker
|
||||
.form-group
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
%hr
|
||||
%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))
|
||||
= 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
|
||||
git add README.md
|
||||
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
|
||||
%h5 Existing folder
|
||||
|
@ -69,7 +71,9 @@
|
|||
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
|
||||
git add .
|
||||
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
|
||||
%h5 Existing Git repository
|
||||
|
@ -78,8 +82,10 @@
|
|||
cd existing_repo
|
||||
git remote rename origin old-origin
|
||||
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
|
||||
git push -u origin --all
|
||||
git push -u origin --tags
|
||||
- if @project.can_current_user_push_to_default_branch?
|
||||
%span><
|
||||
git push -u origin --all
|
||||
git push -u origin --tags
|
||||
|
||||
- if can? current_user, :remove_project, @project
|
||||
.prepend-top-20
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
- unless @repository.gitlab_ci_yml
|
||||
= 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
|
||||
|
||||
.content-list.builds-content-list
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
%ul
|
||||
- pipeline.yaml_errors.split(",").each do |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
|
||||
.bs-callout.bs-callout-warning
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
docker login #{Gitlab.config.registry.host_port}
|
||||
%br
|
||||
%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 }
|
||||
%br
|
||||
%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_errors(@project)
|
||||
%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
|
||||
= f.label :runners_token, "Runner token", class: 'label-light'
|
||||
.form-control.js-secret-value-placeholder
|
||||
|
|
|
@ -12,10 +12,22 @@
|
|||
%button.btn.js-settings-toggle{ type: 'button' }
|
||||
= expanded ? 'Collapse' : 'Expand'
|
||||
%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
|
||||
= 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) }
|
||||
.settings-header
|
||||
%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')
|
||||
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
|
||||
.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',
|
||||
'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