Merge remote-tracking branch 'upstream/master' into qa-add-more-key-tests
* upstream/master: (86 commits) Fix unassign slash command preview Add CHANGELOG entry Show Runner's description on job's page Fix an N+1 for MRs from forks on the MR index page Improve documentation of SSRF protection Replace find file project spinach tests with RSpec Fix docs typo for ci/lint [Backport] Burndown chart for group milestone Bump lograge to 0.10.0 and remove monkey patch Add Capybara debugging methods to docs Update CHANGELOG.md for 10.7.1 Add missing changelog entry Resolve "Avatar URLs are wrong when using a CDN path and Object Storage" Fix users not seeing labels from private groups when being a member of a child project Update doorkeeper for: Update links to /ci/lint with ones to project ci/lint Resolve "Namespace factory is problematic" Auth::User classes refactor adds should_save? Replace define_method with alias_method in Omniauth Controllers Describe workaround when restore fails because of `Errno::EBUSY` ...
This commit is contained in:
commit
0a8430375f
312 changed files with 3677 additions and 1576 deletions
|
@ -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)
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
10.7.0-pre
|
||||
10.8.0-pre
|
||||
|
|
245
app/assets/javascripts/ide/components/file_finder/index.vue
Normal file
245
app/assets/javascripts/ide/components/file_finder/index.vue
Normal file
|
@ -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>
|
113
app/assets/javascripts/ide/components/file_finder/item.vue
Normal file
113
app/assets/javascripts/ide/components/file_finder/item.vue
Normal file
|
@ -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;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
11
app/assets/javascripts/ide/lib/keymap.json
Normal file
11
app/assets/javascripts/ide/lib/keymap.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
[
|
||||
{
|
||||
"id": "file-finder",
|
||||
"label": "File finder",
|
||||
"bindings": ["CtrlCmd+KEY_P"],
|
||||
"action": {
|
||||
"name": "toggleFileFinder",
|
||||
"params": true
|
||||
}
|
||||
}
|
||||
]
|
|
@ -137,6 +137,9 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
|
|||
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
|
||||
};
|
||||
|
||||
export const toggleFileFinder = ({ commit }, fileFindVisible) =>
|
||||
commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
|
||||
|
||||
export * from './actions/tree';
|
||||
export * from './actions/file';
|
||||
export * from './actions/project';
|
||||
|
|
|
@ -42,4 +42,17 @@ 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);
|
||||
|
|
|
@ -58,3 +58,5 @@ export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
|
|||
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
|
||||
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
|
||||
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
|
||||
|
||||
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
|
||||
|
|
|
@ -100,6 +100,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,
|
||||
});
|
||||
|
|
|
@ -42,6 +42,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';
|
||||
|
|
4
app/assets/javascripts/lib/utils/keycodes.js
Normal file
4
app/assets/javascripts/lib/utils/keycodes.js
Normal file
|
@ -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;
|
|
@ -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.')));
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -876,6 +877,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
|
||||
|
|
31
app/controllers/ldap/omniauth_callbacks_controller.rb
Normal file
31
app/controllers/ldap/omniauth_callbacks_controller.rb
Normal file
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -1879,6 +1885,10 @@ class Project < ActiveRecord::Base
|
|||
[]
|
||||
end
|
||||
|
||||
def gitlab_deploy_token
|
||||
@gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def storage
|
||||
|
|
16
app/models/project_ci_cd_setting.rb
Normal file
16
app/models/project_ci_cd_setting.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -260,7 +260,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' : ''
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
40
app/views/projects/settings/ci_cd/_autodevops_form.html.haml
Normal file
40
app/views/projects/settings/ci_cd/_autodevops_form.html.haml
Normal file
|
@ -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
|
5
changelogs/unreleased/45666-project-ci-lint-links.yml
Normal file
5
changelogs/unreleased/45666-project-ci-lint-links.yml
Normal file
|
@ -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 missing changelog type to docs
|
||||
merge_request: 18526
|
||||
author: "@blackst0ne"
|
||||
type: other
|
5
changelogs/unreleased/dz-add-2fa-filter-admin-api.yml
Normal file
5
changelogs/unreleased/dz-add-2fa-filter-admin-api.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add 2FA filter to users API for admins only
|
||||
merge_request: 18503
|
||||
author:
|
||||
type: changed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix direct_upload when records with null file_store are used
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix a case with secret variables being empty sometimes
|
||||
merge_request: 18400
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Respect visibility options and description when importing project from template
|
||||
merge_request: 18473
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added Webhook SSRF prevention to documentation
|
||||
merge_request: 18532
|
||||
author:
|
||||
type: other
|
5
changelogs/unreleased/ide-file-finder.yml
Normal file
5
changelogs/unreleased/ide-file-finder.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added fuzzy file finder to web IDE
|
||||
merge_request:
|
||||
author:
|
||||
type: added
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Removed alert box in IDE when redirecting to new merge request
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fixed IDE not loading for sub groups
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fixed IDE not showing loading state when tree is loading
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/issue_45463.yml
Normal file
5
changelogs/unreleased/issue_45463.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix users not seeing labels from private groups when being a member of a child project
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Support Markdown rendering using multiple projects
|
||||
merge_request:
|
||||
author:
|
||||
type: performance
|
5
changelogs/unreleased/sh-bump-lograge.yml
Normal file
5
changelogs/unreleased/sh-bump-lograge.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Bump lograge to 0.10.0 and remove monkey patch
|
||||
merge_request:
|
||||
author:
|
||||
type: other
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix N+1 queries when loading participants for a commit note
|
||||
merge_request:
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Show Runner's description on job's page
|
||||
merge_request: 17321
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/update-doorkeeper-changelog.yml
Normal file
5
changelogs/unreleased/update-doorkeeper-changelog.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication
|
||||
merge_request: 18543
|
||||
author:
|
||||
type: fixed
|
|
@ -1,131 +1,4 @@
|
|||
# rubocop:disable GitlabSecurity/PublicSend
|
||||
|
||||
require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible
|
||||
|
||||
class Settings < Settingslogic
|
||||
source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" }
|
||||
namespace Rails.env
|
||||
|
||||
class << self
|
||||
def gitlab_on_standard_port?
|
||||
on_standard_port?(gitlab)
|
||||
end
|
||||
|
||||
def host_without_www(url)
|
||||
host(url).sub('www.', '')
|
||||
end
|
||||
|
||||
def build_gitlab_ci_url
|
||||
custom_port =
|
||||
if on_standard_port?(gitlab)
|
||||
nil
|
||||
else
|
||||
":#{gitlab.port}"
|
||||
end
|
||||
|
||||
[
|
||||
gitlab.protocol,
|
||||
"://",
|
||||
gitlab.host,
|
||||
custom_port,
|
||||
gitlab.relative_url_root
|
||||
].join('')
|
||||
end
|
||||
|
||||
def build_pages_url
|
||||
base_url(pages).join('')
|
||||
end
|
||||
|
||||
def build_gitlab_shell_ssh_path_prefix
|
||||
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
|
||||
|
||||
if gitlab_shell.ssh_port != 22
|
||||
"ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
|
||||
else
|
||||
if gitlab_shell.ssh_host.include? ':'
|
||||
"[#{user_host}]:"
|
||||
else
|
||||
"#{user_host}:"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_base_gitlab_url
|
||||
base_url(gitlab).join('')
|
||||
end
|
||||
|
||||
def build_gitlab_url
|
||||
(base_url(gitlab) + [gitlab.relative_url_root]).join('')
|
||||
end
|
||||
|
||||
# check that values in `current` (string or integer) is a contant in `modul`.
|
||||
def verify_constant_array(modul, current, default)
|
||||
values = default || []
|
||||
unless current.nil?
|
||||
values = []
|
||||
current.each do |constant|
|
||||
values.push(verify_constant(modul, constant, nil))
|
||||
end
|
||||
values.delete_if { |value| value.nil? }
|
||||
end
|
||||
|
||||
values
|
||||
end
|
||||
|
||||
# check that `current` (string or integer) is a contant in `modul`.
|
||||
def verify_constant(modul, current, default)
|
||||
constant = modul.constants.find { |name| modul.const_get(name) == current }
|
||||
value = constant.nil? ? default : modul.const_get(constant)
|
||||
if current.is_a? String
|
||||
value = modul.const_get(current.upcase) rescue default
|
||||
end
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
def absolute(path)
|
||||
File.expand_path(path, Rails.root)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_url(config)
|
||||
custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
|
||||
|
||||
[
|
||||
config.protocol,
|
||||
"://",
|
||||
config.host,
|
||||
custom_port
|
||||
]
|
||||
end
|
||||
|
||||
def on_standard_port?(config)
|
||||
config.port.to_i == (config.https ? 443 : 80)
|
||||
end
|
||||
|
||||
# Extract the host part of the given +url+.
|
||||
def host(url)
|
||||
url = url.downcase
|
||||
url = "http://#{url}" unless url.start_with?('http')
|
||||
|
||||
# Get rid of the path so that we don't even have to encode it
|
||||
url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
|
||||
|
||||
URI.parse(url_without_path).host
|
||||
end
|
||||
|
||||
# Runs every minute in a random ten-minute period on Sundays, to balance the
|
||||
# load on the server receiving these pings. The usage ping is safe to run
|
||||
# multiple times because of a 24 hour exclusive lock.
|
||||
def cron_for_usage_ping
|
||||
hour = rand(24)
|
||||
minute = rand(6)
|
||||
|
||||
"#{minute}0-#{minute}9 #{hour} * * 0"
|
||||
end
|
||||
end
|
||||
end
|
||||
require_dependency File.expand_path('../../lib/gitlab', __dir__) # Load Gitlab as soon as possible
|
||||
|
||||
# Default settings
|
||||
Settings['ldap'] ||= Settingslogic.new({})
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue